JVM可以说是为了Java开发人员屏蔽了很多复杂性,让Java开发的变的更加简单,让开发人员更加关注业务而不必关心底层技术细节,这些复杂性包括内存管理,垃圾回收,跨平台等,今天我们主要看看JVM的垃圾回收机制是怎么运行的,希望能够帮到大家,

哪些对象是垃圾呢?

Java程序运行过程中时刻都在产生很多对象,我们都知道这些对象实例是被存储在堆内存中,JVM的垃圾回收也主要是针对这部分内存,每个对象都有自己的生命周期,在这个对象不被使用时,这个对象将会变成垃圾对象被回收,内存被释放,那么如何判断这个对象不被使用呢?主要有如下两种方法:

引用计数算法

这个方法什么意思呢?就是给每个对象绑定一个计数器,每当指向该对象的引用增加时,计数器加1,相反减少时也会减1,当计数器的值变为0时,该对象就会变成垃圾对象,也就是最终没有任何引用指向该对象。这种方法比较简单,实现起来也容易,但有一个致命缺点,有可能会造成内存泄漏,也就是垃圾对象无法被回收,我们看下面代码,创建两个对象,每个对象的成员变量都持有对方的引用,这就是一个循环引用,形成了一个环,此时虽然两个对象都不再使用了,但每个对象的计数器并不为0,导致无法被回收,那有办法解决吗?当然有,看下面的算法。

class Person {
public Object object = null;
public void test(){
Person person1 = new Person();
Person person2 = new Person(); person1.object = person2;
person2.object = person1;
person1 = null;
person2 = null;
}
}
可达性分析算法

知道了上面算法的缺点,那么可达性分析是怎么解决的呢?在堆内存中,JVM定义了一系列GCroots对象,这些对象称为GC时个根对象,沿着这些根对象像链表一样一直往下找,凡是在这个链上的对象都是符合可达性的,否则认为这个对象不可达,那么这个对象就是一个垃圾对象,也就是说垃圾对象和GC根对象没有直接或者间接关联关系,如下图,黄色的对象就是可以被回收的垃圾对象,因为根GC根对象没有任何关联。

理解了可达性分析算法的原理,那么估计有疑问了,哪些对象能作为GCroot对象呢,一起来看一下JVM中对GCroot对象定义的规范。

  1. Java虚拟机栈中引用的对象
  2. 堆中静态属性引用的对象(JDK8以前时方法区中)
  3. 堆中常量引用的对象(JDK8以前是方法区中)
  4. 本地方法(Native方法)栈中引用对象的

垃圾回收算法解读

在确定了哪些垃圾对象可以被回收后,垃圾收集器要做的就是开始回收这些垃圾,那么如何在堆内存中高效的回收这些垃圾对象呢?,加下来我们介绍几种算法思想

标记清除算法

标记清除是一种比较基础的算法,其思想对内存中的所有对象扫描,将垃圾对象进行标记,最后将标记的垃圾对象清除,那么这部分内存就可以使用了,如下图,第一行是回收前的内存状态,第二行是回收后的内存状态,发现了什么?对,就是内存碎片,内存碎片会导致大对象分配失败,假设我们接下来的对象都是使用2M内存,则那个1M就会浪费掉。

标记整理算法

相对标记清除算法,标记整理多了一步,其思想也是对内存中的对象扫描,标记存活对象和垃圾对象,然后将对象移动,使得存活的对象一边,待回收的对象在一边,然后再对待回收对象进行回收,这样就解决了内存碎片问题,但是对象频繁的移动会带来指针地址指向不断发生变化,整理内存碎片会消耗较长时间,引起应用程序的暂停。

分半复制算法

标记整理算法解决了内存碎片问题,但内存整理也带来了新的问题,复制算法能够缓解对象移动的问题,但不能根本上解决,复制算法本质上是空间换时间的一种算法,将内存分为大小相等的两部分, 在其中一部分内存使用完之后,将其中活着的对象移入到另一半内存中,然后将这一半内存清空。这种算法的代价浪费一半的内存,比如8G内存,只有4G是可以使用的。

