Java 的一大特点就是可以自动进行垃圾回收,不要开发人员去关心内存资源的释放情况。
本章主要记录垃圾回收方法和 Hot Spot 虚拟机支持的垃圾回收器。
垃圾回收要解决的问题
- 那些对象需要被回收?
- 何时回收这些对象?
- 如何回收这些对象?
如何判断那些对象需要被回收
参考文章:JVM 如何判断一个对象可被回收?
垃圾回收算法与思想
-
标记-清除算法
标记-清除将垃圾回收分为两个节点:标记阶段和清除阶段。在标记阶段,首先通过根节点(GC Root)标记从根节点开始的可达对象,未被标记的就是垃圾对象。在清除阶段将清除所有未被标记的对象。
缺点: 空间碎片
如图所示,回收后的空间是不连续的。在对象的对空间分配过程中,尤其是大对象的内存分配,不连续的内存空间的工作效率要低于连续空间的工作效率。 -
复制算法
核心思想:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换内存的角色,完成垃圾回收。
缺点: 内存减半
在 Java 的新生代串行垃圾回收器中,使用了复制算法的思想。新生代分为 eden 、s0 和 s1 3部分。其中 s0 、s1 可以视为用于复制的两块大小相同,地位相等,且可进行角色互换的空间块。s0 和 s1 用于存放未被回收的对象。
复制算法比较使用与新生代。因为新生代垃圾对象通常多于存活对象,复制算法的效果比较好。
-
标记-压缩算法
复制算法适合在新生代中使用,但是在老年代中,大部分对象是存活对象。如果使用复制算法,由于存活的对象比较多,复制成本会很高。所以老年的垃圾回收需要使用新的算法。标记压缩算法是一种老年代的回收算法,其在标记-清除上做了一些优化。
其在清除的过程中,将所有存活的对象压缩到内存的一端。然后清除边界外的所有空间,这样既避免了空间碎片,又不需要两块相同的内存空间。
-
增量算法
在垃圾回收的过程中,应用软件处于一种 Stop the World 的状态。应用程序的所有线程都会挂起,暂停正常工作,等待垃圾回收完成。
基本思想: 让垃圾回收线程和应用线程交替执行。每次,垃圾回收线程只收集一小片区域的内存空间,接着切换到应用线程。如此反复,直到垃圾收集完成。
但是,因为线程切换和上下文转化的消耗,会使垃圾回收的总成本上升,系统的吞吐量下降。 -
分代
以 Hot Spot 虚拟机为例,年轻代使用复制算法,老年代使用标记-整理算法。
垃圾收集器
按线程数分
- 串行垃圾回收器
- 并行垃圾回收器
按工作模式
- 并发式垃圾回收器
- 独占式垃圾回收器
按碎片处理方式
- 压缩式垃圾回收器
- 非压缩式垃圾回收器
按工作的内存空间
- 新生代垃圾回收器
- 老年代垃圾回收器
-
新生代串行收集器
串行收集器主要有两个特点:
- 它仅仅使用单线程进行垃圾回收
- 它是独占式的垃圾回收
在串行收集器进行垃圾回收时,Java 应用程序中的线程都要暂停,等待垃圾回收完成。
新生代串行处理器使用复制算法,实现简单,逻辑处理高效,并且没有线程切换的开销。
在 Hot Spot 虚拟机中,使用 -XX:+UserSerialGC 参数指定使用新生代串行收集器和老年代串行收集器。在客户端(Client)模式运行时,它是默认的垃圾收集器。 -
老年代串行收集器
老年代串行收集器使用的是标记-压缩算法。和新生代串行收集器一样,它也是串行、独占式的垃圾回收器。
若要使用老年代串行收集器:
- -XX:+UseSerialGC:新生代、老年代都使用串行收集器
- -XX:+UseParNewGC:新生代使用并行收集器,老年代使用串行收集器(可以和 CMS 垃圾收集器一起使用)
- -XX:+UseParallelGC:新生代使用并行回收回收器,老年代使用串行收集器(不可以和 CMS 垃圾回收器一起使用)
- 并行收集器
只是简单的将串行回收器多线程化。
- -XX:+UseParNewGC:新生代使用并行收集器,老年代使用串行收集器
- -XX:+UseConcMarkSweepGC:新生代使用并行收集器,老年代使用 CMS
并行收集器工作时的线程数可以使用 -XX:ParallelGCThreads 指定。默认情况下,CPU 个数小于 8 时,其值等于 CPU 的数量;当 CPU 数量大于 8 时,其值为 3+[(5 * CPU_Count)/8]
- 新生代并行回收(Parallel Scavenge)收集器
新生代并行回收收集器也是使用复制算法的收集器。表面来看,其和并行收集器一样,同样是多线程和独占式的垃圾回收。但是并行回收收集器的特点是: 其十分关注系统的吞吐量。
-
-XX:+UseParallelGC:新生代使用并行回收收集器,老年代使用串行收集器
-
-XX:+UseOldParallelGC:新生代和老年代都使用并行回收收集器
并行回收收集器提供了两个重要的参数用于控制系统的吞吐量:
(1)-XX:MaxGCPauseMillis:设置最大垃圾收集的停顿时间,它的值是大于0的一个整数。
(2)-XX:GCTimeRatio:设置吞吐量大小,其值是 0~100 之间的整数。假设 GCTimeRatio 为 n,系统用于垃圾回收的时间不超过 1/(1+n)。默认 n=99,即不超过 1% 的时间。除了以上的不同,并行回收收集器和并行收集器的不同之处还在于,其支持自适应的 GC 调整策略。使用 -XX:+UseAdaptiveSizePolicy 可以打开自适应调整策略。
在手工调优比较困难的场合下,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆(-Xmx)、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMillis),让虚拟机自己完成调优工作。
-
老年代并行回收收集器
老年代并行回收收集器使用标记-压缩算法,在 JDK 1.6 中才可以使用。
- -XX:+UseParallelOldGC
- -XX:ParallelGCThreads
-
CMS 收集器(Concurrent Mark Sweep)
和并行回收收集器不同,CMS 收集器主要关注于系统的停顿时间。其使用的标记清除算法,同时也是一个使用多线程并行回收的垃圾收集器。
CMS 主要步骤有:初始标记、并发标记、重新标记、并发清除和并发重置是可以和用户线程一起执行的。
从整体上说,CMS 手机不是抢占式的。
CMS 默认启动的线程数是 (ParallelGCTheads + 3)/4,也可通过 -XX:ParallelCMSThreads 直接指定。
当CPU资源比较紧张时,由于 CMS 收集器的线程的影响,应用系统的性能在垃圾回收阶段会很糟糕。回收阈值:-XX:CMSInitiatingOccupancyFraction,默认 68.
CMS 是基于标记-清除算法的收集器,会造成大量的空间碎片,导致可用空间无法给大对象分配空间。
CMS 提供了用于内存整理的参数:- -XX:+UseCMSCompactAtFullCollection(内存碎片的整理并不是并发执行的)
- -XX:CMSFullFCsBeforeCompaction 用于设定进行多少次 CMS 后,进行一次内存压缩
-
G1 收集器(Garbage First)
G1 收集器基于标记-压缩算法的。
设置使用G1收集器
- -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions
设置 G1 回收器的目标停顿时间
- -XX:MaxGCPauseMillis=50
- -XX:GCPauseIntervalMillis=200