美文网首页Java基础
JVM(二) GC算法与分代回收策略

JVM(二) GC算法与分代回收策略

作者: Timmy_zzh | 来源:发表于2020-11-14 07:21 被阅读0次
  1. 可达性分析

  2. GCRoot场景

  3. 垃圾回收算法

  4. 分代回收策略

  5. 引用

垃圾回收

  • 垃圾回收(Garbage Collection,简写为GC),相较于C语言开发需要手动释放内存,jvm中的垃圾回收器会自动回收

  • 程序运行时的内存区域,其中程序计数器,虚拟机栈和本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出执行者入栈和出栈操作,这三个区域都不需要过多考虑内存回收的问题

  • 而堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,只有在程序处于运行期间才能知道会创建那些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注就是这部分内存

什么是垃圾
  • 垃圾就是内存中已经没有用的对象。要进行垃圾回收,必须先知道那些对象是垃圾;

  • java虚拟机中使用一种叫做可达性分析的算法来决定那些对象是否可以被回收

可达性分析

可达性分析算法是从离散数学中的图论引入的,jvm把内存中所有的对象之间的引用关系看作一张图,通过一组名为GC Root的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:

1.可达性分析.png
  • 上图中,对象A/B/C/D/E与GC Root之间都存在一条直接或间接的引用链,表示他们与GC Root之间是可达的,因此他们是不能被GC回收掉的。

  • 而对象M,K虽然被J引用到,但是并不存在一条引用链连接他们与GC Root,所以当GC进行垃圾回收时,只要遍历到J/K/M,3个对象那个就会将他们回收

GC Root对象

在java中,有以下几种对象可以作为GC Root:

  1. Java虚拟机栈(局部变量表)中引用的对象

  2. 方法区中静态引用指向的对象

  3. 仍处于存活状态中的线程对象

  4. Native方法中JNI引用的对象

GC回收时机
  1. Allocation Failure:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次GC

  2. System.gc():在应用层,Java开发工程师可以主动调用此API来请求一次GC

垃圾回收算法

1.标记清除算法

从GC Roots集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分为两步:

  • 1.Mark标记阶段:找到内存中的所有GC Root对象,只要是和GC Root对象直接或间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)

  • 2.Sweep清除阶段:当遍历完所有的GC Root之后,则将标记为垃圾的对象直接清除

如下图所示:

2.标记清除算法.png
  • 优缺点:

    • 优点:实现简单,不需要将对象进行移动

    • 缺点:这个算法需要中断进程内其他组件的执行(Stop The World),并且可能产生内存碎片,提高了垃圾回收频率

2.复制算法

将现有的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

  • 1.复制算法之前,内存分为A/B两块,并且当前只是用内存A,内存状况如下图
3.复制算法1.png
  • 标记完之后,所有可达对象都被按次序复制到内存B中,并设置B为当前使用中的内存。
3.复制算法2.png
  • 优缺点

    • 优点:按顺序分配内存即可,实现简单,运行高校,不用考虑内存碎片

    • 缺点:可用的内存大小缩小为原来的一般,对象存活率高时会频繁进行复制操作

3.标记-压缩算法

需要先从根节点开始对所有可达对象做一次标记,之后,并不是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一段。最后,清理边界外所有的空间。

  • 1.Mark标记阶段:找到内存中所有GC Root对象,只要是和GC Root对象直接或间接相连则标记为灰色存活对象,否则标记为黑色垃圾对象

  • 2.Compact压缩阶段:将剩余存活对象按顺序压缩到内存的某一端

4.标记压缩算法.png
  • 优缺点

    • 优点:这种方法即避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高

    • 缺点:所谓压缩操作,需要对局部对象移动,所以一定程度上还是降低了效率

JVM分代回收策略

  • java虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代,老年代,这就是JVM的内存分代策略

    • 注意在HotSpot中除了新生代和老年代,还有永久代
  • 分代回收的中心思想就是:对于新创建的对象会在新生代中分配内存,此区域的对象生命周期一般较短。如果经过多次回收仍然存活下来,则将他们转移到老年代中。