分代算法(集所有优点,弃缺点)

上面三种算法各有优缺点,但都不能完美的解决垃圾回收中遇到的问题,那能不能将上面三种算法的优点都集合起来形成一种新的组合呢?是的,分代算法就是这样的,我们常用不考虑业务的架构都是耍流氓,那么垃圾回收算法也需要结合对象的生命周期来决定,我们都知道应用程序中大多数对象都是朝生夕死的,分代算法将内存分为年轻代和年老代两个区域,年轻代中采用复制算法,因为年轻代中每次收集时都有大量对象死去,只有少量对象存活,所以采用复制算法这样移动的对象比较少,年老代中采用标记清除算法,年老代中的对象都是存活时间比较长的对象,但当内存碎片比较严重时可以进行一次整理(结合使用),

前面提到复制算法会浪费一半的内存,有没有办法浪费的少一点呢?分代算法在年轻代中是怎么解决呢?首先确定的每次垃圾收集时存活对象总是少量的,年轻代中将内存分成了三部分,Eden区域,Survivor1区,Survivor2区,后两个区域用来存储存活的对象,对象创建时总是在Eden区域,每当Eden区域满了之后,垃圾回收时开始将所有存活的对象放入其中一个Survivor区域,并且将另一个Survivor区域和Eden区域清空,如此,两个Survivor区域只需要少量内存空间,这样就可以充分利用内存了。

JVM垃圾回收器详解

基于上面的垃圾回收算法,有很多的垃圾收集器,JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。

Serial和Serial Old垃圾收集器

Serial收集器历史非常悠久了,它是在新生代上实现垃圾收集的,SerialOld是在老年代上实现垃圾收集的

他们两都是单线程工作的(早期多核发展还不是这么好),它在工作时必须暂停应用程序的线程,也就是会发生Stop The World,直到垃圾回收工作完成

Serial年轻代采用复制算法 ,Serial Old老年代采用标记整理算法,

这种收集器的优点是简单,工作起来非常高效,对于单核CPU来说没有线程切换的开销,专门做自己的事,所以在单核CPU上或者内存较小时非常适用,缺点也很明显,当内存过大时,应用程序暂停无法提供服务,"-XX:+UseSerialGC"这个参数用来开启Serial垃圾收集器。

ParNew垃圾收集器

ParNew是Serial收集器的多线程版本,除了是多线程,其它的都一样(也会发生Stop The World,也是新生代的收集器)。它是目前唯一能够和CMS合作使用的新生代垃圾收集器。

Parallel Scavenge和Parallel Old垃圾收集器

Parallel Scavenge收集器是一个新生代收集器,Parallel Old是一个老年代收集器,前者使用的是复制算法,后者使用的是标记整理算法,他们又都是并行的多线程收集器。

Parallel Scavenge和Parallel Old收集器关注点是吞吐量(如何高效率的利用CPU)所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。

在对CPU (吞吐量)比较敏感的情况下,建议使用这两者结合

CMS(Concurrent Mark Sweep)收集器

重点来了,CMS收集器的目标是获取最短停顿的时间(即GC时应用程序线程暂停的时间最短),它是老年代收集器,基于标记清除算法(产生内存碎片),并发收集(多线程),CMS是HotSpot在JDK1.5推出的第一款真正意义上的并发(Concurrent)收集器;第一次实现了让垃圾收集线程与用户线程(基本上)同时工作;他的应用场景主要是在和用户交互较多的地方使用,减少用户感受到的服务延迟。

CMS收集器的运作过程比较复杂,下面我们仔细了解一下这个过程,看看CMS的优秀设计思想

上面提到CMS是基于标记清除算法,CMS将标记分为了三部分,清除一部分,总共四部分

初始标记

首先这个过程是发生STW的,也就是应用程序线程暂停,其次这个过程是非常短暂的,并且是单线程执行的,这一步的主要做的事情标记GCRoots能直接关联老年代对象,遍历新生代,标记新生代中可达的老年对象

