JVM 的内存管理(垃圾收集)

文章主要从垃圾收集来讲解内存的管理

如何知道对象可以被回收

JVM垃圾回收,主要是对“死亡”的对象进行回收,因此如何知道一个对象是“死亡”的,是可以被垃圾回收器回收的,JVM有几个方法:

1.引用计数器算法

在堆中的每一个对象才有一个引用计数器,当一个对象被创建了,任何一个对象变量赋值为该对象的引用时,对象的计数器就加1;变量不再引用该对象时,
对象的计数器就减1,当一个对象的计数为0时,它就会当作垃圾被回收器收集,这种就去看起来非常简单、易理解,但缺点就是,两个对象相互引用时,即对象循环引用时,
则这些对象的计数永远不可能为0,所以实际上这种方法不被接受,JVM不采用这种策略。

2.对象可达性算法

这种方法很好地解决了引用计数算法中出现“循环引用”的问题,创建的对象会以一个树状的形式,树根节点就是“GC Roots”的对象,从GC Roots开始往树叶遍历标记,
没被标记的对象则是不可达的,是垃圾对象,是可以被回收的,这种方法,就算有循环引用的对象,只要它们不被标记则是可以被回收的。

被当作GC Roots的对象:

JVM栈中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI引用的对象;

回收算法

1.标记-清除算法

在对象可达性算法中,遍历树时,树节点对象进行“标记”,而遍历过后未被标记的对象进行清除,这就是标记-清除算法,不过有一个缺点,会带来另一个新的问题: 内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。

2.复制算法

由于标记-清除算法会带来内存碎片化的问题,而复制算法可以很好地解决这一问题。首先还是先标记处待回收内存和不用回收的内存,下一步将不用回收的内存复制到新的内存区域,这样旧的内存区域就可以全部回收,而新的内存区域则是连续的。它的缺点就是会损失掉部分系统内存,因为你总要腾出一部分内存用于复制。
Java堆中被分为了新生代、老年代,永久代,这样的划分是方便回收。Java堆中的新生代就使用了复制算法。在新生代中又分为了三个区域:Eden 空间、To Survivor空间、From Survivor空间。
新创建的对象实例会在Eden空间,发生在Eden空间上的垃圾回收称为Minor GC,当在新生代发生一次GC后,会将Eden和其中一个Survivor空间的内存复制到另外一个Survivor中,如果反复几次有对象一直存活,此时内存对象将会被移至老年代。可以看到新生代中Eden占了大部分,而两个Survivor实际上占了很小一部分。这是因为大部分的对象被创建过后很快就会被回收。


所以年轻代的垃圾回收是使用了复制算法。

3.标记-压缩算法

对于新生代,大部分对象都不会存活,所以在新生代中使用复制算法较为高效,而对于老年代来讲,大部分对象可能会继续存活下去,如果此时还是利用复制算法,效率则会降低。标记-压缩算法首先还是“标记”,标记过后,将不用回收的内存对象压缩到内存一端,此时即可直接清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。老年代的垃圾回收称为“Major GC”。

所以年老代的垃圾回收是使用了标记-压缩算法。

回收器类型

年轻代分为:Eden、两个survivor空间:To和From


1.串行收集器(Serial Collector)

年轻代和年老代都是用串行收集,即单线程的,收集器工作时,程序会暂停工作。
从J2SE 5.0 release,当程序不是以"-server"方式启动的,默认都是使用"串行收集器",
当然了,也可以通过指定启动参数"-XX:+UseSerialGC"来指定"串行收集器"。

1.1.使用串行收集器的年轻代

Eden中存活的对象复制到"To"空间中(如果对象太大以致于"To"放不下则会直接复制到年老代)
"From"空间的存活对象也复制到"To"空间中,如果"From"空间中的存活对象已经被收集很多次(次数阀值有默认值,亦可配置),那么会把这部分对象复制到年老化。
如果"To"空间满了,Eden和"From"中存活的对象会复制到年老代。
当完成了一次"年轻代收集",Eden和"From"空间都是空的,"From"和"To"互换,则下次进行"年轻代收集"之前,"To"就是空的了。

