JVM GC的心酸路程

首先看一下JVM的内存分布

image.png

再复习一下 如何辨别垃圾

1.计数法 被引用了+1 不被引用了-1 缺陷: 无法解决循环问题。

image.png 这几个对象因为相互引用 永远无法被回收

2.GCRoot(可达性分析) 可达性算法的原理是以一系列叫做 GC Root 的对象为起点出发,引出它们指向的下一个节点,再以下个节点为起点,引出此节点指向的下一个结点,到下一节点,到下一节点,到下一节点,会形成一个链,最后那些没有出现在这些GCRoot链中的节点便是可以回收的节点

image.png 其中C便是可以认为被回收

GCRoot可以是 1.虚拟机栈中的引用对象(局部变量) 2.类静态成员变量 3.方法区中常量引用的对象 4.方法区中JNI引用的对象

JVM如何开始进行GC

1.OopMap: JVM(HotSpot为例)会报一些类信息放到OoMap这个数据结构里,主要记录着什么偏移量上是什么样的数据类型,是在类加载的时候放到这里的,这样就可以不用每次去扫所有的类挨个去找类的引用关系了。

2.安全点(SafePoint):有了OopMap,HotSpot就可以快速准确的完成GC Root枚举,JVM在进行GCRoot枚举以及进行垃圾回收的时候会Stop The World 为了防止用户线程在GC线程进行RCRoot枚举时或回收时 更改掉已经标记出的节点的引用,而设计的一种方案,当开始进行GCRoot枚举时,用户线程到达这个安全点变会暂停运行,这些安全点的选举基本上以“是否具有让程序长时间执行的特征”。

2.1 那么如何让线程(不包含JNI调用的线程)到指定的安全点停下来呢? 2.1.1 抢先式中断:在垃圾收集发生时,系统首先会把所有用户线程全部中断,如果发现用户线程中断不在安全点上,就恢复让该线程继续执行,直到跑到安全点(几乎没有虚拟机用) 2.1.2主动式中断:不直接对线程操作,仅仅设置一个标识位,各个线程回去loop这个标识位(使用内存保护陷阱的方式),一旦发现中断标识位为真时自己就在最近的安全点主动挂起

3.安全区:安全点过多也不是很好呀(可以看一下 有哪些东西可以作为安全点),所以又设计出安全区这个概念,就是用户线程执行到安全区里面的代码的时候,首先会标识自己进入安全区了,那么这段时间内如果虚拟机发生GC Root枚举或者进行垃圾收集的时候,便会跨过这些线程,当线程要离开安全区时,它会去检查虚拟机是否完成了GC Root枚举或者垃圾收集过程中其它需要暂停线程的阶段,如果完成了,那就啥事没有继续执行,否则就一直在等待。

image.png

JVM垃圾算法

1.标记清除 标记出来要清理的对象,在进行清除 缺点:效率比较低 会产生大量的内存碎片 这些碎片太多可能会造成之后的对象无法得到相应的内存而再造成GC

2.标记复制 该算法会把内存分为两个区,每次使用其中的一块,发生GC时,会把未回收的放到另一块内存中,之后剩余的进行清除 算法简单清晰 缺点:浪费空间 每次都只能使用一半内存 对象存活率较高的时候会很不划算

3.标记整理 使存活的对象往一段走 之后在把存活边界后面的全部清理掉 既解决了碎片化的问题又解决了复制算法的对象存活高情况下的内存浪费问题 缺点:很复杂 消耗资源 适合老年代

回头再说说JVM跨代引用问题

1.回顾一下现在JVM使用的分代回收算法 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率高,没有额外空间对它进行分配担保,所以使用标记整理算法。

2.跨代引用问题: 如果一个老年代的对象引用指向了年轻代,那年轻代的进行垃圾回收的时候,被引用的这个在年轻代的对象会怎样处理呢? 在进行GCRoot扫描的时候会再扫一遍老年区么? 答案是肯定不会的 因为这样的话意味着是不是每次进行Minor GC的时候 还要去扫一遍老年?那岂不是相当于进行了一次 次级别Full GC ,所以JVM是怎样解决这个问题的呢?答案是卡表。

先笼统地介绍下记忆集(基本思想): 记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。JVM会把所有跨代的引用放到一起,年轻代发生GC时 被引用的对象不会被扫出去 而是会增加年龄 直至进入老年代 这样跨代的问题就没有了

卡表(记忆集的实现): 在hotspot虚拟机中卡表是一个字节数组,数组的每一项对应内存中某一块连续的地址区域,如果该区域中有引用只想了待回收区域的对象,卡表对应的元素将被设置成1,没有则设置成0

image.png

如图:每块连续的内存地址向右移动9位 便是它在卡表上的位置了

再说说写屏障: 卡表上的 这些数据是如何变脏的呢? 何时变脏很明确,有其它分代区域对象引用本区域对象时候,其对应卡表元素就应该变脏,变脏的时间点应该是发在引用赋值那一刻,所以这个之后就可以通过写屏障指令来维护卡表状态的 类似于AOP切面 。

卡表副作用: 1.写屏障的性能开销 2.内存“伪共享” 当多个线程修改相互独立的变量时,恰好这些变量都在一个缓存行,就会彼此影响(写回、无效化或者同步)导致性能降低

JDK 7 以后 HotSpot虚拟机增加一个新的参数 -XX:+UseCondCardMark 用来决定是否开启卡表更新。

引:深入理解java虚拟机 第三版 周志明

猜你喜欢

转载自juejin.im/post/7034872110606450701
今日推荐