java 垃圾收集机制
本篇从垃圾收集的算法(方法论)和垃圾收集器(方法论的实现)角度讲java 的垃圾收集机制,希望看完本篇文章对java的垃圾收集有较全面的认识。
概述:java 垃圾收集通过可达性分析算法与两次标记判断对方是否存活,将堆内存分为年轻代(young 1/3)、老年代(old/Tenured 2/3 ), 方法区称为永久代(java7及之前, java 8使用元数据空间(Metaspace) 代替了 PermGen space), 年轻代的内存区域被进一步划分成3个区域Eden、From Survivor、To Survivor 默认比例Eden : from : to = 8 : 1 : 1。Eden是进行内存分配的地方,是一块连续的空闲内存区域。两个Survivor中始终有一个是空白的。在进行垃圾回收的时候,Eden和其中一个非空存活区中还存活的对象根据其存活时间被复制到当前空白的存活区或年老世代中,将这些对象的年龄设置为1。对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定 ),这些对象就会成为老年代。一些较大的对象 ( 即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法.Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。
下图是整个文章思路的思维导图
一、堆空间图示
下图是jvm 内存模型的示意图, 线程私有的部分
二、对象存活判定算法
基于引用计数与基于可达性分析这两大算法的自动内存管理方式最大的不同之处在于:前者只需要局部信息,而后者需要全局信息。而java 中对于对象的存活判断算法使用后者, 前者在微软的COM 组件中使用,C++中较为常见。
1.1 引用计数算法
在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。当A引用B ,B引用A 时就会造成循环引用,导致资源一直被占用, 而C++ 引入weak_ptr弱引用指针来解决循环引用问题。
1.2 可达性分析算法
GC Roots利用图论中的可达性原理来判断,内存是否需要被回收. 图中可达对象便是存活对象,而不可达对象则是需要回收的垃圾内存。可达性分析算法需要全局的对象图信息,从对象图的“根”(也就是必然活的引用)出发扫描出去,基于引用的可到达性来判断对象的生死。这使得对象的生死状态只能批量的被识别出来,然后批量释放死对象。
注:Java语言中,可作为GC Roots的对象包括下面几种:
1) 虚拟机栈(栈帧中的本地变量表)中引用的对象
2) 方法区中类静态属性引用的对象
3) 方法区中常量引用的对象
4) 本地方法栈中JNI(即一般说的Native方法)引用的对象
reference1、reference2、reference3都是GC Roots
三、跟踪收集算法
在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。
3.1 标记-清除(Mark-Sweep)算法
这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
从图中可以很容易看出标记-清除算法实现起来比较容易,但是有一个比较严重的问题就是容易产生内存碎片,碎片太多可能会导致后续过程中需要为大对象分配空间时无法找到足够的空间而提前触发新的一次垃圾收集动作。
3.2 复制(Copying)算法
为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。
3.3 标记-整理(Mark-Sweep)算法
为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
3.4 分代收集算法 (Generational Collecting)
虚拟机中的共划分为三个代:年轻代(Young Generation 堆)、年老代(Old Generation 堆)和持久代(Permanent Generation 方法区、常量池、即时编译代码)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用:复制算法, 只需要付出少量存活对象的复制成本就可以完成回收。
在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。
1).新生代:目前大部分垃圾收集器对新生代都采用复制算法,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。 默认的,Edem : from : to = 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。新生代中98%情况下空白Survivor都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。
Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。
使用java虚拟机-Xmn参数可以指定新生代内存大小。
2).老年代:年老代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在年老代中使用标记-整理垃圾回收算法。
Java虚拟机对年老代的垃圾回收称为MajorGC/Full GC,次数相对比较少,每次回收的时间也比较长。
当新生代中无足够空间为对象创建分配内存,年老代中内存回收也无法回收到足够的内存空间,并且新生代和年老代空间无法在扩展时,堆就会产生OutOfMemoryError异常。java虚拟机-Xms参数可以指定最小内存大小,-Xmx参数可以指定最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出年老代最小和最大内存容量。
3).永久代: 它用来存储class类、常量、方法描述等。对永久代的回收主要回收两部分内容:废弃常量和无用的类。
3.5 Stop-The-World机制简称STW
在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起
为什么要进行 STW 呢? 如果在标记或者计算的过程中如果还有新对象产生。换做是你的话,你能够计算或者又什么方式能够处理这个问题吗?显然JVM设计这个垃圾回收的也没办法,所以这个时候就需要STW了,即Stop The World。暂停工作吧,先把垃圾处理完,大家在继续对外服务工作,要不然大家都玩不下去了
STW是如何实现的 ?这里引入了 安全点( Safepoint)的概念, 是java代码中一个线程可能暂停执行的一个位置,其执行一共可以分为四个阶段,Spin阶段->Block阶段->Cleanup ->VM Operation
- Spin阶段。因为jvm在决定进入全局safepoint的时候,有的线程在安全点上,而有的线程不在安全点上,这个阶段是等待未在安全点上的用户线程进入安全点。
- Block阶段。即使进入safepoint,用户线程这时候仍然是running状态,保证用户不在继续执行,需要将用户线程阻塞。http://blog.youkuaiyun.com/iter_zc/article/details/41892567
- Cleanup。这个阶段是JVM做的一些内部的清理工作。
- VM Operation. JVM执行的一些全局性工作,例如GC,代码反优化。
只要分析这个四个阶段,就能知道什么原因导致的STW时间过长,另外GC的所以垃圾收集算法的实现为了解决一个根本问题,用最短的时间高效回收。
三.典型的垃圾收集器
四、垃圾收集器
4.1 Serial 收集器
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩;垃圾收集的过程中会Stop The World(服务暂停)
参数控制:-XX:+UseSerialGC 串行收集器
1.Serial收集器
单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为Stop The World,下称STW),使用复制收集算法,虚拟机运行在Client模式时的默认新生代收集器。
2.ParNew收集器
ParNew收集器就是Serial的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对象分配规则、回收策略等都与Serial收集器一摸一样。对应的这种收集器是虚拟机运行在Server模式的默认新生代收集器,在单CPU的环境中,ParNew收集器并不会比Serial收集器有更好的效果。
3.Parallel Scavenge收集器
Parallel Scavenge收集器(下称PS收集器)也是一个多线程收集器,也是使用复制算法,但它的对象分配规则与回收策略都与ParNew收集器有所不同,它是以吞吐量较大化(即GC时间占总运行时间最小)为目标的收集器实现,它允许较长时间的STW换取总吞吐量较大化。
4.Serial Old收集器
Serial Old是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收集器。
5.Parallel Old收集器
老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6提供,在此之前,新生代使用了PS收集器的话,老年代除Serial Old外别无选择,因为PS无法与CMS收集器配合工作。
6.CMS(Concurrent Mark Sweep)收集器
CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率较高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩
4.7 G1 收集器
4.7 ZGC 收集器
目前所有的新生代gc都是需要STW的
Serial:单线程STW,复制算法
ParNew:多线程并行STW,复制算法
Parallel Scavange:多线程并行STW,吞吐量优先,复制算法
G1:多线程并发,可以精确控制STW时间,整理算法
ZGC:未分代,所有阶段并发,通过着色指针(Colored Pointer) 和 读取屏障(Load barrier)做并发保证,ZGC并没有彻底摆脱STW。收集器在开始标记,结束标记和开始重新安置时需要暂停。但这种暂停通常很短,只有几毫秒。
五、垃圾收集参数配置
六、总结
推荐阅读:
http://www.cnblogs.com/dolphin0520/p/3783345.html
http://www.infoq.com/cn/articles/cf-java-garbage-references