年轻代
  • 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,回收率很高。

  • 新生代中因为要进行一些复制操作,所以一般采用的GC回收算法是复制算法

  • 新生代可以继续细分为3部分:Eden,Survivor0(简称S0),Survivor1(S1).这三部分

    按照8:1:1的比例来划分新生代

  1. 绝大多数刚刚被创建的对象会存放在Eden区,如图
5.分代策略-新生成对象存放在Eden区1.png
  1. 当Eden区第一次满的时候,会进行垃圾回收。首先将Eden区的垃圾对象回收清除,并将存活的对象复制到S0,此时S1是空的。如图
5.分代策略2.png
  1. 下一次Eden区满时,再执行一次垃圾回收,此次会将Eden和S0区中所有垃圾对象清除,并将存活对象复制到S1,此时S0变为空。如图
5.分代策略3.png
  1. 如此反复在S0和S1之间切换几次(默认15次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将他们转移到老年代中。如图
5.分代策略4.png
老年代
  • 一个对象如果在新生代存活了足够长的时候而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。

    • 如果对象比较大(比如字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。

    • 可以使用-XX:PretenureSizeThreshold 来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

  • 老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法

    • 注意:对于老年代可能存在一种情况,老年代中的对象有时候会引用到新生代对象,这时如果要执行新生代GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然时低效的。

    • 所以,老年代中维护了一个512byte的card table,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生GC时,只需要检查这个card table即可,大大提高了性能。

GC Log分析

JVM提供了相应的GC日志,在GC执行垃圾回收事件的时候,会有各种相应的log被打印出来,其中新生代与老年代的打印日志是有区别的。

  • 新生代GC:这一区域的GC叫做Minor GC,因为Java对象大多朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。

  • 老年代GC:发生在这一区域的GC也叫做Major GC或者Full GC。当出现一了Major GC,经常伴随至少一次的Minor GC

    • 在有些虚拟机实现中,Major GC只是代表回收老年代内存,而Full GC则代表回收整个堆中的内存,也就是新生代+老年代

GC Log分析相关的Java命令参数:

6.GCLog分析参数.png

Java引用

判断对象是否存活可以通过GC Roots的引用可达性来判断,JVM中的引用关系有四种,根据引用强度由强到弱,分别是:强引用,软引用,弱引用,虚引用

7.Java引用2.png
软引用隐藏问题
  • 被软引用对象关联的对象会自动被垃圾回收器回收,在内存紧张时,GC会将SoftObject所占用内存回收,

  • 但是每一个SoftReference又被Set所引用(强引用),最终结果是内存不足的情况下也不会被回收

  • 优化:注册一个引用队列,每次循环之后将引用队列中出现的软引用对象从cache中移除。

/**
 * 软引用:只会在内存不足的情况下被回收
 * 在有集合持有的情况下处理
 */
public class _6SoftReferenceTest {

    static class SoftObject {
        byte[] data = new byte[1024];  //1kb
    }

    public static int removeSoftRefs = 0;
    public static int CACHE_INITL_CAPACITY = 100 * 1024;
    //静态集合保存软引用,会导致这些软引用对象本身无法被垃圾回收器回收
    public static Set<SoftReference<SoftObject>> cache = new HashSet<>();
    public static ReferenceQueue<SoftObject> referenceQueue = new ReferenceQueue<>();

    public static void main(String[] args) {
        for (int i = 0; i < CACHE_INITL_CAPACITY; i++) {
            SoftObject obj = new SoftObject();
            cache.add(new SoftReference<SoftObject>(obj, referenceQueue));

            clearUselessReference();
            if (i % 10000 == 0) {
                System.out.println("size of cache:" + cache.size());
            }
        }
        System.out.println("end removeSoftRefs:" + removeSoftRefs);
    }

    private static void clearUselessReference() {
        Reference<? extends SoftObject> reference = referenceQueue.poll();
        while (reference != null) {
            if (cache.remove(reference)) {
                removeSoftRefs++;
            }
            reference = referenceQueue.poll();
        }
    }
}

相关文章

网友评论

    本文标题:JVM(二) GC算法与分代回收策略

    本文链接:https://www.haomeiwen.com/subject/ndeibktx.html