JVM 可达性分析法
1. 垃圾回收器的基本概念
什么是垃圾回收器:JVM 为 Java 提供了垃圾回收机制,其实是一种偏自动的内存管理机制。简单来说,垃圾回收器会自动追踪所有正在使用的对象,并将其余未被使用的对象标记为垃圾,不需要开发者手动进行垃圾回收,JVM 自动进行垃圾回收,释放内容。
为什么进行垃圾回收:如果不进行垃圾回收,内存迟早都会被消耗空,因为我们在不断的分配内存空间而不进行回收。除非内存无限大,我们可以任性的分配不回收,但是事实并非如此。所以,垃圾回收是必须的。
哪些内存需要回收:哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓"要回收的垃圾"无非就是那些不可能再被任何途径所使用的对象。无需再使用的对象,会被标记为垃圾,等待JVM回收此部分内存。
Tips:Java中通过可达性分析法来检测对象是否为垃圾,如果不可达,则将对象标记为垃圾,会被 JVM 回收,接下来我们学习可达性分析法。
2. 可达性分析法基本原理
方法原理:通过一系列称为"GC Roots"的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链(即GC Roots到对象不可达时),则证明此对象是不可用的。
那么问题又来了,如何选取 GCRoots 对象呢?在 Java 语言中,可以作为 GCRoots 的对象包括下面几种:
- 虚拟机栈(栈帧中的局部变量区,也叫局部变量表)中引用的对象;
- 方法区中的类静态变量属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native方法)引用的对象。
Tips:看了如上的原理与 GC Roots 选择的描述,感觉概念性问题比较抽象,难于理解,我们继续通过示例来进一步理解可达性分析法。
3. 可达性分析法示例
上文中提到了,可达性分析法是通过 GC Roots 为起点的搜索,有四种对象可以作为 GC Roots,那么我们通过如下示意图来理解下,何为不可达对象。
GC Roots 四种类型解释:从上图中,我们可以看到四种 GC Roots。这里我们对这四种 GC Roots 做一下更为细致的解释。
- 虚拟机栈中的引用的对象:我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的;
- 全局的静态的对象:也就是使用了 static 关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为 GC Roots 是必须的;
- 常量引用:就是使用了 static final 关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为 GC Roots;
- Native 方法引用对象:这一种是在使用 JNI 技术时,有时候单纯的 Java 代码并不能满足我们的需求,我们可能需要在 Java 中调用 C 或 C++ 的代码,因此会使用 native 方法,JVM 内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为 GC Roots。
从上图来理解可达性分析法就会非常简单,四种 GC Roots 无非是 Java 中的引用对象,从GC Roots 出发,类似于我们使用开发工具看代码,发现某部分代码用不到了,我们就会删除这部分代码。其实可达性分析法也是如此,发现某些对象不可达了,就会被垃圾回收器收集。
从上图中来看,对象 A,B,C,D,E,F 为可达对象;而对象 G,H,I,J,K 为不可达对象,会被标记为垃圾对象,最终被垃圾回收器回收。
JVM 四种引用
1. 可达性分析的四种引用类型
上节内容讲解了可达性分析,可达性分析的 GC Roots 均为引用对象,那么引用对象有 4 种引用类型如下:
- 强引用;
- 软引用;
- 弱引用;
- 虚引用。
2. 强引用
定义 :强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object()
这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
代码示例:
java
public class DemoTest {
public static void main(String[] args) {
Object obj = new Object(); // 强引用
}
}
代码块12345
在强引用的定义中有这样一句话:"只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。" 那么有没有办法将强引用消除呢?
消除强引用示例代码:
java
public class DemoTest {
public static void main(String[] args) {
Object obj = new Object(); // 强引用
obj = null; //消除强引用
}
}
如果不使用强引用时,可以赋值 obj=null
,显示的设置 obj 为 null,则 gc 认为该对象不存在引用,这时候就可以回收此对象。
3. 软引用
定义:软引用用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,如果内存充足,则垃圾回收器不会回收该对象,如果内存不够了,就会回收这些对象的内存。
在 JDK 1.2 之后,提供了 SoftReference 类来实现软引用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
软引用使用场景 :Android 应用图片
软引用主要应用于内存敏感的高速缓存,在 Android 系统中经常使用到。一般情况下,Android 应用会用到大量的默认图片,这些图片很多地方会用到。如果每次都去读取图片,由于读取文件需要硬件操作,速度较慢,会导致性能较低。所以我们考虑将图片缓存起来,需要的时候直接从内存中读取。
但是,由于图片占用内存空间比较大,缓存很多图片需要很多的内存,就可能比较容易发生 OutOfMemory 异常。这时,我们可以考虑使用软引用技术来避免这个问题发生。
SoftReference 可以解决 OOM 的问题,每一个对象通过软引用进行实例化,这个对象就以cache的形式保存起来,当再次调用这个对象时,那么直接通过软引用中的 get() 方法,就可以得到对象中的资源数据,这样就没必要再次进行读取了,直接从 cache 中就可以读取得到,当内存将要发生 OOM 的时候,GC 会迅速把所有的软引用清除,防止 OOM 发生。
4. 弱引用
定义:弱引用描述非必需对象。被弱引用关联的对象只能生存到下一次垃圾回收之前,垃圾收集器工作之后,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。Java 中的类 WeakReference 表示弱引用。
代码示例:
java
import java.lang.ref.WeakReference;
public class Main {
public static void main(String[] args) {
WeakReference<String> sr = new WeakReference<String>(new String("hello"));
System.out.println(sr.get());
System.gc(); //通知JVM的gc进行垃圾回收
System.out.println(sr.get());
}
}
结果验证:第二个输出结果是 null,这说明只要 JVM 进行垃圾回收,被弱引用关联的对象必定会被回收掉。
hello
null
5. 虚引用
定义 :"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。虚引用在 Java 中使用 java.lang.ref.PhantomReference
类表示。
作用:虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的区别:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
使用示例:虚引用必须和引用队列(ReferenceQueue)联合使用
java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class Main {
public static void main(String[] args) {
ReferenceQueue<String> queue = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
System.out.println(pr.get());
}
}
JVM 常见的垃圾回收算法
1. 垃圾回收算法种类
我们先来讨论一个问题,垃圾回收算法有几种?
如果单纯从一些博客或者论坛上的内容来说,部分作者会将垃圾回收分为如下 4 种算法:
- 标记-清除(Mark-Sweep)算法;
- 复制(coping)算法;
- 标记-整理(Mark-Compact)算法;
- 分代收集算法。
但是这种分类是不准确的,准确来说,垃圾回收只有 3 种算法:
- 标记-清除(Mark-Sweep)算法;
- 复制(coping)算法;
- 标记-整理(Mark-Compact)算法。
为什么会有所谓的"分代收集算法"呢? 此处我们埋下一个伏笔,后文中我会在适当的地方给予解释。
2. 标记-清除(Mark-Sweep)算法
标记 - 清除(Mark-Sweep)算法是最基本的算法。
基本概念:标记-清除算法就如同它的名字一样,分为"标记"和"清除"两个阶段:
- 首先标记出所有需要回收的对象,这就是标记阶段;
- 标记完成后统一回收所有被标记的对象,这就是所谓的清除阶段。
缺点:这种算法的不足主要体现在效率和空间。
- 从效率的角度讲:标记和清除两个过程的的效率都不高;
- 从空间的角度讲:标记清楚后会产生大量不连续的内存碎片,内存碎片太多可能会导致以后程序运行过程中在需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾回收动作。
为了更加透彻的理解标记-清除(Mark-Sweep)算法,我们来看下如下示意图,通过直观的图形展示,彻底搞懂标记-清除(Mark-Sweep)算法。
3. 复制(coping)算法
Tips:前文提到过,标记-清除(Mark-Sweep)算法从效率的角度讲,"标记"和"清除"两个过程的的效率都不高,为了提升效率,我们引出了复制(coping)算法。
基本概念:复制算法是为了解决效率问题而出现的,它将可用的内存分为两块,每次只用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存一次性清理掉。这样每次只需要对整个半区进行内存回收,内存分配的执行过程如下图所示:
缺点:不过这种算法有个缺点,内存缩小为原来的一半,这样代价太高了。
现在的商用模拟机都采用这种算法来回收新生代,不过研究表明1:1的比例非常不科学,因此新生代的内存被划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
每次回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。
HotSpot虚拟机默认Eden区和Survivor区的比例为8:1,意思是每次新生代中可用内存空间为整个新生代容量的90%。当然,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖老年代进行分配担保(Handle Promotion)。
4. 标记-整理(Mark-Compact)算法
Tips:复制算法在对象存活率较高的场景下要进行大量的复制操作,效率还是很低。并且每次只使用一半的内存空间,资源浪费严重。标记-整理(Mark-Compact)算法解决了内存利用率的问题,并且减少了大量复制的问题。
根据老年代的特点,有人提出了另外标记-整理(Mark-Compact)算法,标记过程与标记-整理(Mark-Compact)算法一样,不过不是直接对可回收对象进行整理,而是让所有存活对象都向一端移动,然后清理掉边界以外的内存。标记-整理算法的工作过程如图:
5. 分代收集
问题:我们上文埋下了伏笔,分代清理到底是不是第四种算法呢?
解答:不是,我们通常称之为分代收集理论,或称之为分代收集思想。目前虚拟机基本都采用分代收集理论来进行垃圾回收。
分代收集理论结合了以上的 3 种算法,根据对象的生命周期的不同将内存划分为几块,然后根据各块的特点采用最适当的收集算法。准确的说,分代收集理论就是在不同的内存区域使用不同的算法,它是以上 3 种算法的使用者。
因此说,分代清理并非是一种单独的算法,而是一种收集理论。
MinorGC、MajorGC、FullGC垃圾回收介绍
MinorGC (新生代垃圾回收)
JDK1.8 堆内部结构
从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC,也叫Young GC。因为Java对象大多具备朝生夕死的特征,所以MinorGC非常频繁,一般回收速度也比较快。一般采用复制算法。
说明:Minor GC可能会引发STW,暂停其他用户的线程,需要等JVM垃圾回收结束后,用户线程才恢复运行。
Minor GC 触发条件
- Eden伊甸园区满了
- 新new的对象需要分配到新生代的Eden伊甸园区,当Eden区的空间不够的时候需要进行MinorGC策略回收,
Major GC(老年代垃圾回收)
Major GC指发生在老年代的GC。
Major GC触发条件
老年代空间不足时,会先尝试触发Minor GC。Minor GC之后空间还不足,则会触发Major GC。
说明:发生在老年代的GC ,基本上进行一次Major GC 就会伴随进行一次 Minor GC。Major GC 的速度一般会比 Minor GC 慢 10 倍,并且STW的时间更长。
Full GC (新生代+老年代垃圾回收)
Full GC可以理解为Major GC+Minor GC组合后进行的一整个过程,是清理JVM整个堆空间(年轻代和老年代空间)。
Full GC触发条件
- 调用System.gc()方法时,可通过-XX:+ DisableExplicitGC 参数来禁止调用System.gc()
- 当方法区空间不足时
- Minor GC后存活的对象大小超过了老年代剩余空间
- Minor GC时中Survivor幸存区空间不足时,判断是否允许担保失败,不允许则触发Full GC。允许,并且每次晋升到老年代的对象平均大小>老年代最大可用连续内存空间,也会触发Full GC
- CMS GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次"Concurrent Mode Failure"失败,会触发Full GC
STW(Stop The World):垃圾回收发生过程中,会产生应用程序的停顿现象。停顿产生的时候整个应用程序线程都会被暂停,有点应用程序像卡死的情况。
JVM 垃圾回收器分类
1. 前言
本节主要讲解 7 种垃圾回收器,其中有 3 种垃圾回收器是作用于年轻代垃圾回收的收集器;另外 3 种圾回收器是作用于老年代垃圾回收的收集器;剩余的 1 种垃圾回收器能够同时作用于年轻代和老年代。
7 种垃圾回收器以及其作用的内存区域如下图所示:
2. Serial收集器
基本概念:Serial收集器是最基本、发展历史最久的收集器,这个收集器是采用复制算法的单线程的收集器。
Tips:从概念上来看,我们需要注意Serial收集器的两个特点:一个是采用复制算法,另外一个是单线程收集。
单线程的收集器:单线程一方面意味着他只会使用一个 CPU 或者一条线程去完成垃圾收集工作,另一方面也意味着他进行垃圾收集时必须暂停其他线程的所有工作,直到它收集结束为止。
不过实际上到目前为止,Serial收集器依然是虚拟机运行在 Client 模式下的默认新生代收集器,因为它简单而高效。Serial 收集器运行过程如下图所示:
3. Parnew收集器
基本概念:Parnew 收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为和 Serial 收集器完全一样,但是他却是 Server 模式下的虚拟机首选的新生代收集器。
Tips:从概念上来看,我们需要注意Parnew收集器的两个特点:一个是采用复制算法,另外一个是多线程收集。
特点:
- 除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。CMS 收集器第一次实现了让垃圾收集器与用户线程基本上同时工作;
- Parnew 收集器默认开启的收集线程数与 CPU 数量相同,在 CPU 数量非常多的情况下,可以使用
-XX:ParallelGCThreads
参数来限制垃圾收集的线程数。
Parnew 收集器运行过程如图所示:
4. Parallel Scavenge收集器
基本概念:Parallel Scavenge 收集器也是一个新生代收集器,也采用了复制算法,也是并行的多线程收集器。Parallel Scavenge 收集器的目标是达到一个可控制的吞吐量。Parallel Scavenge 收集器是虚拟机运行在 Server 模式下的默认垃圾收集器。被称为"吞吐量优先收集器"。
Tips:从概念上来看,我们需要注意Parallel Scavenge收集器的三个个特点:一个是采用复制算法,一个是多线程收集,一个是达到控制吞吐量的目标。
Parallel Scavenge 收集器运行过程同 Parnew 收集器一样:
控制吞吐量 :CMS 等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量。所谓吞吐量就是 CPU 用于运行用户代码时间与 CPU 总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
。
空间吞吐量参数介绍 :虚拟机提供了-XX:MaxGCPauseMills
和 -XX:GCTimeRatio
两个参数来精确控制最大垃圾收集停顿时间和吞吐量大小。不过不要以为前者越小越好,GC 停顿时间的缩短是以牺牲吞吐量和新生代空间换取的。由于与吞吐量关系密切,Parallel Scavenge 收集器也被称为"吞吐量优先收集器"。
Parallel Scavenge 收集器有一个参数 -XX:UseAdaptiveSizePolicy 参数,这是一个开关参数,这个参数打开之后,就不需要手动指定新生代大小、Eden 区和 Survivor 参数等细节参数了,虚拟机会根据当前系统的运行情况以及性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
如果对于垃圾收集器运作原理不太了解,以至于在优化比较困难的时候,可以使用 Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成。
5. Serial Old收集器
基本概念: Serial Old 收集器同样是一个单线程收集器,作用于老年代,使用"标记-整理算法",这个收集器的主要意义也是在于给 Client 模式下的虚拟机使用。
Serial Old 收集器运行过程如图所示:
6. Parallel Old收集器
基本概念: Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,使用多线程和"标记-整理算法"进行垃圾回收。
这个收集器在 JDK 1.6 之后的出现,"吞吐量优先收集器"终于有了比较名副其实的应用组合,在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge收集器+Parallel Old收集器
的组合。
Parallel Scavenge 收集器+Parallel Old 收集器
的组合运行过程如下图所示:
7. CMS收集器
基本概念:CMS(Conrrurent Mark Sweep,连续标记扫描)收集器是以获取最短回收停顿时间为目标的收集器。使用标记-清除算法。
收集步骤:收集过程分为如下四步:
- 初始标记:标记 GCRoots 能直接关联到的对象,时间很短;
- 并发标记:进行 GCRoots Tracing(可达性分析)过程,时间很长;
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,时间较长;
- 并发清除:回收内存空间,时间很长。其中,并发标记与并发清除两个阶段耗时最长,但是可以与用户线程并发执行。
CMS 收集器运行过程如下图所示:
8. G1收集器
基本概念:G1 是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉 JDK1.5 中发布的 CMS 收集器。
与其他GC收集器相比,G1收集器具有以下特点:
- 并发和并行:使用多个 CPU 来缩短 Stop The World 停顿时间,与用户线程并发执行;
- 分代收集:独立管理整个堆,但是能够采用不同的方式去处理新创建对象和已经存活了一段时间、熬过多次 GC 的旧对象,以获取更好的收集效果;
- 空间整合:基于标记-整理算法,无内存碎片产生;
- 可预测的停顿:能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集器上的时间不得超过N毫秒。
在G1之前的垃圾收集器,收集的范围都是整个新生代或者老年代,而 G1 不再是这样。使用 G1 收集器时,Java 堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分(可以不连续)Region 的集合。
9. 小结
本节主要讲解了 7 种垃圾收集器:Serial 收集器,Parnew 收集器,Parallel Scavenge 收集器,Serial Old 收集器,Parallel Old 收集器,CMS 收集器和G1 收集器。
其中专门针对年轻代的收集器有 Serial 收集器,Parnew 收集器和 Parallel Scavenge 收集器;专门作用于老年代的收集器有Serial Old 收集器,Parallel Old 收集器和 CMS 收集器;而 G1 收集器即能够作用于年轻代,也能够作用于老年代。