.


1.2.使用串行收集器的年老代

在年老化或者永久代,串行收集器使用了"标记-清除-合并"(mark-sweep-compact)的算法收集垃圾对象。
收集器标识出存活的对象,然后清除掉无用的垃圾对象,将存活的对象合并到年老化的前部分,
这样就可以空出后部分邻接块内存给新进来的对象,这样可以减少内存碎片。

2.并行收集器(Parallel Collector)

当机器出现多个CPU,并行收集器,也叫吞吐量收集器,可以充分利用机器的多个CPU。
从J2SE 5.0 release,如果程序以"-server"方式启动的,默认使用"并行收集器",
或者通过指定启动参数"-XX:+UseParallelGC"来指定"并行收集器"。

2.1.使用并行收集器的年轻代

并行收集器跟串行收集器一样收集年轻代,JAVA应用在收集期间也会停止运行(stop-the-world),不过多CPU并行收集可以减少垃圾收集时间,从而提高应用的吞吐量。

2.2.使用并行收集器的年老代

并行收集器跟串行收集器一样单线程地收集年老代,算法也是"mark-sweep-compact"。

3.并行合并收集器(Parallel Compacting Collector)

并行合并收集器是从J2SE 5.0 update 6开始被采用的,它跟并行收集器不同的是年老代的收集算法不同。
如果我们应用想使用这种收集器,可以通过"-XX:+UseParallelOldGC"启动参数来指定使用并行合并收集器。

3.1.使用并行合并收集器的年轻代

年轻代的收集跟并行收集器是一样的。

3.2.使用并行合并收集器的年老代

年老代分为3个步骤:标记、统计、合并。这里用到分的思想,把年老代划分为很多个固定大小的区(region)。
标记阶段(多线程),把所有存活的对象划分为N组(应该与回收线程数相同),每一个线程独立的负责自己那一组,标记存活对象的位置以及所在区(Region)的存活率信息。
统计阶段(单线程),统计每一个区(Region)的存活率,原则上靠前面的存活率较高,从前到后, 找到值得合并的开始位置(绝大多数对象都存活的区不值得合并)。
合并阶段(多线程),依据统计阶段的信息,把存活的对象从一个区(Region)复制到另外一个区(Region)。

并行合并收集器在年老代是多线程处理的,所以比并行收集器中断时间还少,它更适合于对应用中断时间有限制的情况下,
不过,它不适合大量应用共享的机器,并且这些应用不能单独长时间垄断CPU,在这种情况,可以通过"–XX:ParallelGCThreads=n"限制垃圾收集的线程数。

4.并发标记清除收集器(Concurrent Mark-Sweep Collector)CMS

在串行、并行收集器中,虽然年轻代的收集不会中断时间很长,年老代的收集不频繁,但是年老代收集中断比较长,所以出现了CMS收集器,CMS收集器也称低延时收集器。

4.1.使用CMS收集器的年轻代

CMS收集器的年轻代与并行收集器的年轻代一样。

4.2.使用CMS收集器的年老代

初始标记(Initial Mark)、并发标记(Concurrent Mark)、重标记(Remark)、并发清除(Concurrent Sweep)
初始标记:单线程,标记应用所有存活的对象->存活对象集合,应用程序短暂暂停;
并发标记:单线程,此时应用程序也在同时运行(所以叫并发标记),标记初始标记中的"存活对象集合"的存活对象,应用同时运行可能更新一些对象的引用,所以会有"重标记"阶段;
重标记:多线程,对"并发标记"阶段引用被更新过的对象,标记出存活对象,应用程序第二次暂停;
并发清除:单线程,经过之前几个阶段,堆里的垃圾对象已经被标记出来,清除垃圾对象。

