上一篇:JVM:(二)JVM运行时数据区
JVM垃圾回收机制
一、四种对象引用类型
1. 强引用(StrongReferenceDemo)
对于强引用对象,就算是出现了OOM也不会对该对象进行回收。
强引用是我们最常见的普通对象引用,只要还有强引用指向一个对
象,就表明这个对象还活着,垃圾收集器不会回收这种对象。
/**
* 强引用
*/
public class StrongReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
Object o2 = o1;
o1 = null;
System.gc();
Thread.sleep(1000);//保证完成GC工作,因为gc工作有自己的策略,不一定会马上进行垃圾回收
System.out.println("对象一:" + o1);
System.out.println("对象二:" + o2);
}
}
-------------------------------------------------
控制台打印结果:
对象一:null
对象二:java.lang.Object@5ebec15
可以看到o1被置为空后,new Object()还没有被回收,因为它和o2是强引用。
2. 软引用(SoftReferenceDemo)
软引用是一种相对强引用弱化了一些的引用,需要java.lang.ref.SoftReference类来实现。
当系统内存充足的时候,不会被回收;
当系统内存不足时,它会被回收。
/**
* 软引用
*/
public class SoftReferenceDemo {
/**
* -Xms50M(堆最小内存) -Xmx50M(堆最大内存)
* java的垃圾回收器在内存使用达到-Xms值的时候才会开始回收,如果两个值一样,那就意味着,只有当java使用完所有内存时才会回收垃圾
*
* 软引用 内存充足 和 内存不足情况
*/
public static void softRefMemoryNotEnough()
throws InterruptedException {
byte[] testArry = new byte[30 * 1024 * 1024];
// 30M的数组对象
SoftReference<Object> softReference = new SoftReference<>(testArry);
System.out.println("----------------------第一次打印-----------------");
System.out.println(testArry);
System.out.println(softReference.get());
//赋值为NULL 调用GC
testArry = null;
System.gc();
Thread.sleep(1000); // 保证完成GC工作
System.out.println("----------------------第二次打印-----------------");
System.out.println(testArry);
System.out.println(softReference.get());
try {
byte[] bits = new byte[30 * 1024 * 1024];
// 再申请一个30M大小的数组
} catch (Throwable ex) {
ex.printStackTrace();
} finally {
System.out.println("--------------------第三次打印----------------- ");
System.out.println(testArry);
System.out.println(softReference.get());
}
}
public static void main(String[] args) throws InterruptedException {
softRefMemoryNotEnough();
}
}
控制台打印结果:
----------------------第一次打印----------------------
[B@1e643faf
[B@1e643faf
----------------------第二次打印----------------------
null
[B@1e643faf
----------------------第三次打印----------------------
null
null
由代码可以看到,我们启动项目前设置了堆的最大和最小内存:-Xms50M、-Xmx50M。
首先创建一个30M大小的对象赋值给了testArry,再给testArry创建了一个软引用对象softReference,第一次去打印他们的结果由上图可知;
然后给testArry赋值为null后进行gc工作,因为testArry存在软引用对象softReference并且内存足够,所以softReference不会被回收掉了,见第二次打印结果;
最后再创建一个30M大小的对象赋值给bits,因为我们设置的堆大小只有50M,所以第一个30M的对象被垃圾回收掉了,可见第三次打印结果。
3. 弱引用(WeakReference)
弱引用需要用到java.lang.ref.WeakReference类来实现,它比软引用的生存周期更短。对于只有弱引用的对象来说,只要有垃圾回收,不管JVM的内存空间够不够用,都会回收该对象占用的内存空间。
/**
* 弱引用
*/
public class WeakReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object o1 = new Object();
WeakReference<Object> weakReference = new WeakReference<>(o1);
o1 = null;
System.out.println(o1);
System.out.println(weakReference.get());
System.gc();
Thread.sleep(1000);
System.out.println("===============");
System.out.println(o1);
System.out.println(weakReference.get());
}
}
控制台打印:
null
java.lang.Object@1e643faf
===============
null
null
4. 虚引用(PhantomReference)
虚引用需要java.lang.ref.Phantomreference类来实现,虚引用的作用主要是跟踪对象被垃圾回收的状态。
设置虚引用关联的唯一目的,就是在这个对象被回收的时候收到一个系统通知或者是后续添加进一步的操作处理。
/**
* 虚引用
*/
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
System.out.println(obj);
ReferenceQueue rq = new ReferenceQueue();
PhantomReference pr = new PhantomReference(obj, rq);
System.out.println("虚引用对象:" + pr);
obj = null;
System.gc();
Thread.sleep(1000);
System.out.println("被回收的虚引用对象:" + rq.poll());
}
}
控制台打印:
java.lang.Object@1e643faf
虚引用对象:java.lang.ref.PhantomReference@21bcffb5
被回收的虚引用对象:null
被回收的虚引用对象:java.lang.ref.PhantomReference@21bcffb5
可以看到如果虚引用被垃圾回收后,就会把该虚引用加入引用队列 ,可以用rq.poll()获取队列,这一块没有细研究过,不太清楚具体的逻辑,应用场景大概是类似于钩子函数那种吧。
5. 总结
我们日常工作中其实大多数都是只用过强引用。
强引用:强引用对象不能被回收;
软引用:内存不够时才回收;
弱引用:只要有GC,就回收;
虚引用:检测对象的回收机制。
二、什么样的对象是垃圾
1. 引用计数法
顾名思义,只要JVM中该对象被别人所引用(持有该对象的引用),就
说明该对象不是垃圾,如果一个对象在JVM中没有任何指针对其引
用,它就是垃圾。
弊端:
如果A与B对象相互持有对方的引用,但是却没有其他的对象对其引用,可能导致这一对对象永远不能被回收。
案例:
可以看出使用 引用计数法 去定位垃圾可能会出现上图的问题,导致对象引用一直存在从而不能被GC。就是因为这种问题我们jdk的不同的版本都没有使用这种方式去进行垃圾处理,大多数用的是 可达性分析 这种方式。
2. 可达性分析
可达性分析算法就是选择一些对象作为起始点,这些对象称之为:GC Roots。然后从GC Roots开始向下搜索,搜索路径称之为引用链(Reference Chain),当一个对象不在任何一条引用链上时,就说明此对象是不可达对象,可以被回收(被判定为不可达的对象不一定就会成为可回收对象,他还有很多细节的地方,但我们可以先这样去理解这个概念,这块我没仔细研究过。)。
网上很多文章大多数都是粘贴复制的,感觉都没有理解就发出来了。(至少我看后没有理解)
下面这篇文章我看后有种茅塞顿开的感觉,仅供参考:
https://www.cnblogs.com/yixiaofeng/p/11985040.html
三、GC的分类和概念普及
首先我们先说下堆内存的区域划分以及比例。
堆大小 = 新生代 (young) + 老年代(old)。
old : young = 2 : 1
新生代又分为eden、S0、S1这几个区域
eden : s0 : s1 = 8 : 1 : 1
虚拟机启动时堆区内存自动分配创建,用于存放对象的实例,几乎所有对象都在堆上分配内存,当对象无法在该空间申请到内存时将抛出OutOfMemoryError异常。堆区也是垃圾收集器管理的主要区域。
下面说的from和to对应的就是上图的S0和S1
新生代(Young Generation)
新生代分为两部分,伊甸园(Eden sapce)和幸存者区(Survivor space),大多数对象都是在伊甸园区被new出来的。幸存者区又分为From和To区。当Eden区的空间用完时,程序又要创建对象,Jvm的垃圾回收器将Eden区进行垃圾回收(Minor GC),将Eden区中的不再被其他对象应用的对象进行销毁,然后将Eden区中剩余的对象移到From Survivor区。若From Survivor区也满了,在对该区进行垃圾回收然后移动到To Survivor区(如此往复)。另外每个对象都有一个计数器,记录了存活对象的年龄,当达到一个阈值的时候直接移至老年代,这个阈值默认是15,通过参数-XX:MaxTenuringThreshold控制。
老年代(old Generation)
新生代经过多次GC仍然存在的对象移动到老年区。若老年代也满了,这时将发生Major GC(也可叫Full GC),进行老年区的内存清理,若老年区执行了Full GC之后发现依然无法进行对象的保存,就会抛出OOM(OutOfMemoryError)异常。
垃圾回收分类
Minor GC:指young区的垃圾回收过程
Major GC:指old区的垃圾回收过程
Full GC:young区和old区一起执行的垃圾回收过程
在JVM的运行情况下并不存在单独的Major GC, Major GC 一定会
伴随着Minor GC 即 Full GC。
垃圾回收触发条件
1.当Eden区或者Survivor区空间不足时会触发Minor GC(自动)
2.老年代空间不足时会触发Full GC(自动)
3.手动调用 System.gc() 时会通知 jvm 进行垃圾回收,但是 jvm 理不理你就另说了(jvm有自己的策略,当你调用 System.gc() 后 jvm 会进行垃圾回收,但是触会发时机是不确定的)。
四、垃圾回收算法
通过 可达性分析 可标记出来哪些对象是垃圾对象,然后在经过垃圾回收算法进行垃圾清除。
JVM有三种常见的垃圾回收算法,下面的图中整块区域可以看成是堆区域,蓝色方块代表堆中被使用的区域(不可垃圾回收),灰色方块代表可以被垃圾回收的区域(通过可达性分析标记的对象),白色方块代表未使用的区域。
1. 标记-清除(mark-sweep)
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
优点:
效率高。
缺点:
标记清除之后会产生大量不连续的内存碎片,碎片太多可能会导致程序运行过程中需要分配较大对象时,无法满足分配要求。
2. 标记-复制(mark-copy)
为了解决Mark-Sweep算法的缺陷,标记-复制算法就被提了出来。它将活着的内存空间一分为二,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。(在堆中的幸存者S0区和幸存者S1区,也是采用这种复制算法。)
这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
优点:
不会出现空间碎片和浪费空间的问题,效率相对较高,适合每次存活的对象都不是很多的情况。
缺点:
空间利用率低。始终有一块内存区域是未使用的
3. 标记-压缩(mark-compact)
为了解决mark-copy算法的缺陷,充分利用内存空间,提出了Mark-Compact(标记 - 压缩算法或者标记 - 整理算法)。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
优点:
不会出现空间碎片和浪费空间的问题。
缺点:
效率低。整理过程中带来的计算很消耗性能。
五、垃圾收集器
可以理解为 垃圾收集器 是针对上面的 垃圾回收算法 的具体实现。
由图可以看出堆中的young区和old区(Tenured Generation就是指老年代)分别用那些垃圾收集器。
有连线的代表两个垃圾回收器可以在年轻代和老年代互相组合。
图中有个特殊的地方,CMS和Serial Old连了一条线,是因为CMS会产生空间碎片,多次CMS垃圾回收后需要运行Serial Old进行垃圾回收,具体的可以往下看。
1. Stop The World
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。
类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。当gc线程在处理垃圾的时候,其它java线程要停止才能彻底清除干净,否则会影响gc线程的处理效率增加gc线程负担,特别是在垃圾标记的时候。
2. Serial & Serial Old
Serial & Serial Old收集器是最基本、发展历史最悠久的垃圾收集
器,在JDK1.3之前他们是唯一的选择。
Serial是针对新生代的垃圾收集器,采用标记-复制算法。
Serial Old是老年代的垃圾收集器,采用标记-整理算法。
Serial这套组合垃圾收集器特点是单线程垃圾收集器,它只会使用
单个收集线程去完成垃圾清理工作,更重要的是其在进行垃圾收集
的时候需要暂停其他线程(Stop The World)。
3. ParNew
ParNew垃圾收集器是新生代的多线程并行的垃圾收集器。
可以说ParNew只是Serial收集器的多线程版本,在其他的方面几乎跟Serial特性都是一致的,并没有其他的创新点。
4. Parallel Scavenge
Parallel Scavenge收集器是一个多线程并行新生代收集器。
Parallel Scavenge与ParNew的工作机制基本相同。
Parallel Scavenge更关注系统的吞吐量,吞吐量是评测垃圾回收器的重要指标,吞吐量越高则停顿(GC)时间越少,用户体验就更好。
系统的吞吐量 = 用户线程工作时间 / (用户线程工作时间 + GC时间)
用户工作时间:99S
GC时间:1S
吞吐量 = 99/(99+1) = 99%
GC线程工作的时间设置: -XX:MaxGCPauseMillis
5. Parallel Old
Parallel Old是Parallel Scavenge的老年代垃圾收集器,采用的标记整理算法。
Parallel Old是多线程并行的垃圾收集器。
所以Parallel Scavenge在Parallel Old出来之前他就很尴尬,因为Parallel Scavenge + Serial Old的组合很鸡肋。新生代用并行类的收集器,但是Old区又跟不上脚步。
ps + po:
6. CMS(concurrent mark sweep)
CMS垃圾收集器是Old区的垃圾收集器,采用标记-清除算法。
是第一个实现并发收集的收集器。(这里要注意下,前面说的垃圾收集器都是串行或者并行的,即只针对gc线程;而CMS是可以支持gc线程和用户线程并行的。)
设计的目标以最短STW停顿时间,发挥多核CPU并发运行为设计原则。
6.1 工作流程
1.初始标记-(需要STW)
仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
2.并发标记-(GC标记线程与用户线程一起工作)
进行GC Roots Tracing的过程,在整个过程中耗时最长。
并发标记过程中,有可能用户会产生新的垃圾或者用户程序运行产生的变动的情况。
3.重新标记-(需要STW)
为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
4.并发清理-(GC清理线程与用户线程一起工作)
采用标记-清除算法,这种算法只需要清理垃圾的对象就行,不需要做内存的整理。
6.2 总结
优点:
充分利用CPU资源并发收集、低停顿。
缺点:
1.标记-清除算法将产生空间碎片问题。
2.并发标记和并发清理阶段由于并不是全力进行GC工作一定会带来GC时间过长,影响吞吐量。
3.清理不彻底,会产生浮动垃圾,且浮动垃圾只能在下一次垃圾回收才能处理。(在我们执行并发清理这一步时,有可能用户线程又产生了垃圾。)
7. G1(Garbage First)
自行百度。
8. 总结
收集器 | 串行/并行/并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |