绝大多数的 Java 对象存活时间都非常短,很多时候就是在一个方法内创建对象,对象引用放在栈中,当方法调用结束,栈帧出栈的时候,这个对象就失去引用了,成为垃圾。
也就是说,对于新创建的对象和经过多次回收之后还没有回收的对象,在处理的方式上是不一致的。
针对这种情况,JVM 将堆空间分成新生代(young)和老年代(old)两个区域,创建对象的时候,只在新生代创建,当新生代空间不足的时候,只对新生代进行垃圾回收,这样需要处理的内存空间就比较小,垃圾回收速度就比较快。而针对老年代,可以采取延后回收的方式。
新生代又分为 Eden 区、From 区和 To 区三个区域,新创建的对象都会放在 Eden 区,而每次垃圾回收,都是扫描 Eden 区和 From 区,将存活对象复制到 To 区,然后交换 From 区和 To 区的名称引用,下次垃圾回收的时候继续将存活对象从 From 区复制到 To 区。当一个对象经过几次新生代垃圾回收,也就是几次从 From 区复制到 To 区以后,依然存活,那么这个对象就会被复制到老年代区域。
HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。
<aside> 💡 存在特定情况,假设 To 区无法容纳存活之后的所有对象,这时候就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion),也就是说从老年代中借用部分空间
</aside>
当老年代空间已满,也就是无法将新生代中多次复制后依然存活的对象复制进去的时候,就会对新生代和老年代的内存空间进行一次全量垃圾回收,即 Full GC 。所以根据应用程序的对象存活时间,合理设置老年代和新生代的空间比例对 JVM 垃圾回收的性能有很大影响,
<aside> 💡 JVM 设置老年代新生代比例的参数是 -XX:NewRatio。
</aside>
这是 JVM 早期的垃圾回收器,只有一个线程执行垃圾回收。但事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint) 最小的;
对于单核处理器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。
对于新生代,采取复制算法多线程回收,对于老年代采取标记整理算法
启动多线程执行垃圾回收。如果 JVM 运行在多核CPU 上,那么显然并行垃圾回收要比串行垃圾回收效率高。
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器