并发标记

这一阶段用户线程是运行的,因为这一阶段应用程序线程还在执行,所有还会持续产生新的对象,这一阶段主要是根据初始阶段标记出来的可达的GCRoots直接关联对象继续递归遍历这些对象的可达对象,但是不会标记产生的新对象,为了避免后续重新扫描老年代,这一阶段会把新产生的对象打一个标记(Dirty脏对象),后续只会扫描这些标记为Dirty的对象

这一阶段耗时最长了,所以在这一阶段用户产生的垃圾对象足够多时(也就是老年代已经无法存储了)就会发生concurrent mode failure,当这一错误出现时CMS就会退化为另一个垃圾会收器(Serial Old)暂停用户线程,单线程回收,这也是CMS缺点之一

预清理

这一阶段用户线程是运行的,主要是处理新生代已经发现的引用,比如在上面的并发阶段,Enen区域分配了一个新的对象M,M引用了老年代的一个对象N,但这个N之前没有被标记为存活,那么此时这个N就会被标记,同时也会把上一阶段的Dirty对象重新标记,这一阶段也可以通过参数CMSPrecleaningEnabled来进行关闭,默认是开启

可中断的预清理

这一阶段用户线程是运行的,该阶段发生有一个前提,就是新生代Eden区域内存使用必须大于2M,这个值可以通过如下参数控制。

CMSScheduleRemarkEdenSizeThreshold

可中断的预处理是什么意思呢?就是这一阶段可以中断,在该阶段主要循环做两件事,一是处理From和To区域的对象,标记可达的老年代对象,二是扫描标记Dirty对象

中断就指的是这个循环是可以中断的,条件有三个:

  1. MSMaxAbortablePrecleanLoops设置循环次数,默认是0,表示无限制
  2. CMSMaxAbortablePrecleanTime设置执行阈值,默认是5秒
  3. CMSScheduleRemarkEdenPenetration,新生代内存使用率到了阈值,默认是50%
并发重新标记

这一阶段也是STW的,这个过程也会非常短暂,为什么呢?因为上面并发标记,预清理已经标记了大部分存活对象,这一阶段也是针对上面新产生的对象进行扫描标记,可能产生的新的引用如下

  1. 老年代的新对象被GCRoots引用
  2. 老年代未标记的对象被新生代的对象引用
  3. 老年代已标记的对象增加新引用指向老年代其它未标记的对象
  4. 新生代对象指向老年的代的引用被删除

上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以还有进行如下的处理

  1. 遍历新生代对象,重新标记
  2. 根据GC Roots,重新标记
  3. 遍历老年代的Dirty,重新标记,这里的Dirty Card大部分已经在clean阶段处理过

这个过程中会遍历所有新生代对象,如果新生代对象较多,可能比较耗时,但是如果上面可中断预处理过程中发生了一次YGC,那么这次遍历就会轻松很多,但是这一次并不可控制,CMS算法中提供了一个参数:CMSScavengeBeforeRemark,默认并没有开启,如果开启该参数,在执行该阶段之前,会强制触发一次YGC,可以减少新生代对象的遍历时间,回收的也更彻底一点。但这个参数也有缺点,利是降低了Remark阶段的停顿时间,弊的是在新生代对象很少的情况下也多了一次YGC,就看运气了。

并发清除

这一阶段用户线程是运行的,同时GC线程开始对为标记的区域做清扫,回收所有的垃圾对象,这一阶段用户线程还会产生新的对象,这一部分变成垃圾对象后,CMS是无法清理的,这一部分垃圾对象也被称为浮动垃圾,这也是CMS缺点之一

内存碎片问题

我们知道CMS是基于标记-清除算法的,CMS只会删除无用对象,会产生内存碎片,那么内存碎片什么时候整理呢?下面这个参数可以配置

-XX:CMSFullGCsBeforeCompaction=n

