JVM垃圾收集器详解

说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言。当List还在胚胎时期时,人们就在思考GC需要完成的3件事情:

  1. 哪些内存需要回收?
  2. 什么时候回收?
  3. 如何回收?

一、哪些内存需要回收?

从JVM区域结构看,可将这些区域划分为“静态内存”和“动态内存”两类。程序计数器、虚拟机栈、本地方法3个区域是“静态”的,因为这几个区域的内存分配和回收都具备确定性,都随着线程而生,随着线程而灭。但Java堆和方法区不一样,内存分配都存在不确定性,只有在程序处于运行期间才能知道会创建哪些对象,这部分内存和回收都是动态的,垃圾收集器所关注的是这部分内存。

在堆里面存放着Java世界几乎所有的对象实例,垃圾回收器在对堆进行回收前,第一件事情就是就是要确定这些对象哪些还"存活"着,哪些已经"死去"。那么又怎么确定对象已经"死去"呢?

1.引用计数法:

分配对象时给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是没有再被使用了。客观地说,引用计数法(Reference Counting)的实现简单,判断效率也很高,但是在主流的Java虚拟机里面没有选用引用计数法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。例如:

public class ReferenceCountingGC {
 public Object instance = null;
 private byte[] bigsize = new byte[2*1024*1024];
 public static void testGC(){
 ReferenceCountingGC objA = new ReferenceCountingGC();
 ReferenceCountingGC objB = new ReferenceCountingGC();
 objA.instance = objB;
 objB.instance = objA;
 objA = null;
 objB = null;
 System.gc();
 }
}

当设置objA = null;objB = null后这两个对象再无任何引用,实际上这两个对象已经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。如果这个对象特别大,则会造成严重的内存泄露。

2.可达性分析算法:

可达性分析(Reachability Analysis)的基本思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时(也就是GC Roots到这个对象不可达),则证明此对象是不可用的。如下图所示:

对象Object5、Object6、Object7相互虽然有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

二、什么时候回收?

虚拟机为了分析GC Roots这项工作必须在一个能确保一致性的快照中进行,这里的“一致性”的意思就是指在整个分析期间整个执行系统看起来就像被冻结在某个时间点上——这叫安全点。当然,程序执行时并非在所有地方都能停顿下来开始GC,只有到达安全点时才能暂停。安全点选址也有规定的,选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的。这里的长时间执行的最明显特征是指令列复用,例如方法调用、循环跳转、异常跳转等。

虚拟机为了能让所有线程都“跑”到安全点上停顿下来,设计了两个方案:抢先式中断和主动式中断。其中抢先式中断是虚拟机发生GC时,首先把所有线程全部中断,如果发生有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。这种方式现在比较用了。而主动式中断是虚拟机需要GC时仅仅简单的设置一个标志,各个线程执行到安全点时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

三、如何回收?

3.1 垃圾收集算法:

(1)标记-清除(Mark-Sweep)算法

这是最基础的算法,就像它名字一样,算法分为“标记”和“清除”两个阶段:首先标记处所有需要回收的对象(如哪些内存需要回收所描述的对象),对标记完成后统一回收所有被标记的对象,如下图所示:

缺点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除后悔产生大量的不连续的内存碎片,可能会导致后续无法分配大对象而导致再一次触发垃圾收集动作。

(2)复制算法

为了针对标记-清除算法的不足,复制算法将可用内存容量划分为大小相等的两块,每次只使用一块。当一块的内存用完了,就将还存活的对象复制到另一块上面去。然后把已使用过的内存空间一次清理掉,如下图所示:

缺点:使用内存比原来缩小了一半。

现在的商业虚拟机都采用这种收集算法来回收新生代,有企业分析的得出其实并不需求将内存按1:1的比例划分,因为新生代中的对象大部分都是“朝生夕死”的。所以,HotSpot虚拟机默认的Eden和Survivor的大小比例是8:1。一块Eden和两块Survivor,每次使用一块Eden和一块Survivor,也就是说只有10%是浪费的。如果另一块Survivor都无法存放上次垃圾回收的对象时,那这些对象将通过“担保机制”进入老年代了。

