分为新生代(Young Generation)和老年代(Old Generation)
其中新生代又分为:
其中 新生代三个区的比例默认为: 8:1:1
可通过 -XX:SurvivorRatio 参数调整,如-XX:SurvivorRatio=9
Minor GC(Young GC)
对新生代进行GC
Major GC(Old GC)
对老年代进行GC。目前只有CMS收集器能够单独收集老年代对象。
Full GC
对整个堆和方法区进行GC
eden和from区复制到to区
eden区满时触发第一次GC,将eden和from区活着的对象复制到to区,清理eden和from区的空间,同时将到to区的对象年龄+1,但如果已经到达到老年代的阈值(默认15,可通过-XX:MaxTenuringThreshold参数调整),则直接转移到老年代
交换from和to区
from和to区不是固定的,每次minorGC后,两个survivor区空闲的一块作为to区,非空闲的一块作为from区
Minor GC 的步骤1中,出现了to区不足以储存活下来的对象,则这些对象直接被转移到老年代,这个过程就是空间分配担保。
JDK8以后,在进行Minor GC前,如果老年代的连续空间大于新生代对象大小总和或历次晋升的平均大小,则进行Minor GC,否则进行Full GC。
如果Full GC后仍然内存不足,则抛出OOM.
对象优先在Eden区分配
大对象进入老年代
使用-XX:PretenureSizeThreshold
参数指定大于该值的对象直接进入老年代,值单位必须是字节,不能使用M或K,如使用2,097,152表示2M
长期存活的对象进入老年代
使用-XX:MaxTenuringThreshold
参数设置大于该年龄的对象进入老年代
在survivor空间中相同年龄所有对象总和大于survivor空间一半,则年龄大于等于该年龄的对象直接进入老年代
只要满足该条件,则无需达到-XX:MaxTenuringThreshold
参数指定的年龄
MinorGC时,如果to区无法满足存活下的对象的内存需求,则将其分配到老年代
标记出所有要回收的对象, 统一回收被标记的对象。
缺点:
如果对象过多,则会进行大量的标记清除,效率与对象数量成反比。
很明显,标记清除后一定会出现很多不连续的内存空间,此为内存碎片。如果有一个大对象,虽然总内存足够,但因过于碎片化,找不到连续的空间分配给大对象,就会造成一次GC,影响性能。
如下图:
将内存分为大小相等的两块,只使用一块。
当一块用完了,就将存活的对象复制到另一块,并回收这一块使用的内存。
因此。当存活对象过多时,复制的开销非常大。
而大量对象需要回收时,只需要对半个堆的少量对象进行复制,简单而高效,且不会产生碎片。
缺点:
可用内存缩小一半,空间浪费。
如下图:
但是Serial、ParNew等新生代收集器并没有采用这种1:1的划分策略,而是按照上面讲的8:1的比例进行标记-复制回收。
而使用标记-整理算法,该算法也是先对可回收对象进行标记,然后将所有存活的对象向内存的一端移动,再清理掉边界以外的所有内存。
如下图回收前和回收后的对比:
但是老年代中每次回收都有大量对象,移动后还需要更新对象的引用,且需要Stop The World(暂停用户程序)
与标记-复制相比:
1.标记整理需要移动,回收时较复杂,标记-复制不需要移动,但分配时较复杂(大量碎片)
2.复制的停顿时间短,整理停顿长
3.标记-整理算法吞吐量高
虽然标记-复制不需要移动效率高,但内存分配和访问比垃圾收集频率高得多,耗时增加,总体吞吐量仍然是下降的。
HotSpot没有为每个指令都生成OopMap,前面提到的在“特定的位置”记录下这些信息,这些位置称为安全点。
强制用户程序必须执行到安全点才能够暂停。就像高速开车只能在服务区停车休息一样。
抢先式中断
垃圾收集时,首先把所有用户线程全部中断,如果用户线程没有在安全点,就恢复该线程,直到到达安全点。
几乎不再使用该方式。
主动式中断
a. 垃圾收集需要中断线程时,仅简单地设置一个标志位,各线程不停地主动轮询该标志,一旦发现中断标志位为真,就在最近的安全点停下
b. 轮询标志是和安全点重合的
c. hotSpot使用内存保护陷阱的方式把轮询操作精简到只有一条汇编指令。
当程序“不执行”时,如用户线程在Sleep或Blocked,线程无法响应中断到安全点挂起自己,因此需要借助安全区域。
安全区域就是能够确保在某段代码中,引用关系不会发生变化,因此在这个区域任意位置进行GC都是安全的。(可理解为被扩展拉伸的安全点)
为了解决跨代引用问题,在新生代引入的记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个老年代加入GCRoots扫描范围。
垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引用指针的全部细节。
可以采用不同的记录粒度,以节省记忆集的存储和维护成本,如:
第三种卡精度是使用一种叫做“卡表”的方式实现记忆集,也是目前最常用的一种方式
记忆集是一种抽象概念,卡表是它的实现方式。它记录了记忆集的记录精度、与堆内存的映射关系等。
卡表是使用一个字节数组实现:CARD_TABLE[this addredd >>9]=0
,每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡页”。
hotSpot使用的卡页是2^9大小,即512字节
一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0.
GC时,只要筛选卡表中变脏的元素加入GCRoots。
卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1.
hotSpot使用写屏障维护卡表状态。
可看做在虚拟机层面对“引用类型字段赋值”动作的AOP切面,在赋值时产生一个环形通知。赋值前后都属于写屏障,赋值前称为“写前屏障(Pre-Write Barrier)”,赋值后称为“写后屏障(Post-Write Barrier)”。
虚拟机会为所有赋值操作生成相应的指令,一旦收集器在写屏障中增加了更新卡表操作,无论更新的是不是老年代对新生代的引用,每次只要对引用进行更新,就会产生额外的开销。
伪共享问题
并发场景下,当多个互相独立的变量被读取到一个缓存行时,会影响性能。
伪共享参考:https://blog.csdn.net/weixin_43696529/article/details/104884373
解决:
加条件
只有卡表元素未被标记时才将其标记为变脏。
-XX:+UseCondCardMark
参数设定是否开启卡表更新时的条件判断开启条件自然会增加一个判断开销,但能够避免伪共享问题。根据实际来。
可达性分析工作必须要在能保障一致性的快照中进行,因此必须停止用户进程。
如果用户进程被停止,那不会产生任何问题。
但如果用户进程和GC进程并发进行,就会出现两种后果:
使用三色标记来解释上述问题:
白色: 对象未被收集器访问(未扫描)
黑色:对象已被收集器访问,且该对象所有引用已扫描过。(安全存活)(扫描完毕)
灰色:对象被访问过,但对应至少还有一个引用没有扫描过。(正在扫描)
但如果用户线程在并发标记进行时修改了引用关系,如下情况会出现存活对象消亡的现象:
如上图:
同理,当标记到该图的B时。取消 了B到C的引用,添加了A到D的引用,但因为A已经标记过,因此D不会再被扫描,C也不会被扫描,这样D和C也因用户线程的修改“意外死亡”,这就是“对象消失“的问题。
原因1和2分别对应两个解决方案:
增量更新(CMS用到)
记录下新插入的引用,并发扫描完毕后,重新以记录下的引用关系的黑色对象为根扫描。
即黑色一旦插入了新的到白色的引用,就变成了灰色。
原始快照(G1和Shenandoah)
灰色对象要删除指向白色对象的引用时,将该引用记录下来,扫描完毕后,再从被记录下的引用的灰色对象开始重新扫描。
HotSpot主要采用直接指针进行对象访问。
======================================================================
其他相关笔记:
================================================================
参考:
《深入理解java虚拟机第三版》