意思是说在经过n次CMS的GC时,才会做内存碎片整理。如果n等于3,也就是没经过3次后的CMS-GC会进行一次内存碎片整理,这个默认值是0,代表着直到碎片空间无法存储新对象时才会进行内存碎片整理。

还有一种情况,在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年带有足够的空闲空间,但是由于碎片较多,新生代要转移到老年带的对象比较大,找不到一段连续区域存放这个对象导致的,这个时候会发生FullGC,同时进行碎片空间整理。

针对concurrent mode failure解决办法
-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSInitiatingOccupancyOnly

我们都知道了concurrent mode failure产生的原因,那么可以通过上面两个参数来防止这个问题产生 第二个参数是用来指定使用第一个参数的,如果没有第二个参数,则JVM垃圾回收时只有第一次会采用第一个参数,后续会自行调整。

第一个参数代表设定CMS在对内存占用率达到70%的时候开始GC,,这个参数要好不管监控调整以达到一个合适的值,如果过小则gc过于频繁,如果过大则可能产生上面标题的问题(本身这个参数是用来解决这个问题,设置不当可能会引发这个问题)

还有一个参数,这个参数开启后每次FulllGC都会压缩整理内存碎片,默认值是false,不开启

XX:+UseCMSCompactAtFullCollection

大多数情况下不需要设置这两个参数,JVM会自行调优,决定在什么时候GC,除非你觉得你比JVM的自动调优做的好,那么你可以自行调优。

过早提升和提升失败

在 Minor GC 过程中,Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为过早提升(Premature Promotion),这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步,如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为提升失败(Promotion Failure)。

早提升的原因Survivor空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor调大,否则年轻代生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的full gc;对象太大,Survivor和Eden没有足够大的空间来存放这些大对象。

提升失败原因当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。为什么是没有足够的连续空间而不是空闲空间呢?老年代容纳不下提升的对象有两种情况:老年代空闲空间不够用了;老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象。

查看JDK8默认垃圾收集器

控制台输入如下命令

java -XX:+PrintCommandLineFlags -version

得到结果如下,我们可以看到 -XX:+UseParallelGC 这个参数,这个参数表示JDK8的年轻代使用垃圾收集器为Parallel Scavenge,老年代垃圾收集器为Serial Old

 XX:InitialHeapSize=266390080
-XX:MaxHeapSize=4262241280
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers -XX:+UseCompressedOops
-XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_191"
Java(TM) SE Runtime Environment (build 1.8.0_191-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode)