(3)标记-整理(Mark-Compact)算法

复制算法一般是对对象存活率较低的一种回收操作,但对于对象存活率较高的内存区域(老年代)来说,效果就不是那么理想了,标记-整理算法因此诞生了。标记-整理算法和标记-清除算法差不多,都是一开始对回收对象进行标记,但后续不是直接对对象清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,如下图所示:

(4)分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。

3.2 垃圾收集器:

(1)七种垃圾收集器:

  1. Serial(串行GC)-复制
  2. ParNew(并行GC)-复制
  3. Parallel Scavenge(并行回收GC)-复制
  4. Serial Old(MSC)(串行GC)-标记-整理
  5. CMS(并发GC)-标记-清除
  6. Parallel Old(并行GC)--标记-整理
  7. G1(JDK1.7update14才可以正式商用)

说明:

  1. 1~3用于年轻代垃圾回收:年轻代的垃圾回收称为minor GC
  2. 4~6用于年老代垃圾回收(当然也可以用于方法区的回收):年老代的垃圾回收称为full GC
  3. G1独立完成"分代垃圾回收"

注意:并行与并发

  1. 并行:多条垃圾回收线程同时操作
  2. 并发:垃圾回收线程与用户线程一起操作

(2)常用五种组合:

  1. Serial/Serial Old
  2. ParNew/Serial Old:与上边相比,只是比年轻代多了多线程垃圾回收而已
  3. ParNew/CMS:当下比较高效的组合
  4. Parallel Scavenge/Parallel Old:自动管理的组合
  5. G1:最先进的收集器,但是需要JDK1.7update14以上

(2.1)Serial/Serial Old:

特点:

  • 年轻代Serial收集器采用单个GC线程实现"复制"算法(包括扫描、复制)
  • 年老代Serial Old收集器采用单个GC线程实现"标记-整理"算法
  • Serial与Serial Old都会暂停所有用户线程(即STW)

说明:

STW(stop the world):编译代码时为每一个方法注入safepoint(方法中循环结束的点、方法执行结束的点),在暂停应用时,需要等待所有的用户线程进入safepoint,之后暂停所有线程,然后进行垃圾回收。

适用场合:

  • CPU核数<2,物理内存<2G的机器(简单来讲,单CPU,新生代空间较小且对STW时间要求不高的情况下使用)
  • -XX:UseSerialGC:强制使用该GC组合
  • -XX:PrintGCApplicationStoppedTime:查看STW时间
  • 由于它实现相对简单,没有线程相关的额外开销(主要指线程切换与同步),因此非常适合运行于客户端PC的小型应用程序,或者桌面应用程序(比如swing编写的用户界面程序),以及我们平时的开发、调试、测试等。

(2.2)ParNew/Serial Old:

说明:

ParNew除了采用多GC线程来实现复制算法以外,其他都与Serial一样,但是此组合中的Serial Old又是一个单GC线程,所以该组合是一个比较尴尬的组合,在单CPU情况下没有Serial/Serial Old速度快(因为ParNew多线程需要切换),在多CPU情况下又没有之后的三种组合快(因为Serial Old是单GC线程),所以使用其实不多。

-XX:ParallelGCThreads:指定ParNew GC线程的数量,默认与CPU核数相同,该参数在于CMS GC组合时,也可能会用到

(2.3)Parallel Scavenge/Parallel Old:

特点:

  1. 年轻代Parallel Scavenge收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
  2. 年老代Parallel Old收集器采用多个GC线程实现"标记-整理"算法
  3. Parallel Scavenge与Parallel Old都会暂停所有用户线程(即STW)

说明:

  1. 吞吐量:CPU运行代码时间/(CPU运行代码时间+GC时间)
  2. CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)
  3. Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况)

