随着程序的运行,内存中存在的变量、对象等信息占据的内存越来越多,如果不及时进行垃圾回收,必然会带来程序性能的下降,甚至造成大量内存泄漏(Memory Leak),使得系统出现可用内存不足导致不必要的异常
引用计数算法通过判断对象的引用数量来决定对象是否可以回收,该方法是垃圾回收的早期策略,在这种方法中,堆中的每个对象实例都有一个引用计数,代表该对象被引用的数量,任何引用计数为 0 的对象可以被当作垃圾回收
这种方法存在一个重大缺陷,即循环引用问题
对象1引用对象2,而对象2引用对象1,二者的引用计数均为 1,无法归零,故永远无法回收,代码示例如下
public class Demo {
private Object ref;
public static void main(String[] args) {
Demo demo1 = new Demo();
Demo demo2 = new Demo();
demo1.ref = demo2;
demo2.ref = demo1;
demo1 = null;
demo2 = null;
}
}
可达性分析算法是目前 JVM 所使用的算法,该算法通过一系列的名为 “GC Roots” 的对象作为起始点进行搜索,如果在 “GC Roots” 和一个对象之间没有可达路径,则称该对象是不可达的
在Java中,可作为 GC Root 的对象包括以下几种:
在可达性分析算法中被判定为不可达的对象,还要经历再次标记过程才能判定为可回收对象
第一次标记并进行一次筛选:对象没有重写 finalize() 方法或者已经执行过 finalize() 方法,则判定为可回收对象(筛选的本质是判断此对象是否有必要执行 finalize() 方法)
第二次标记:对象重写了 finalize() 方法或者 finalize() 方法没有被执行,则将此对象放置在 F-Queue 队列中,并在稍后由一个虚拟机自动建立、低优先级(优先级为 8)的 Finalizer 线程去执行 finalize() 方法(如果一个对象的 finalize 方法运行缓慢,将会导致队列后的其他对象永远等待,严重时将会导致系统崩溃),执行完毕后,会再次判断该对象是否可达,若仍然不可达,则判定为可回收对象,否则对象 “复活”(移出即将回收对象的集合)
在 JDK1.2 之后,Java 对引用的概念进行了扩充,将引用分为了如下四种
Java 默认的引用即为强引用,垃圾回收器永远不会回收被引用的对象,当内存不足时,JVM 直接抛出 OutOfMemoryError
Object obj = new Object();
软引用用来描述一些非必需但仍有用的对象,在内存足够的时候,软引用对象不会被回收,当内存不足时,系统会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出 OutOfMemoryError
在 JDK1.2 之后,用 java.lang.ref.SoftReference 类来表示软引用
SoftReference sr = new SoftReference<>(new String("CacheData"));
弱引用的强度比软引用要更低一些,无论内存是否足够,只要 JVM 开始垃圾回收,那些被弱引用的对象都会被回收
在 JDK1.2 之后,用 java.lang.ref.WeakReference 来表示弱引用
WeakReference wr = new WeakReference<>(new String("CacheData"));
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
在 JDK1.2 之后,用 java.lang.ref.PhantomReference 类来表示虚引用,虚引用必须要和 ReferenceQueue 引用队列一起使用
ReferenceQueue rq = new ReferenceQueue<>();
PhantomReference pr = new PhantomReference<>(new String("Phantom"), rq);
引用队列(ReferenceQueue)可以配合软引用、弱引用和虚引用使用,当引用的对象将要被 JVM 回收时,会将引用本身加入到引用队列中
当引用的对象被回收后,可能需要对引用本身进行处理,如将 ArrayList 中已经为空的引用删去,此时需要使用引用队列
ReferenceQueue rq = new ReferenceQueue<>();
SoftReference sr = new SoftReference<>(new String("Soft"), rq);
WeakReference wr = new WeakReference<>(new String("Weak"), rq);
PhantomReference pr = new PhantomReference<>(new String("Phantom"), rq);
Reference extends String> ref = rq.poll();
System.out.println(ref); //对软/弱/虚引用本身进行处理
JVM 首先通过可达性分析算法判断对象是否为垃圾,在确定了哪些垃圾可以回收后,JVM 通过垃圾回收算法进行高效的垃圾回收,JVM 规范中没有对如何实现垃圾收集器做出明确的规定,故不同的 JVM 可能对垃圾回收有着不同的实现,下面介绍几种常见的垃圾回收算法
标记-清除算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象
它存在两个缺点:
复制算法将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上,然后再将之前的那一块的内存空间一次性全部清空,之后分配内存就在另一块上分配,直到内存再次用完,循环这个步骤。
优点
缺点
标记-整理算法与标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收垃圾对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新相应的指针。
标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但却解决了内存碎片的问题
JVM 将堆内存划分为新生代、老年代和永久代(JDK1.7后逐渐移除),根据不同代的特点选择最适合的回收算法
新生代(Young)分为 Eden 区和 Suvivor 区,Suvivor 区又分为 From 区和 To 区,默认比例为8:1:1。划分的目的是因为 JVM 采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在 Eden 区分配(大对象除外,大对象直接进入老年代),当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次垃圾回收,称作 Minor GC
GC 开始时,对象只会存在于 Eden 区和 From 区,To 区是空的(作为保留区域),GC 进行时, Eden 区中所有存活的对象都会被复制到 To 区,而在 From 区中,仍存活的对象会根据它们的年龄决定去向,年龄达到阀值(默认为 15,新生代中的对象每经历一轮垃圾回收,年龄加 1,GC 分代年龄存储在对象的 header 中)的对象会被移到老年代中,没有达到阀值的对象会被复制到 To 区,接着清空 Eden 区和 From 区,到此,新生代中存活的对象都在 To 区。
接着, From 区和 To 区会进行交换,也就是上次 GC 清空的 From 区成为新的 To 区,上次的 To 区成为新的 From 区,以保证新的一轮 GC 中,To 区是空的,当 To 区没有足够的空间存放新生代的存活对象时,通过分配担保机制将新生代对象放入老年代中
老年代中的对象生命周期较长,存活率比较高,在老年代中进行 GC 的频率相对而言较低
当老年代内存不足、或是显式的调用 System.gc() 时触发一次 Full GC,也称 Major GC,
Full GC 的速度一般会比 Minor GC 慢 10 倍以上,Full GC 会伴随着至少一次 Minor GC,并对整个堆的垃圾对象进行回收
老年代的垃圾回收算法采用的是标记-整理算法或标记-清除算法(根据垃圾回收器的选择不同而不同)
STW(Stop-The-World),是指在垃圾回收时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集器),native 代码可以执行,但不能与 JVM 交互
垃圾回收算法服务于垃圾回收器,目前 HotSpot 虚拟机用到的垃圾回收器如下图所示,只有两个回收器之间有连线时才能配合使用
串行垃圾回收器在进行垃圾回收时,会触发 STW 机制,挂起 Java 应用程序的其他所有线程
串行垃圾回收器是为单线程环境而设计的,只使用一个线程去回收
新生代串行回收器
老年代串行回收器
并行指多条垃圾回收器线程并行工作,也会造成 STW,适合 Server 模式以及多核 CPU 环境
并行垃圾回收器有三种,分别为 ParNew 、Parallel Scavenge 、Parallel Old
Serial 的多线程版本,新生代回收器
默认开启的回收器线程数和 CPU 核数一样,通过如下参数控制并行的垃圾回收线程数
-XX:ParallelGCThreads=n
Parallel Scavenge 回收器也是一个并行的多线程新生代回收器,其目标是达到一个可控制的吞吐量
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务
Parallel Scavenge 回收器提供了一个参数
-XX:+UseAdaptiveSizePolicy
使用该参数后,就不需要手工指定新生代的大小、Eden 和 Survivor 区的比例、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。
Parallel Scavenge 的老年代版本
CMS(Concurrent Mark Sweep)回收器是一种以获取最短回收停顿时间为目标的回收器,工作在老年代,使用标记-清除算法
CMS 回收器的工作流程分为以下 4 个步骤:
优点
缺点
相关参数
-XX:CMSInitiatingOccupancyFraction:考虑到浮动垃圾的产生,CMS 必须在 GC 时预留足够空间,而不能等到内存空间被完全填满时才进行 GC,可以通过该参数(默认92%)的值来调整 CMS GC 的触发阈值,该值设置得过低则会因频繁进行 CMS GC 导致应用程序吞吐量下降,设置得过高则会因并发失败(Concurrent Mode Failure)而频繁触发 Full GC 导致应用程序停顿
-XX:+UseCMSCompactAtFullCollection:用于指定在执行完 Full GC 后对内存空间进行整理,以避免内存碎片的产生。不过由于内存整理过程无法并发执行,所带来的问题就是停顿时间变得更长
-XX:CMSFullGCsBeforeCompaction:设置在执行多少次 Full GC 后对内存空间进行整理
G1 是当今垃圾回收技术最前沿的成果之一,JDK7 加入 JVM 的回收器大家庭中,成为 HotSpot 重点发展的垃圾回收技术,JDK9 后成为默认的垃圾回收器
G1 是一个分代的,增量的,并行与并发的标记-复制垃圾回收器。它的设计目标是为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间,同时兼顾良好的吞吐量,面向服务端应用
G1 采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数 -XX:G1HeapRegionSize=n 可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为 2048 个分区
Humongous:当分配的一个对象超过一半区域的大小时,这个对象就会被放入这个区域,这个区域属于老年代区域
G1 会执行一个并发的全局标记的阶段来去确定整个堆当中对象的存活情况,优先回收垃圾多的 Region ,这就是为什么这种垃圾回收器的方式称为 Garbage-First 的原因
初始标记(Initial Marking)
初始标记阶段仅仅标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 的值,让下一个阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象,这一阶段需要停顿线程,但是耗时很短
要达到 GC 与用户线程并发运行,必须要解决回收过程中新对象的分配,所以 G1 为每一个 Region 区域设计了两个名为 TAMS(Top at Mark Start)的指针,从 Region 区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围
并发标记(Concurrent Marking)
并发标记阶段是从 GC Root 开始对堆中的对象进行可达性分析,找出存活的对象,这一阶段耗时较长,但可与用户程序并发执行
最终标记(Final Marking)
最终标记阶段修正在并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分标记记录
筛选回收(Live Data Counting and Evacuation)
筛选回收阶段首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划
使用范围不同
STW 的时间不同
垃圾碎片不同
CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片
G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片。