新生代
收集器
Parallel Scavenge
复制
多线程
并行
关注吞吐量
什么时候触发Young GC
新生代空间不足时:新分配对象时,发现 Eden 空间不足,此时,会触发 Young GC
具体过程
Eden 区存活对象:复制到 Survivor 1 区
Survivor 2 区中存活对象:复制到 Survivor 1 区
Eden 区 和 Survivor 1 区:清空
什么时候触发对象进入「老年代」
对象年龄
对象在「新生代」中survivor 区,存活的年龄,达到阈值(默认为 15),则,进入「老年代」
-XX:MaxTenuringThreshold
同龄对象过半
年龄动态计算,对象在「新生代」中survivor 区,同龄对象大小之和,超过了 survivor 区的 50%,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值,集体进入「老年代」
引入动态年龄计算,主要基于如下两点考虑
如果固定按照MaxTenuringThreshold设定的阈值作为晋升条件
MaxTenuringThreshold设置的过大,原本应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了
MaxTenuringThreshold设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的Major GC。分代回收失去了意义,严重影响GC性能
相同应用在不同时间的表现不同:特殊任务的执行或者流量的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题
大对象
大对象,在「新生代」放不下,直接进入「老年代」
老年代引用新生代的对象,GC怎么处理
因为跨代引用,这样在<font color="#c41230">Young GC</font>必须扫描整个老年代来判断对象是否存活
怎么避免,经过统计信息显示,老年代持有新生代对象引用的情况不足1%
卡标记 Card Marking
卡表 Card Table,字节数组结构,卡表的每个标记项为1个字节
老年代划分为卡页,每个512B,如果老年代对象发生了修改,或者老年代对象指向了新生代对象,就把这个老年代对象所在的 Card 标记为脏 dirty。Young GC 时,dirty card 加入待扫描的 GC Roots 范围,避免扫描整个老年代
单独存储一个卡表,标识哪个卡页存在指向年轻代的引用
存在指向年轻代对象的卡页,对应的卡表记录,被标记为 dirty
扫描 dirty的卡表记录,从而避免全堆扫描
我引用了谁 points-out结构
标识了哪个「老年代」区域,存在指向「年轻代」对象的引用
写屏障 Write Barrier
Incremental update,只要在写屏障(write barrier)里发现要有一个白对象的引用被赋值到一个黑对象 的字段里,那就把这个白对象变成灰色的。即插入的时候记录下来
问题
无条件写屏障带来的性能开销
每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作
高并发下虚共享(false sharing)带来的性能开销
假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行
HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB
如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能
解决
先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty,这样可以减少并发写操作
JDK7 -XX:+UseCondCardMark
反之不成立
老年代的 GC 是低频操作
新生代,绝大多数对象,存活时间非常短
如果记录「新生代」到「老年代」的引用,会耗费比较多的存储空间
关于 CMS 垃圾收集器:并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如
新生代的对象晋升到老年代
直接在老年代分配对象
老年代对象的引用关系发生变更
...
Full GC、Magjor GC、Minor GC、Young GC 之间的关系
Full GC == Major GC指的是对老年代/永久代的stop the world的GC
Full GC的次数 = 老年代GC时 STW 的次数
Full GC的时间 = 老年代GC时 STW 的总时间
CMS ≠ Full GC,CMS 分为多个阶段,只有 STW 的阶段被计算到了Full GC的次数和时间,而和业务线程并发的 GC 的次数和时间则不被认为是Full GC
Full GC本身不会先进行 Young GC,我们可以配置,让 Full GC之前先进行一次 Young GC,因为老年代很多对象都会引用到新生代的对象,先进行一次Young GC可以提高老年代GC的速度
各分区的大小对GC的性能影响很大。如何将各分区调整到合适的大小,分析活跃数据的大小是很好的切入点
活跃数据的大小是指,应用程序稳定运行时长期存活对象在堆中占用的空间大小,也就是Full GC后堆中老年代占用空间的大小。可以通过GC日志中Full GC之后老年代数据大小得出,比较准确的方法是在程序稳定后,多次获取GC数据,通过取平均值的方式计算活跃数据的大小
总大小,3-4 倍活跃数据的大小
新生代,1-1.5 活跃数据的大小
老年代,2-3 倍活跃数据的大小
永久代,1.2-1.5 倍Full GC后的永久代空间占用