参数设置:

  1. -XX:+UseParallelOldGC:使用该GC组合
  2. -XX:GCTimeRatio:直接设置吞吐量大小,假设设为19,则允许的最大GC时间占总时间的1/(1 +19),默认值为99,即1/(1+99)
  3. -XX:MaxGCPauseMillis:最大GC停顿时间,该参数并非越小越好
  4. -XX:+UseAdaptiveSizePolicy:开启该参数,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold这些参数就不起作用了,虚拟机会自动收集监控信息,动态调整这些参数以提供最合适的的停顿时间或者最大的吞吐量(GC自适应调节策略),而我们需要设置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio两个参数就好(当然-Xms也指定上与-Xmx相同就好)

适用场合:

  1. 很多的CPU计算任务而用户交互任务较少的情况
  2. 不想自己去过多的关注GC参数,想让虚拟机自己进行调优工作
  3. 对吞吐量要求较高,或需要达到一定的量。

(2.4)ParNew/CMS:

说明:

  1. 以上只是年老代CMS收集的过程,年轻代ParNew看"2.2、ParNew/Serial Old"就好
  2. CMS是多回收线程的,不要被上图误导,默认的线程数:(CPU数量+3)/4
  3. CMS主要注重STW的缩短(该时间越短,用户体验越好,所以主要用于处理很多的交互任务的情况)

特点:

1.年轻代ParNew收集器采用多个GC线程实现"复制"算法(包括扫描、复制)

2.年老代CMS收集器采用多线程实现"标记-清除"算法

  • 初始标记:标记与根集合节点直接关联的节点。时间非常短,需要STW
  • 并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。时间较长。
  • 重新标记:重新遍历trace并发期间修改过的引用关系对象。时间介于初始标记与并发标记之间,通常不会很长。需要STW
  • 并发清理:直接清除非存活对象,清理之后,将该线程占用的CPU切换给用户线程

3.初始标记与重新标记都会暂停所有用户线程(即STW),但是时间较短;并发标记与并发清理时间较长,但是不需要STW

关于并发标记期间怎样记录发生变动的引用关系对象,在重新标记期间怎样扫描这些对象

缺点:

  • 并发标记与并发清理:按照说明的第二点来讲,假设有2个CPU,那么其中有一个CPU会用于垃圾回收,而另一个用于用户线程,这样的话,之前是两CPU运行用户线程,现在是一个,那么效率就会急剧下降。也就是说,降低了吞吐量(即降低了CPU使用率)。
  • 并发清理:在这一过程中,产生的垃圾无法被清理(因为发生在重新标记之后)
  • 并发标记与并发清理:由于是与用户线程并发的,所以用户线程可能会分配对象,这样既可能对象直接进入年老代(例如,大对象),也可能进入年轻代后,年轻代发生minor GC,这样的话,实际上要求我们的年老代需要预留一定空间,也就是说要在年老代还有一定空间的情况下就要进行垃圾回收,留出一定内存空间来供其他线程使用,而不能等到年老代快爆满了才进行垃圾回收,通过-XX:CMSInitiatingOccupancyFraction来指定当年老代空间满了多少后进行垃圾回收
  • 标记-清理算法:会产生内存碎片,由于是在老年代,可能会提前触发Full GC(这正是我们要尽量减少的)

参数设置:

  • -XX:+UseConcMarkSweepGC:使用该GC组合
  • -XX:CMSInitiatingOccupancyFraction:指定当年老代空间满了多少后进行垃圾回收
  • -XX:+UseCMSCompactAtFullCollection:(默认是开启的)在CMS收集器顶不住要进行FullGC时开启内存碎片整理过程,该过程需要STW
  • -XX:CMSFullGCsBeforeCompaction:指定多少次FullGC后才进行整理
  • -XX:ParallelCMSThreads:指定CMS回收线程的数量,默认为:(CPU数量+3)/4

适用场合:

用于处理很多的交互任务的情况

方法区的回收一般使用CMS,配置两个参数:-XX:+CMSPermGenSweepingEnabled与-XX:+CMSClassUnloadingEnabled

