在Java程序运行时,Java虚拟机管理的内存区域,其中程序计数器、虚拟机栈和本地方法栈这个三个区域随线程而生,随线程而灭,因此,这几个区域的内存分配和回收都具备确定性,当方法结束或者线程结束时,内存自然就跟着回收了。
Java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序运行期间时才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器(Garbage Collection, GC)主要关注的是这部分内存。Java垃圾回收集中在堆区,方法区涉及到类型的卸载,而且回收的效果也并不是太好,基本可以忽略不计。上面讲到的是垃圾回收的内存区域,垃圾回收还涉及到另外两个问题:

  1. 如何识别出一个对象是要回收的垃圾对象;
  2. 一旦识别出垃圾对象后,如何回收

识别垃圾对象

垃圾对象的识别主要有两种方法:引用计数法和可达性分析。引用计数法由于存在相互引用的问题,Java虚拟机并没有使用,在Java中使用的是可达性分析的方法。
可达性分析方法的思路是从一些被称为GC ROOTS的对象作为起点,从这些对象节点向下搜索,搜索走过的路径被称为引用链,当一个对象到GC ROOTS没有任何引用链相连时,则证明该对象不可用。在Java语言中,以下四种对象可以作为GC ROOTS对象:

  • 虚拟机栈中引用的对象;
  • 方法区中静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈JNI引用的对象;

比如下面方法中创建的Rain对象,在创建完毕后,rain持有对这个对象的引用,那这个对象是可以被访问到。当对象引用被设置成null后,就没有途径可以获取到该对象,那么该对象就不可达。

  1. Rain rain = new Rain();
  2. rain = null;

垃圾收集方法

垃圾收集方法有四种:标记-清除、标记-整理、复制算法以及分代算法。

  • 标记-清除算法

标记-清除算法分为两个阶段:第一个阶段标记出可回收的对象,第二个阶段回收相应的对象,标记-清除会产生内存碎片的问题。

  • 标记-整理算法

标记-整理算法是在标记-清除的基础上,对内存进行整理。

  • 复制算法

复制算法将内存分为两块:from区和to区,每次内存垃圾回收,将存活的对象从from区转移到to区。这样存在的一个问题是内存浪费比较严重。

  • 分代算法

对于需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串及数组。大对象对虚拟机的内存分配来说就是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前出发垃圾收集以获取足够的连续空间来“安置”它们。虚拟机设置了一个阈值,当对象大于这个阈值时就直接在老年代中分配。
一个有效的管理内存方法是把内存空间划分为不同代,这样垃圾回收器就不用扫描整个堆区。大多数对象的生命周期都很短暂,那些生命周期较长的对象往往直到应用退出后才清除。JVM把堆划分为新生代和老年代,在新生代中,GC可以快速的标记回收“死对象”,而不需要扫描整个Heap中的存活较长时间的“老对象”。
HotSpot又把新生代进一步划分为3个区域:一个相对大一些的区域,称为“Eden区”;两个相对小一点的区域,称为“From幸存区(survivor)”和“To幸存区(survivor)”。当一个Java应用创建了一个对象,这个对象就被存储到Eden中(如果对象过大,会被存储在老年代中)。一旦新生代存储满了,就会在新生代触发一次minor gc(小范围的垃圾回收)。新生代的GC使用复制算法。在GC前,To幸存区(survivor)会被清空,对象保存在Eden区和From幸存区(survivor)中。GC运行时,Eden中的幸存对象被复制到To幸存区(survivor)。针对From幸存区(survivor)中的幸存对象,会考虑对象年龄,如果没有达到阈值(tenuring threshold),对象会被复制到To 幸存区(survivor)。如果达到阈值,则被复制到老年代中。复制阶段完成后,Eden 和From 幸存区中只保存死对象,可以视为清空。如果在复制过程中To 幸存区被填满了,剩余的对象会被复制到老年代中。最后 From 幸存区和 To幸存区会调换下名字,在下次GC时,To 幸存区会成为From 幸存区,如下图所示,其中黄色表示死对象,绿色表示剩余空间,红色表示幸存对象。
虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放入新生代,哪些对象放入老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加一岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。
image.png
当一个对象存活到一定的周期后,它就会被移动到堆中的老年代(tenured pool)。最后,当老年代被填满时,就会被触发一次full gc或者major gc(完全的垃圾回收),以清理老年代。
一般我们把初生池和幸存池所在的区域合并成为新生代,把老年代所在的区域称为老年代。对应的,在新生代上产生的gc称为minor gc,在老年代上产生的gc称为full gc。当垃圾回收执行的时候,所有的应用线程都要被停止,系统产生一次暂停。minor gc非常频繁,所以被优化的能够快速的回收死对象,是新生代的内存的主要回收方式。major gc运行起来用相对慢得多,因为要扫描非常多的活着的对象。

survivor区为什么要定义两个

垃圾收集器

Java虚拟机之垃圾收集器 - 图2新生代垃圾收集器:Serial、Parallel Scavenge、ParNew GC
老年代垃圾收集器:Parallel old、CMS GC、Serial Old
整堆垃圾收集器:G1、ZGC

指标

垃圾回收需要关注几个指标: Java虚拟机之垃圾收集器 - 图3

参考

https://www.jianshu.com/p/2a1b2f17d3e4
https://blog.csdn.net/zerohuan/article/details/50451269
https://juejin.cn/post/6844903782107578382