在收集年老代的垃圾对象后,CMS并没有复制合并空闲内存,这可以节省垃圾收集时间,不过会存在内存碎片。
CMS收集器并没有像其他收集器那样,等到年老代已经满的时候才收集垃圾,CMS会在年老代未满之前启动收集工作,
因为CMS多次暂停应用标记垃圾对象,总的时间变长,而CMS在进行垃圾收集时应用也在并发运行,所以不能等年老代满了再收集,一般CMS会在年老代达到阀值就进行收集垃圾,
可以通过"-XX:CMSInitiatingOccupancyFraction=n"来设计阀值,n默认是68,即年老代使用达到68%就启动年老代收集。

增量模式
把回收操作分为多个片段,执行一个片段之后释放CPU资源给应用程序,在年轻代收集期间,周期性地接着上次的结果继续回收下去。目的也是减少延时。
当机器只有少量CPU时,这个模式非常有用,比如只有1,2个CPU,可以减少延时。

通过"-XX:+UseConcMarkSweepGC"参数指定CMS收集器,如果希望指定增量模式收集,在"-XX:+UseConcMarkSweepGC"的基础上,再指定参数"-XX:+CMSIncrementalMode"。

译自
http://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf?ssSourceSiteId=otncn

5.G1收集器

垃圾优先型垃圾回收器
G1收集器从JDK 7 update 4才支持,它是在"-server"模式下使用的垃圾收集器。
-XX:+UseG1GC设置命令参数指定G1收集器;
-XX:MaxGCPauseMillis=200设置最大的中断时间,默认是200毫秒;
-XX:InitiatingHeapOccupancyPercent=45设置触发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。

G1比CMS更好的解决方案,G1会进行合并操作,此外,G1提供了更多的可预测的垃圾收集暂停比CMS收集器,并允许用户指定所需的暂停目标。
内存分成许多固定大小的内存域(每个域是Eden、Survivor、old generation spaces, In addition, there is a fourth type of object known as Humongous regions. ),大小从1M到32M不等的2000个区域,所以G1垃圾收集器最大内存32M x 2000 = 62.5G。
而这些区域在逻辑上是连读的一小块内存,不像CMS那样是物理上是连续的。

5.1.年轻代收集

Young GC时,STW(stop-the-world),经过收集后的存活对象被复制到一个或多个空闲域,达到某个阀值时(默认15)后移到年老代,
完成收集后,根据Young GC的统计信息,包含中断的时间等,来调整Eden和Survivor域的大小,可以合理利用内存,提高收集效率。
Young GC收集是多线程并行的。

5.2.年老代收集

初始标记(Initial Mark):Stop-The-World,Young GC标记Survivor域(根区域)中的对象,这些对象可以引用到年老代的对象;
根区域扫描(Root Region Scanning):在初始标记阶段标记存活的对年老代的引用对象,该阶段java应用并发执行,不会STW,这阶段完成之后,再进行下一次Young GC;
并发标记(Concurrent Marking):在整个堆中标记存活的对象,该阶段java应用并发执行,不会STW,这阶段有可能被Young GC中断;
重标记(Remark):Stop-The-World,使用初始快照算法:snapshot-at-the-beginning (SATB)标记"并发标记"阶段产生的垃圾(因为这个阶段应用同时并发执行),"初始快算法"比CMS的"重标记"阶段更快。
清理(Cleanup):Stop-The-World,统计存活的对象和空的区域,清除Remembered Set;与java应用并发执行,重置空的区域并还给空闲列表;所以这阶段是部分STW,部分并发;
复制(Copying):Stop-The-World,将存活对象复制到新的区域,会在年轻代完成(GC日志:[GC pause (young)]),也可能在年轻代和年老代完成(GC日志:[GC Pause (mixed)],mixed:混合)

5.3.Remembered Set

G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。

参考

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

http://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf?ssSourceSiteId=otncn

http://www.oracle.com/technetwork/cn/articles/java/g1gc-1984535-zhs.html

可参照这个博客,写得不错- JVM 垃圾回收算法及垃圾回收器:

http://blog.brucefeng.info/post/jvm-garbage-collection

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值