![](https://img2018.cnblogs.com/blog/706455/201909/706455-20190911210708072-261554801.jpg)

深入JVM垃圾回收机制,值得你收藏的更多相关文章

  1. JVM垃圾回收机制总结:调优方法

    转载: JVM垃圾回收机制总结:调优方法 JVM 优化经验总结 JVM 垃圾回收器工作原理及使用实例介绍

  2. JVM内存管理和JVM垃圾回收机制

    JVM内存管理和JVM垃圾回收机制(1) 这里向大家描述一下JVM学习笔记之JVM内存管理和JVM垃圾回收的概念,JVM内存结构由堆.栈.本地方法栈.方法区等部分组成,另外JVM分别对新生代和旧生代采 ...

  3. JVM垃圾回收机制概述

    JVM垃圾回收机制概述 1.定义 是指JVM用于释放那些不再使用的对象所占用的内存. 2.方式 2.1引用计数(早期) 当引用程序创建引用以及引用超出范围时,JVM必须适当增减引用数.当某个对象的引用 ...

  4. Java虚拟机学习笔记——JVM垃圾回收机制

    Java虚拟机学习笔记——JVM垃圾回收机制 Java垃圾回收基于虚拟机的自动内存管理机制,我们不需要为每一个对象进行释放内存,不容易发生内存泄漏和内存溢出问题. 但是自动内存管理机制不是万能药,我们 ...

  5. JVM基础系列第8讲:JVM 垃圾回收机制

    在第 6 讲中我们说到 Java 虚拟机的内存结构,提到了这部分的规范其实是由<Java 虚拟机规范>指定的,每个 Java 虚拟机可能都有不同的实现.其实涉及到 Java 虚拟机的内存, ...

  6. JVM内存管理、JVM垃圾回收机制、新生代、老年代以及永久代

    内存模型 JVM运行时数据区由程序计数器.堆.虚拟机栈.本地方法栈.方法区部分组成,结构图如下所示. JVM内存结构由程序计数器.堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)程序计数器 ...

  7. JVM 垃圾回收机制和常见算法和 JVM 的内存结构和内存分配(面试题)

    一.JVM 垃圾回收机制和常见算法 Sun 公司只定义了垃圾回收机制规则而不局限于其实现算法,因此不同厂商生产的虚拟机采用的算法也不尽相同.GC(Garbage Collector)在回收对象前首先必 ...

  8. JVM垃圾回收机制和常用算法

    由于疫情的原因,所以目前一直在家远程办公,所以很多时间在刷面试题,发现2019大厂的面试虽然种类很多,但是总结了一下发现主要是这几点:算法和数据结构. JVM.集合.多线程.数据库这几点在面试的时候比 ...

  9. 真的可惜,四面阿里,结果我被JVM垃圾回收机制与 OOM异常卡住了

    前言 为什么需要垃圾回收 首先我们来聊聊为什么会需要垃圾回收,假设我们不进行垃圾回收会造成什么后果,我们举一个简单的例子 我们住在一个房子里面,我们每天都在里面生活,然后垃圾都丢在房子里面,又不打扫, ...

随机推荐

  1. key_load_public: invalid format

    ssh-keygen -f ~/.ssh/id_rsa -y > ~/.ssh/id_rsa.pub

  2. 避免在ASP.NET Core 3.0中为启动类注入服务

    本篇是如何升级到ASP.NET Core 3.0系列文章的第二篇. Part 1 - 将.NET Standard 2.0类库转换为.NET Core 3.0类库 Part 2 - IHostingE ...

  3. 写了个 Task.WhenAll(t)的一个例子。

    public static void Main() { var t = Task.Run(() => { throw new Exception("aa"); }); Tas ...

  4. 【python系统学习06】一张图看懂列表并学会操作

    点击跳转-原文地址 数据类型 - 列表(list) 「目录:」 一张图了解列表 列表是什么 列表长啥样 语法格式 代码示例 格式特征 列表定义 列表操作 - 提取单个:偏移量 什么是偏移量 偏移量提取 ...

  5. [Other]THUWC2020 游记

    Dec. 20th 一下飞机,\(\text{FJ}\) 选手感觉 \(\text{BJ}\) 好冷 下午去了鸟巢,晚上回 \(\text{GLHT}\) 酒店吃泡面 写了洛谷上的线段树分治模板题之后 ...

  6. 美食家App开发日记5

    今天将ListView控件用更强大的Recyclerview控件取代,最后调试了程序. 感觉Android编程难度实在是远远高于javaweb,初次接触,感觉有很多东西想实现,想得很容易,但是实现起来 ...

  7. lind.ddd博客笔记索引

    先占位 整理 写博客呢 可以理解为一个动手的过程 写博客呢和实际动手也是一段差距

  8. 创建dynamics CRM client-side (七) - 用JS 来控制Auto-Save

    在我们的system setting里面, 我们可以设置打开/关闭 auto save的功能. 我们可以用js来控制auto-save this.formOnSave = function (exec ...

  9. Python单元测试unittest测试框架

    本文的主题是自动化测试框架的实现,在实现之前,先了解一下关于unittest模块的相关知识: Python中有一个自带的单元测试框架是unittest模块,用它来做单元测试,它里面封装好了一些校验返回 ...

  10. Python使用requests发送post请求的三种方式

    1.我们使用postman进行接口测试的时候,发现POST请求方式的编码有3种,具体的编码方式如下: A:application/x-www-form-urlencoded ==最常见的post提交数 ...