JVM学习(二)
谁需要GC?
栈:栈中的生命周期是跟随线程,所以一般不需要关注
堆:堆中的对象是垃圾回收的重点
方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低。
GC如何判断对象是否该被回收?
引用计数
在JVM早期使用,计算对象被引用的次数。但是在对象相互引用时,很难判断对象是否该回收。
可达性分析:GC root
该方法的思路,就是通过被称为 “GC root” 的对象作为起点,从这些对象开始向下搜索,搜索的路径被称为引用链,当一个对象到GC root没有引用链相连时,证明该对象是可以被回收的。
GC Roots对象包括下面几种:
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。(Ps:方法内使用的对象)
- 本地方法栈中,Native方法引用的对象。
堆内存划分
堆内存分为老年代和新生代,划分比列为2 : 1。
新生代
新生代分为Eden空间、From survivor空间、To survivor空间,默认为8:1:1,其中From和To空间大小是一致的。
Minor GC 是发生在新生代的垃圾收集动作,所采用的是复制算法。
Eden space、Form survivor、To survivor
新生代分为一个Eden,两个Survivor空间。对象优先在Eden空间分配,如果Eden内存空间不足,就会发生Minor GC。并把存活对象复制到Survivor空间,由于新生代使用复制算法,所以有两个Survivor空间。
为什么新生代分区比例是8:1:1?
新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
Eden和Survivor的大小比例默认是8:1:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。
新生代的回收算法
新生代中,Minor GC比较频繁,对象存活低,使用用复制算法回收时效率也会更高,只需要付出少量存活对象的复制成本就可以完成收集,并且也不会产生内存碎片。
复制算法
将可用内存按容量划分为大小相等的两块(From survivor、To survivor),每次只使用其中一块,在垃圾回收的时候,将正在使用的内存中的存活对象复制到另一块内存区域中,然后清除正使用过的内存区域,交换两个区域的角色,完成垃圾回收。
这样每次Minor GC都是针对整个半区进行回收,内存分配时就无需考虑内存碎片等情况。只要按照顺序分配内存即可,实现简单,运行高效。缺点就是内存缩小为原来的一半。
老年代
老年代的回收算法
Full GC 是发生在老年代的垃圾收集动作。所采用的是标记-清除、标记-整理算法。
在老年代里面的对象几乎都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。所以,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长,也更加耗费性能。
标记-清除算法
先标记出所有需要回收的对象,在标记完成后统一清除所有被标记的对象。
标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
标记-整理算法
标记过程与“标记-清除”算法一样,首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
堆内存的分配策略
大对象会直接进入老年代
为什么大对象会直接进入老年代?大对象需要大量连续的内存空间,比如很长的字符串和大型数组。
1、导致内存有空间,还是需要进行垃圾回收获取连续空间来放他们,
2、会进行大量的内存复制。
长期存活的对象将进入老年代
对象优先在Eden空间分配,如果Eden内存空间不足,就会发生Minor GC,通过复制算法会把正在使用的对象移动到From空间,此时存活的对象年龄+1,后续每次Minor GC,都会复制存活对象进入From或To空间,并且存活对象年龄+1。直到对象达到指定年龄(默认15岁),会把该对象移入老年代。
动态年龄判断
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了指定年龄才能晋升老年代。
如果在Survivor空间中年龄相近对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到要求的年龄。
Ps:动态对象年龄判断,计算的是年龄从小到大进行累加,当加入某个年龄段后,累计超过Survivor区域一半的时候,就从这个年龄段往上的年龄的对象进行晋升。
空间分配担保
在发生Minor GC前,虚拟机会检查老年代的连续空间大于新生代对象的总大小?
- 大于:可以进行Minor GC。
- 小于:检查HandlePromotionFailure参数,是否允许担保失败。
- 允许:继续检测老年代的连续空间大于新生代对象的总大小或者历次晋升的平均大小。若大于,将尝试进行一次Minor GC,若失败,则进行一次Full GC。
- 不允许:老年代进行Full GC。
新生代使用复制收集算法,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况是内存回收之后,新生代中所有的对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
老年代要进行这样的担保,前提是老年代还有容纳这些对象的剩余空间,但是Minor GC过后还有多少对象存活下,在Minor GC前是无法确定的。所以只好取历次晋升老年代对象的平均大小作为参考,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
为什么堆内存要分区?
因为要针对不同的区域做不同的垃圾回收算法,不同的区域回收频次是不一样的
分代收集
这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块,
根据各个年代的特点采用最适当的收集算法。新生代采用复制算法,老年代采用标记-清除、标记-整理算法。
垃圾收集器
收集器 | 收集对象 | 收集器类型 |
---|---|---|
Serial | 新生代,复制算法 | 单线程 |
ParNew | 新生代,复制算法 | 并行的多线程收集器 |
Parallel Scavenge | 新生代,复制算法 | 并行的多线程收集器 |
收集器 | 收集对象 | 收集器类型 |
---|---|---|
Serial Old | 老年代,标记整理算法 | 单线程 |
Parallel Old | 老年代,标记整理算法 | 并行的多线程收集器 |
CMS | 老年代,标记清除算法 | 并行与并发收集器 |
G1 | 跨新生代和老年代;标记整理 + 化整为零 | 并行与并发收集器 |
单线程收集
多线程收集(并行收集)
CMS收集器
CMS会有浮动垃圾,因为CMS收集器是并发收集,在清理的同时,用户线程还在运行,所以会再产生新的垃圾
并行:垃圾收集的多线程的同时进行。
并发:垃圾收集的多线程和应用的多线程同时进行。