文章源于学习若干个博客过程中整合,图源于图文并茂,万字详解,带你掌握 JVM 垃圾回收!
7种垃圾回收器特点,优劣及使用场景
GC garbage collection 垃圾回收
C/C++中,对象的申请和释放都需要程序员自己进行操作,然而程序员如果忘记对内存空间进行正确的释放,可能导致内存泄漏。
为避免此类情况发生,GC机制可以让程序员开发过程中更专注程序应用本身,而无需考虑内存泄露问题。
JVM一共有五个区域: Method Area, VM stack, Native Mathod Stack, Heap, Program Counter Register
其中GC回收的区域为:
GC回收区域的共同点为:线程共享
对象无引用,或不可达
判断方法
static class Test{
public Test instance;
}
public void run() {
Test t1 = new Test();
Test t2 = new Test();
t1.instance = t2;
t2.instance = t1;
// t1和t2相互引用
}
如果我们执行run()
方法后,虽然t1和t2不会再被访问,但由于t1, t2相互引用对方,引用计数器不为0,无法对他们进行回收。所以,市面上主流的Java虚拟机都没有使用这个算法,而是使用可达性分析法
通过 GC Roots根对象来作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程的就是一条引用链(Reference Chain)。不在这个链里面的对象,就认为是 可回收的
虚拟机栈引用的对象
当运行某个函数的时候,JVM就会为这个函数在栈区开辟内存,如果运行main函数,那么JVM为main函数的局部变量在栈区开辟内存
public class Test {
public static void main(String[] args) {
Test a = new Test();//a是栈中局部变量
a = null; //a为null的时候,他和原本的new Test()断开连接
//对象回收
}
}
方法区中类静态属性引用的对象
方法区本身用于存放类的信息:名称,父类,接口,变量等
public class Test {
public static Test s; //s为类静态属性引用的对象
public static void main(String[] args) {
Test a = new Test();
a.s = new Test();
a = null;
}
}
a = null 时,由于 a 原来指向的对象与 GC Root (变量 a) 断开了连接,所以 a 原来指向的对象会被回收。
然而s是类静态属性,且被赋值引用,被认为是GC Root,s依然 可达
方法区中常量引用的对象
public class Test {
public static final Test s = new Test();
public static void main(String[] args) {
Test a = new Test();
a = null;
}
同上,a对象被回收不会影响到常量s指向的对象
本地方法栈中 JNI(Java Native Interface引用) 的对象
区别?
Java方法 | 本地方法 |
---|---|
虚拟机会创建一个栈桢并压入 Java 栈 | 虚拟机会保持 Java 栈不变,不会在 Java 栈祯中压入新的祯,虚拟机只是简单地动态连接并直接调用指定的本地方法。 |
强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)
//强引用
String s = new String("hello");
System.gc();
System.out.println(s);
和前文一样,强引用存在,垃圾收集器永远不会回收被引用的对象,只有当引用被设为null的时候,对象才会被回收。如果赋值给static变量,对象很长一段时间都不会被回收
//软引用
String s = new String("hello");
//强引用添加到软引用
SoftReference<String> softReference = new SoftReference<>(s);
s=null;
//执行垃圾回收
System.gc();
//再次获取
if(softReference !=null ){
System.out.println(softReference.get());
}
GC过程中,如果内存充足,软引用对象不会被释放
//弱引用
WeakReference<String> weakReference = new WeakReference<>(new String("hello"));
//执行垃圾回收
System.out.println("执行垃圾回收之前");
System.out.println(weakReference.get());
System.gc();
System.out.println("执行垃圾回收之后:");
System.out.println(weakReference.get());
只要系统执行完垃圾回收,无论内存是否足够,弱引用变量指向的对象都会被回收。
ReferenceQueue<String> referenceQueue = new ReferenceQueue<>();
PhantomReference<String> phantomReference = new PhantomReference<>(new String("hello"), referenceQueue);
System.gc();
System.out.println(referenceQueue.remove().get());
无论内存是否足够,弱引用变量指向的对象都会被回收,和虚引用的区别在于,无法通过虚引用来取得一个对象实例,虚引用也不会对生成他的对象生存时间产生影响。
作用:对象被收集器回收的时候得到系统通知
自动调用的情况:
手动调用的情况
System.gc()
发现垃圾时,立即回收。
最大限度减少程序暂停,因为发现后立即回收,减少了程序因内存爆满而被迫停止的现象
时间开销大,因为引用计数算法需要时刻监控引用计数器的变化。
无法回收循环引用的对象
未引用对象并不会被立即回收,垃圾对象将一直累计到内存耗尽为止,当内存耗尽时,程序将会被挂起,垃圾回收开始执行
优点:简单,快速
缺点:标记和清除过程效率不高 —— 之后会产生大量不连续的内存碎片,提高触发另一次垃圾收集动作的概率
标记过程仍然与“标记-清除”算法一样
第二阶段不对可回收对象进行清理,而让所有存活的对象都向一端移动
改进了标记-清除算法的效率
内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
优点
缺点
于是我们结合前面的2,3,4 三种策略,得到了当前最流行的分带回收机制算法
弱分代假说:绝大多数对象都是朝生夕死的。
强分代假说:熬过越多次的垃圾回收的对象,就越难消亡
绝大多数对象在前几次GC过程中回收,经过多次GC过程未被回收的对象很难消亡。
于是把对象分为 新生代, 老年代。然后分配到不同的区域后,执行不同的策略。比例一般为1:2
新生代 | 老年代 |
---|---|
大部分对象会被回收 | 难被回收 |
频繁使用可达性分析法 | 较少频率回收 |
存活对象被复制到幸存者区域后被释放 |
详细说明
新生代
老年代
除此之外,还有一个永久代(在非heap内存)
前面说过,新生代(Young generation)、老年代(Old generation)所占空间比例为 1 : 2,其中新生代还会被分为三个空间
默认情况下,新生代空间的分配:Eden : Fron : To = 8 : 1 : 1
假设我们没有survivor
如果不划分新生代区,有无其他方法避免上述情况?
方案 | 优点 | 缺点 |
---|---|---|
增加老年代空间 | 更多存活对象才能填满老年代。降低Full GC频率 | 随着老年代空间加大,一旦发生Full GC,执行所需要的时间更长 |
减少老年代空间 | Full GC所需时间减少 | 老年代很快被存活对象填满,Full GC频率增加 |
上述两种解决方案都不能从根本上解决问题
Survivor:就是减少被送到老年代的对象,进而减少Full GC的发生。Survivor的预筛选保证:经历16次Minor GC还能存活的对象,才会被送到老年代。
若只有一个Survivor区,会出现以下情况
-XX:+UserSerialGC #选择Serial作为新生代垃圾收集器
serial的一个多线程版本
多核机器上,其默认开启的收集线程数与cpu数量相等。可以通过如下命令进行修改
-XX:ParallelGCThreads #设置JVM垃圾收集的线程数
当用户线程都执行到安全点时,所有线程暂停执行,采用复制算法进行垃圾收集工作
特点
-XX:UseParNewGC #新生代采用ParNew收集器
吞吐量 = 运行用户代码时间 运行用户代码时间 + 垃圾收集时间 吞吐量=\frac{运行用户代码时间}{运行用户代码时间+垃圾收集时间} 吞吐量=运行用户代码时间+垃圾收集时间运行用户代码时间
例如虚拟机一共运行了 100 分钟,其中垃圾收集花费了 1 分钟,那吞吐量就是 99%
垃圾收集器每 100 秒收集一次,每次停顿 10 秒,和垃圾收集器每 50 秒收集一次,每次停顿时间 7 秒,虽然后者每次停顿时间变短了,但是总体吞吐量变低了,CPU 总体利用率变低了。
特点
Serial Old是Serial收集器的老年代版本
标记-整理算法
适用场景
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
参数指定
-XX:+UserParallelOldGC
获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除“
-XX:CMSInitiatingOccupancyFraction修改CMS触发的百分比
面向服务端应用的垃圾收集器,目前是JDK9的默认垃圾收集器
Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但两者之间不是物理隔离的。他们都是一部分Region的集合
JVM启动设置每个区为2的次幂大小,最多为2048个区域( 2048 × 32 M = 64 G 2048\times 32M=64G 2048×32M=64G)假如设置 -Xmx8g -Xms8g,则每个区域大小为 8g/2048=4M。
作用