适用于一些需要长期运行且对相应时间有一定要求的后台程序

(2.5)G1

说明:

  • 从上图来看,G1与CMS相比,仅在最后的"筛选回收"部分不同(CMS是并发清除),实际上G1回收器的整个堆内存的划分都与其他收集器不同。
  • CMS需要配合ParNew,G1可单独回收整个空间

原理:

  • G1收集器将整个堆划分为多个大小相等的Region
  • G1跟踪各个region里面的垃圾堆积的价值(回收后所获得的空间大小以及回收所需时间长短的经验值),在后台维护一张优先列表,每次根据允许的收集时间,优先回收价值最大的region,这种思路:在指定的时间内,扫描部分最有价值的region(而不是扫描整个堆内存),并回收,做到尽可能的在有限的时间内获取尽可能高的收集效率。

运作流程:

  • 初始标记:标记出所有与根节点直接关联引用对象。需要STW
  • 并发标记:遍历之前标记到的关联节点,继续向下标记所有存活节点。在此期间所有变化引用关系的对象,都会被记录在Remember Set Logs中
  • 最终标记:标记在并发标记期间,新产生的垃圾。需要STW
  • 筛选回收:根据用户指定的期望回收时间回收价值较大的对象(看"原理"第二条)。需要STW

优点:

  1. 停顿时间可以预测:我们指定时间,在指定时间内只回收部分价值最大的空间,而CMS需要扫描整个年老代,无法预测停顿时间
  2. 无内存碎片:垃圾回收后会整合空间,CMS采用"标记-清理"算法,存在内存碎片
  3. 筛选回收阶段:
  • 由于只回收部分region,所以STW时间我们可控,所以不需要与用户线程并发争抢CPU资源,而CMS并发清理需要占据一部分的CPU,会降低吞吐量。
  • 由于STW,所以不会产生"浮动垃圾"(即CMS在并发清理阶段产生的无法回收的垃圾)

适用范围:

  • 追求STW短:若ParNew/CMS用的挺好,就用这个;若不符合,用G1
  • 追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面没有优势

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!

(0)

