- JVM的垃圾回收
- 垃圾回收算法
- 垃圾回收器
1.6 ★JVM的垃圾回收
- Java 中为了简化对象的释放,引入了自动的垃圾回收(Garbage Collection,简称 GC)机制。通过垃圾回收器来对不再使用的对象完成自动的回收,垃圾回收器主要负责对堆上的内存进行回收。
1.6.1 方法区回收
方法区中能回收的内容主要就是不再使用的类。
判定一个类可以被卸载。需要同时满足下面三个条件:
- 此类所有实例对象都已经被回收,在堆中不存在任何该类的实例对象以及子类对象。
- 加载该类的类加载器已经被回收。
- 该类对应的
java.lang.Class
对象没有在任何地方被引用。
1.6.2 堆回收
1.6.2.1 引用计数法和可达性分析法
如何判断堆上的对象可以回收?
- Java 中的对象是否能被回收,是根据对象是否被引用来决定的。如果对象被引用了,说明该对象还在使用,不允许被回收。
- 常见的判断方法有两种:引用计数法和可达性分析法。
引用计数法:为每个对象维护一个引用计数器,当对象被引用时加 1,取消引用时减 1。
- 引用计数法的优点是实现简单,但是它也存在缺点,主要有两点:
- 每次引用和取消引用都需要维护计数器,对系统性能会有一定的影响。
- 存在循环引用问题,所谓循环引用就是当 A 引用 B,B 同时引用 A 时会出现对象无法回收的问题。
- 引用计数法的优点是实现简单,但是它也存在缺点,主要有两点:
可达性分析法:Java 使用的是可达性分析算法来判断对象是否可以被回收,可达性分析将对象分为两类:垃圾回收的根对象(GC Root)和普通对象,对象与对象之间存在引用关系。
下图中 A 到 B 再到 C 和 D,形成了一个引用链,可达性分析算法指的是如果从某个对象到 GC Root 对象是可达的,那这个对象就不可被回收。
哪些对象被称之为 GC Root 对象呢?
- 线程 Thread 对象。
- 系统类加载器加载的
java.lang.Class
对象。 - 监视器对象,用来保存同步锁
synchronized
关键字持有的对象。 - 本地方法调用时使用的全局对象。
1.6.2.2 五种对象引用
强引用
- 可达性分析算法中描述的对象引用,一般指的是强引用,即是 GC Root 对象对普通对象有引用关系,只要这层关系存在,普通对象就不会被回收。除了强引用之外,Java 中还设计了几种其他引用方式。
软引用
- 软引用相对于强引用是一种比较弱的引用关系,如果一个对象只有软引用关联到它,当程序内存不足时,就会将软引用中的数据进行回收。软引用常用于缓存中。
- 软引用的执行过程如下:
- 将对象使用软引用包装起来,
new SoftReference<对象类型>(对象)
。 - 内存不足时,虚拟机尝试进行垃圾回收。
- 如果垃圾回收仍不能解决内存不足的问题,回收软引用中的对象。
- 如果依然内存不足,抛出
OutOfMemory
异常。
- 将对象使用软引用包装起来,
- 软引用的执行过程如下:
- 软引用中的对象如果在内存不足时回收,
SoftReference
对象本身也需要被回收(盒子里的东西已经被回收了,盒子也没有存在的必要了)。如何知道哪些SoftReference
对象需要回收呢?SoftReference
提供了一套队列机制:- 软引用创建时,通过构造器传入引用队列。
- 在软引用中包含的对象被回收时,该
SoftReference
对象会被放入引用队列。 - 通过代码遍历引用队列,将相关联的
SoftReference
的强引用删除。
弱引用
- 弱引用的整体机制和软引用基本一致,区别在于弱引用包含的对象在垃圾回收时,不管内存够不够都会被直接回收。在JDK 1.2 版之后提供了
WeakReference
类来实现弱引用,弱引用主要在ThreadLocal
中使用。弱引用对象本身也可以使用引用队列进行回收。
虚引用和终结器引用
- 这两种引用在常规开发中是不会使用的。
1.6.2.3 垃圾回收算法
- Java 是如何实现垃圾回收的呢?简单来说,垃圾回收要做的有两件事:
- 找到内存中存活的对象。
- 释放不再存活对象的内存,使得程序能再次利用这部分空间。
垃圾回收算法的评价标准
- Java 垃圾回收过程会通过单独的 GC 线程来完成,但是不管使用哪一种 GC 算法,都会有部分阶段需要停止所有的用户线程。这个过程被称之为
Stop The World
简称 STW,如果 STW 时间过长则会影响用户的使用。 - 判断 GC 算法是否优秀,可以从三个方面来考虑:
- 吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即
吞吐量 = 执行用户代码时间 / (执行用户代码时间 + GC 时间)
。- 吞吐量数值越高,垃圾回收的效率就越高。
- 最大暂停时间:所有垃圾回收过程中 STW 时间最大值。
- 堆使用效率:不同垃圾回收算法,对堆内存的使用方式是不同的。比如标记清除算法,可以使用完整的堆内存。而复制算法会将堆内存一分为二,每次只能使用一半内存。从堆使用效率上来说,标记清除算法要优于复制算法。
- 吞吐量:吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即
标记-清除算法
标记-清除算法的核心思想分为两个阶段:
- 标记阶段:将所有存活的对象进行标记,Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
- 清除阶段:从内存中删除没有被标记也就是非存活对象。
- 优点:
- 实现简单,只需要在第一阶段给每个对象维护标志位,第二阶段删除对象即可。
- 缺点:
- 两次遍历:标记-清楚算法需要两次遍历堆内存。
- 碎片化问题:由于内存是连续的,所以在对象被删除之后,内存中会出现很多细小的可用内存单元。如果我们需要的是一个比较大的空间,很有可能这些内存单元的大小过小而无法进行分配。
- 分配速度慢:由于内存碎片的存在,需要维护一个空闲链表,极有可能发生每次需要遍历到链表的最后才能获得合适的内存空间。
复制算法
复制算法的核心思想是:
- 准备两块空间
From
空间和To
空间,每次在对象分配阶段,只能使用其中一块空间(From
空间)。 - 在垃圾回收 GC 阶段,将
From
中的存活对象复制到To
空间。 - 将两块空间的
From
和To
名字互换(保证存活对象一定是在From
区中)。 - 将互换名字后的
To
空间中的对象清除。
- 优点:
- 吞吐量高:复制算法只需要遍历一次存活对象复制到
To
空间即可,比标记-整理算法少了一次遍历的过程,因而性能较好,但是不如标记-清除算法,因为标记清除算法不需要进行对象的移动。 - 不会发生碎片化:复制算法在复制之后就会将对象按顺序放入
To
空间中,所以对象以外的区域都是可用空间,不存在碎片化内存空间。
- 吞吐量高:复制算法只需要遍历一次存活对象复制到
- 缺点:
- 内存使用效率低:每次只能让一半的内存空间来为创建对象使用。
- 对象移动:在复制算法中需要进行对象的移动,会降低效率。
- 准备两块空间
标记-整理算法
标记-整理算法也叫标记压缩算法,是对标记-清理算法中容易产生内存碎片问题的一种解决方案。核心思想分为两个阶段:
- 标记阶段:将所有存活的对象进行标记,Java 中使用可达性分析算法,从 GC Root 开始通过引用链遍历出所有存活对象。
- 整理阶段:将存活对象移动到堆的一端,清理掉非存活对象的内存空间。
- 优点:
- 内存使用率高:整个堆内存都可以使用,不会像复制算法只能使用半个堆内存。
- 不会发生内存碎片化:在整理阶段可以将对象往内存的一侧进行移动,剩下的空间都是可以分配对象的有效空间。
- 缺点:
- 效率相对不高:既有标记-清除算法的两次遍历,又有复制算法的对象移动。
★分代GC
现代优秀的垃圾回收算法,会将上述描述的垃圾回收算法组合进行使用,其中应用最广的就是分代垃圾回收算法(Generational GC)。
- 分代垃圾回收将整个内存区域划分为新生代和老年代。
- 新生代:存放存活时间比较短的对象。
- 又分为伊甸园区
Eden
和两个幸存者区Survivor
(From
和To
)。
- 又分为伊甸园区
- 老年代:存放存活时间比较长的对象。
- 新生代:存放存活时间比较短的对象。
- 分代垃圾回收将整个内存区域划分为新生代和老年代。
分代 GC 算法的核心思想:
- 新创建的对象,都会先分配到 Eden 区。
- 当伊甸园区内存不足,标记伊甸园与 from 区的存活对象。
- 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸园和 from 内存都得到释放,from 区和 to 区互换名字。
- 经过一段时间后伊甸园的内存又出现不足,标记 Eden 区域和 from 区存活的对象,将其复制到 to 区,复制完毕后,from 区和 to 区互换名字。
- 当幸存区对象熬过几次回收(最多 15 次)后,就晋升到老年代(幸存区内存不足或大对象会提前晋升)。
- 当老年代中空间不足,无法放入新的对象时,先尝试
minor gc
,如果还是不足,就会触发Full GC
,Full GC
会对整个堆进行垃圾回收。 - 如果
Full GC
依然无法回收掉老年代的对象,那么当对象继续放入老年代时,就会抛出Out Of Memory
异常。
1.6.2.4 垃圾回收器
- 垃圾回收器是垃圾回收算法的具体实现。由于垃圾回收器分为新生代和老年代,除了 G1 之外其他垃圾回收器必须成对组合进行使用。具体关系图如下:
Serial-Serial Old
Serial
是一种单线程串行回收年轻代的垃圾回收器。Serial
使用复制算法。
SerialOld
是Serial
垃圾回收器的老年代版本,采用单线程串行回收。SerialOld
使用标记整理算法。
Parallel Scavenge-Parallel Old
Parallel Scavenge
是 JDK8 默认的年轻代垃圾回收器,多线程并行回收,关注的是系统的吞吐量。具备自动调整堆内存大小的特点。Parallel Scavenge
使用复制算法。
Parallel Old
是为Parallel Scavenge
收集器设计的老年代版本,利用多线程并发收集。Parallel Old
使用标记整理算法。
ParNew-CMS
ParNew
垃圾回收器本质上是对Serial
在多 CPU 下的优化,使用多线程进行垃圾回收。ParNew
使用复制算法。
CMS(Concurrent Mark Sweep)
垃圾回收器关注的是系统的暂停时间,允许用户线程和垃圾回收线程在某些步骤中同时执行,减少了用户线程的等待时间。CMS
使用标记清除算法。
CMS
的执行步骤:- 初始标记,用极短的时间标记出 GC Roots 能直接关联到的对象。
- 并发标记,标记所有的对象,用户线程不需要暂停。
- 重新标记,由于并发标记阶段有些对象会发生了变化,存在错标、漏标等情况,需要重新标记。
- 并发清理,清理死亡的对象,用户线程不需要暂停。
- 缺点:
CMS
使用了标记-清除算法,在垃圾收集结束之后会出现大量的内存碎片,CMS
会在Full GC
时进行碎片的整理,这样会导致用户线程暂停。- 无法处理在并发清理过程中产生的“浮动垃圾”,不能做到完全的垃圾回收。
- 如果老年代内存不足无法分配对象,
CMS
就会退化成Serial Old
单线程回收老年代。
★G1
JDK9 之后默认的垃圾回收器是
G1(Garbage First)
垃圾回收器。Parallel Scavenge
关注吞吐量,允许用户设置最大暂停时间,但是会减少新生代可用空间的大小。CMS
关注暂停时间,但是吞吐量方面会下降。
而 G1 设计目标就是将上述两种垃圾回收器的优点融合:
- 支持巨大的堆空间回收,并有较高的吞吐量。
- 支持多 CPU 并行垃圾回收。
- 允许用户设置最大暂停时间。
G1
出现之前的垃圾回收器,内存结构一般是连续的:G1
的整个堆会被划分成多个大小相等的区域,称之为区 Region,区域不要求是连续的。分为Eden
、Survivor
、Old
区。- Region 的大小通过堆空间大小 / 2048计算得到。
- 也可以通过参数
-XX:G1HeapRegionSize=32m
指定(其中 32m 指定 region 大小为32M),Region size 必须是 2 的指数幂,取值范围从 1M 到 32M。
G1
垃圾回收有两种方式:新生代回收(
Young GC
)- 回收 Eden 区和 Survivor 区中不用的对象。采用复制算法来完成。
混合回收(
Mixed GC
)回收所有新生代和部分老年代的对象以及大对象区。采用复制算法来完成。
G1
对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高,这也是G1(Garbage first)
名称的由来。
- 注意:如果清理过程中发现没有足够的空 Region 存放转移的对象,会出现
Full GC
,会单线程执行标记-整理算法,此时会导致用户线程的暂停。所以尽量保证应该用的堆内存有一定多余的空间。