垃圾回收简析

JVM进行垃圾回收主要有两个步骤

1.标记哪些对象是引用不可达,可回收的对象
2.对这些可回收对象,使用合适的算法进行清理

JVM是如何标记可回收对象的?

可达性分析法:

GC Roots: 一组活跃的,可以作为根节点对象引用的集合
Reference Chain :每一个GC Root向下查找,搜索产生的链路,就是引用链
引用可达对象:能够与根节点保持引用链的对象
引用不可达对象:能够与根节点保持引用链的对象

GC roots.png

举一个不太恰当的例子来理解,就是jvm维护一个Gc root集合:[root1,root2,root3...rootn],然后遍历集合的每一个root,每一个root都进行一次引用链的搜索,能够产生引用链的对象标记为可达,那么其他对象,都是不可达不可达的。

GC Roots 都有哪些?
活动线程相关的各种引用。
类的静态变量。
JNI引用。
注意点:GC Roots都是引用,不是对象,存放在虚拟机栈上。

JVM是如何快速枚举GC Roots 的?
1、遍历栈里所有的变量,逐一进行类型判断,如果是 Reference 类型,则属于 GC Roots。
2、从外部记录下栈里那些 Reference 类型变量的类型信息,存成一个映射表 -- 这就是 OopMap 的由来 。

“在解释执行时/JIT时,记录下栈上某个数据对应的数据类型,比如该数据为Reference 类型
现在三种主流的高性能JVM实现,HotSpot、JRockit和J9都是这样做的。其中,HotSpot把这样的数据结构叫做 OopMap ,JRockit里叫做livemap,J9里叫做GC map。”

GC 时,直接根据这个 OopMap 就可以快速实现根节点枚举了。

引用级别
存在引用链的对象,就一定是存活的吗?这里将衍生四个引用级别:
强引用:即使内存不足,并且发生OOM和GC ,也不会被清理,只有切断GC root才会被清理。
软引用:维护一些可有可无的对象。内存足够时,不会清理。内存不足时,才会被清理。适合缓存中使用,如Guava Cache.
弱引用:维护一些生命周期更短的对象。无论内存是否充足,GC时都会被清理。
虚引用:一种形同虚设的引用。主要用来跟踪对象被垃圾回收的活动。如下,GC时,

private static void startMonitoring(ReferenceQueue<MyObject> referenceQueue, Reference<MyObject> ref) {
     ExecutorService ex = Executors.newSingleThreadExecutor();
     ex.execute(() -> {
         while (referenceQueue.poll()!=ref) {
             //don't hang forever
             if(finishFlag){
                 break;
            }
        }
         System.out.println("-- ref gc'ed --");
    });
     ex.shutdown();
}

还有JDK9中cleaner的应用。

引用计数法

对象头维护一个counter计数器,被引用一次+1,引用失效-1。但是存在循环依赖问题,目前主流jvm厂商均不会采取该方式。

JVM是如何回收垃圾,并且都是用什么算法回收?

标记-清除算法

1.标记阶段,使用可达性分析法,进行对象可达性标记。
2.清理阶段,对于标记为可达的对象进行保留,其余不可达的对象直接清理掉


image.png

优点:简单
缺点:可能产生大量的不连续的内存碎片,进而导致空间利用率下降,垃圾收集频率变高。

复制算法

1.标记阶段,使用可达性分析法,进行对象可达性标记。
2.复制阶段,将标记为可达的对象,复制到另一个空闲的内存空间,原来的内存空间全部进行清除。
优点:效率高,没有内存碎片产生。
缺点:需要浪费多一个同等的内存空间。

标记-整理算法

1.标记阶段,使用可达性分析法,进行对象可达性标记。
2.复制阶段,将标记为可达的对象,整理的内存空间的一侧,然后这一侧以外的对象,直接清楚。
优点:不需要额外的空间,也不会产生内存碎片
缺点:效率低。整理过程复杂。

以上算法,均各有优劣,如何集上述算法所长于一体?

弱代假设:大部分对象死的快,小部分对象存活很长时间。
基于弱代假设,分代算法出现了。

分代算法

分代算法在逻辑上或物理上,将内存中对象进行分区。死得快的对象所在的分区称为年轻代。存活很长时间的对象所在的分区,称为老年代。

年轻代

年轻代又分为一个Eden,和两个survivor区。当Eden内存不足时,会触发年轻代的GC,也称为Minor GC。年轻代的GC采用复制算法。
TLAB的分配也是在Eden,是jvm用来加速对象分配的。

老年代

老年代一般采用标记-清除,或者标记整理算法进行GC。
对象如何进入老年代的?
1.提升。Minor Gc过后,对象年龄+1,当达到一定年龄,进入老年代,可配置。
2.分配担保。年轻代每次GC后,如果存活对象大于survivor区,就会依赖老年代进行分配担保。部分存活对象直接晋升到老年代。
3.大对象直接在老年代分配。超过某个值大小的对象,直接在老年代分配,可配置。
4.动态年龄判定。survivor幸存区中,相同年龄对象对象大小的和,大于幸存区的一半。比如年龄为15的对象,存活大于幸存区的一半,那么大于等于年龄15的对象,将直接进入老年代。

跨代引用的对象,如何处理?

年轻代进行Minor Gc时,是在年轻代这个区域单独进行的,不会涉及老年代的空间。但是,如果一个年轻代的对象,引用了老年代的对象,这个时候,年轻代的对象就不该被清理掉,如何保障这种跨代引用?
JVM采用的是卡片标记的方式,就是在老年代维护一个卡表(原理类似bitmap),minor gc时读取下老年代的卡表,能够快速判断年轻代的对象是否有引用老年代的对象。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。