相关推荐

  • 从JVM分析Java的类的加载和卸载机制

    类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构. 加载.class文件的方式: 1.从本地系统中直接加载 2.通过网络下载.class文件 3.从zip,jar等归档文件中加载.class文件 4.从专有数据库中提取.class文件 5.将Java源文件动态编译为.class文件 类的加载的最终产品是位于堆区中的Class对象. Class对象封装了类在

  • 学习JVM之java内存区域与异常

    一.前言 java是一门跨硬件平台的面向对象高级编程语言,java程序运行在java虚拟机上(JVM),由JVM管理内存,这点是和C++最大区别:虽然内存有JVM管理,但是我们也必须要理解JVM是如何管理内存的:JVM不是只有一种,当前存在的虚拟机可能达几十款,但是一个符合规范的虚拟机设计是必须遵循<java 虚拟机规范>的,本文是基于HotSpot虚拟机描述,对于和其它虚拟机有区别会提到:本文主要描述JVM中内存是如何分布.java程序的对象是如何存储访问.各个内存区域可能出现的异常. 二.

  • JVM加载一个类的过程

    类的加载过程 Java源代码被编译成class字节码,JVM把描述类数据的字节码.Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制. 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading).验证(Verification).准备(Preparation).解析(Resolution).初始化(Initialization).使用(Using).卸载(Unloading)七个阶段,

  • 启动 Eclipse 弹出 Failed to load the JNI shared library jvm.dll 错误的解决方法

    错误如下图所示: 有时候,新电脑上回碰到打开Eclipse时,弹出提示"Failed to load the JNI shared library jvm.dll"错误,这里给大家分享解决方案. 通常情况下,如果你是64位的系统,却安装了32位的JDK,就会导致上面的情况. 解决办法 1.卸载掉原来安装的32位的JDK,然后安装64位的JDK到电脑中. 2.重新修改path 和 classpath 两个环境变量.因为在64系统当中,32位软件回安装到program files (*86

  • 简单谈谈JVM、JRE和JDK的区别与联系

    为了学习数据库,重装了系统,之前前一直在用eclipse,现在准备换成myeclipse,这之前当然需要重新设置环境变量,顺手写下有关jdk.jre与jvm的区别与联系以供交流参考. 首先来说一下JDK JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK). JDK是整个JAVA的核心,包括了Java运行环境(Java Runtime Envirnment),一堆Java工具(javac/java/jdb等)和Java基础的类库(即Java API )

  • 分别在Linux和Windows下设置JVM内存的简单方法

    Linux服务器: 在/usr/local/apache-tomcat-5.5.23/bin 目录下的catalina.sh 添加:JAVA_OPTS='-Xms512m -Xmx1024m' 或者 JAVA_OPTS="-server -Xms800m -Xmx800m -XX:MaxNewSize=256m" 或者 CATALINA_OPTS="-server -Xms256m -Xmx300m" Windows服务器: 在/apache-tomcat-5.5.

  • java jvm的知识详细介绍

    java jvm 详解: 关于jvm的相关知识 一.堆内存和栈内存 1.jvm中的栈内存主要存储的是基本类型的变量和对象的引用 2.jvm中的堆内存主要存储的是用new来创建的对象和数组,可变长字符串(StringBuilder和StringBuffered)都是存储在堆内存的 使用堆的优点是动态分配存储空间,更灵活,但缺点是由于要动态分配内存,所以存储速度较慢:而使用栈速度就比较快,也可以实现数据的共享,但缺点是栈中的数据大小和生存期是必须确定的,缺乏灵活性 3.静态存储分配是存储静态变量和静

  • 深入解析JVM对dll文件和对类的装载过程

    JVM的对dll文件的装载过程 操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境. 1.创建JVM装载环境和配置 2.装载JVM.dll 3.初始化JVM.dll并挂界到JNIENV(JNI调用接口)实例 4.调用JNIEnv实例装载并处理class类. 一.JVM装入环境,JVM提供的方式是操作系统的动态连接文件.     既然是文件那就一个装入路径的问题,Java是怎么找这个路径的呢?当你在调用Java test的时候,操作系统会在path下在你的Java

  • JVM 参数配置详细介绍

     JVM参数配置 设置堆大小 -Xms 初始堆大小 -Xmx 最大堆大小 -Xmn 设置年轻代大小 设置每个线程堆栈大小 -Xss 设置每个线程的堆栈大小 设置年轻代大小 -XX:NewSize= -XX:MaxNewSize=设置年轻代大小 -XX:NewRatio=设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代) -XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值 设置持久代大小 -XX:MaxPermSize=设置持久代大

  • JVM Tomcat性能实战(推荐)

    本节只是介绍实战部分,具体的理论参数,请自行百度. 所需工具:linux服务器  Jmeter测试工具  xshell   一个web应用 Tomcat的JVM参数可以配置在catalina.sh,如果是在window上可以配置.bat文件 配置1: 这里 我配置了一个gc日志路径为/home/log/gc.log ,打印gc的日志,初始堆和最大堆内存设置为50M,输出Dump文件在内存溢出的时候 ,使用串行垃圾收集器,永久代大小为50m. 将web应用放到对应的目录,配置好server.xml

  • 详解Java中的JDK、JRE、JVM

    JDK.JRE.JVM JDK包含JRE,而JRE包含JVM JDK(Java Development Kit)是针对Java开发员的产品,是整个Java的核心,包括了Java运行环境JRE.Java工具和Java基础类库.Java Runtime Environment(JRE)是运行JAVA程序所必须的环境的集合,包含JVM标准实现及Java核心类库.JVM是Java Virtual Machine(Java虚拟机)的缩写,是整个java实现跨平台的最核心的部分,能够运行以Java语言写作的

随机推荐