垃圾收集器与内存分配策略

来源:互联网 发布:e博乐网络 编辑:程序博客网 时间:2024/06/05 15:47

参考书籍:深入理解Java虚拟机:JVM高级特性与最佳实践(第三章)

看了《深入理解Java虚拟机:JVM高级特性与最佳实践》之第三章垃圾收集器与内存分配策略,了解了虚拟机的垃圾回收的东西,但是尚未有自己更深的见解,因此在此,只是把看过的自己能记住的一些东西记录下来,依旧是待完善。

java不需要像C一样管理内存溢出等问题,这正是因为有了java虚拟机的GC特性(Garbage Collect,垃圾回收特性)。因此接下来记录的思路是这样的:

1.垃圾回收顾名思义是收集掉不需要了的对象,腾出内存空间给其他的对象使用,那么问题来了,什么样的对象是不再需要的呢?据传有两种判断方式:

1)引用计数算法。给对象添加一个引用计数器,当对象被引用时计算器+1,引用失效时-1,这样当计数器为0时,说明该对象不再被使用,这种情况下,可以判定对象已死。这种方式的优点是:执行效率高;但是缺点是:不能处理相互循环引用。

2)可达性分析算法。在第二篇博客里提到的“有向图方式”就是现在所说的可达性分析算法。即从根节点出发,向下查找对象,如果可以到达对象,则说明该对象还处于被引用中,否则就说明对象已死。如图所示(图来自于参考书籍中),其中1,2,3,4对象仍然存活,5,6,7已经死亡。该算法的优点是,有效的解决了对象相互引用的问题,但是缺点是效率略低,不过java虚拟机有自己的方式来提高效率,接下来会做介绍。


java虚拟机的垃圾回收算法中到底是采用哪种方式来判断对象已死呢,二者的最大区别就是对于相互循环引用的对象是否能够回收,如果能够回收,说明采用了可达性分析算法,否则是计数法,因此,接下来我们通过一段程序来验证下。

public class Velocity{

    public Object instance = null;

    public static void testGC(){

        Velocity objA = new Velocity();

        Velocity objB = new Velocity();

       objA.instance = objB;

       objB.instance = objA;

       objA = null;

      objB = null;

       System.gc();

    }

}

通过运行后的GC日志分析得出,并没有因为objA和objB相互引用就不回收,这说明java虚拟机采用了第二种算法。

但是第二种算法存在一个问题,很多应用仅仅方法就有几百兆,如果一个个的去检查引用,势必会使得效率特别低,而且在GC在执行时会出现一个停顿(Stop the world STW),这是为了保持一致性,也就是说在分析对象引用关系时,不会出现引用关系还在不断变化的情况,因此会出现这样的一个停顿。在停顿时,GC也不是一个不漏的去检查引用,在HotSpot的实现中,使用了一组OopMap的数据结构,在类加载完成时把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特点的位置记录下栈和寄存器中哪些位置是引用,这样GC在扫描时就可以直接知道引用关系了,而且GC没有为每个指令都生成OopMap,只在特定的位置生成,这个点称为安全点(Safespoint),程序在运行到该点的时候会停顿下来执行GC。但是如果有些程序没处于执行状态,比如线程处于Sleep或者Blocked状态,这种情况下就不会走到安全点,对于这种情况,就通过安全区域来解决该问题,该区域的引用关系不会发生变化,因此可以走到这里时可以发起GC。

对于GC采用什么算法有了大致的了解,但是还有两点需要提到的是,什么样的对象可以作为GC Roots对象?引用如何划分?

该书是这么说的,在Java语言中,可作为GC Roots的对象包括下面几种:

a.虚拟机栈(栈帧中的本地变量表)中引用的对象

b.方法区中类静态属性引用的对象

c.方法区中常量引用的对象

d.本地方法栈中JNI(即一般说的Native方法)引用的对象。

Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4种,这4种强度依次逐渐减弱。

强引用:有强引用存在,垃圾收集器就不会回收掉该引用的对象。

软引用:有用但非必须的对象,在系统发生内存溢出异常之前,会对这些对象进行回收。

弱引用:非必须的对象,只能生存到下一次垃圾收集之前。

虚引用:不能通过虚引用来取得一个对象实例,虚引用关联主要是为了能在该对象被回收器回收时受到一个系统通知

2.垃圾回收的算法有哪些?

1)标记-清除算法:先对对象进行标记,然后再清除,缺点是会有很多不连续的内存空间。

2)复制算法:比如一张纸,对折后,在其中半张上放上红黑的棋子,把黑子拿走后(此处代指垃圾清除后),把剩下的红子(还存活的对象)转移到那半张纸的相同位置处。虽然效率高,但是缺点也很明显,浪费空间,一整块的内存只能使用一部分。

3)标记-整理算法:该算法和标记-清除算法类似,唯一不同的是在垃圾回收后对剩余的对象空间进行整理,移动到连续的位置,这样就会留出较大的内存空间了。

4)分代算法:这种算法是目前常用的,即是在新生代采用复制算法,因为新生代的对象垃圾回收频繁,而在垃圾回收不频繁的老年代采用标记-整理算法,这样就可以利用各自的优势了。

3.目前的垃圾收集器有哪些呢?

先根据语境解释两个名词:

并发(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。


图中相连的收集器表示是可以一起使用的。

1)serial收集器(最基本,发展历史最悠久的收集器)。

新生代使用,单线程收集器(在收集时只使用一个CPU或者一条收集线程去完成垃圾收集工作,暂停所有其他的线程)。优点是简单高效(没其他线程干扰,可以专心做垃圾收集工作)。目前为止,仍然是虚拟机运行在Client模式下的默认新生代收集器。收集器运行过程如图3-6所示。


2)ParNew收集器。

实际是Serial收集器的多线程版本,其性能在单个CPU环境下,低于Serial,随着CPU数量增加,她对于GC时系统资源的有效利用还是有好处的,默认开启的收集线程数与CPU的数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。其运行示意图如图3-7所示。


3)parallel Scavenge收集器。

新生代收集器,采用复制算法,并行的多线程收集器。与ParNew等收集器不同的是,它目标是达到一个可控制的吞吐量(Throughput),因此常被称为“吞吐量优先”收集器,其他收集器则是尽可能的缩短时GC时用户线程的停顿时间。-XX:+UseAdaptiveSizePolicy参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了。虚拟机会根据当前系统的运行情况手机性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式成为GC自适应的调节策略(GC Ergonomics)。

吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间),比如虚拟机总运行时间100min,垃圾收集1min,那么吞吐量就是99/100=99%。

4)Serial Old收集器。

Serial收集器的老年代版本,单线程收集器,采用“标记-整理”算法。它有两个主要用途:一种是与Parallel Scavenge收集器搭配使用,另一种是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。


5)Parallel Old收集器。

是Parallel Scavenge收集器的老年代版本,采用多线程和“标记-整理”算法。JDK1.6才开始提供,它的出现解决了Parallel Scavenge收集器只能与在服务端应用性能上不是很好的Serial Old收集器的配合,是的Parallel Scavenge和Parallel Old组合成了“吞吐量优先”的应用组合。在注重吞吐量以及CPU资源敏感的场合可以考虑使用该组合,图3-9位Parallel Old收集器的工作过程示意图。


6)CMS收集器(ConCurrent Mark Sweep)。

是一种以获得最短回收停顿时间为目标的垃圾收集器,基于“标记-清除”算法,整个过程分为四个步骤:

a.初始标记(CMS initial mark):速度快,标记GC Roots能直接关联到的对象

b.并发标记(CMS concurrent mark):GC Roots Tracing的过程

c.重新标记(CMS remark):修正并发标记期间因为用户程序运行而导致的标记变化的那部分对象的标记记录,停顿稍长,但远比并发标记短

d.并发清除(CMS concurrent sweep)

并发标记和并发清除耗时最长。其回收过程是与用户线程一起并发执行的。如图3-10

优点:并发收集,低停顿

缺点:对CPU资源很敏感;无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生;基于“标记-清除”算法实现,会带来较多的空间碎片。

7)G1收集器

最前沿的成果之一,尚未在商业中实用。面向服务端应用的垃圾收集器,运作步骤如下:

a.初始标记(Initial Marking):标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,需要停顿,但耗时短

b.并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活的对象,耗时长,但可与用户程序并发执行

c.最终标记(Final Marking):修正在并发标记期间因用户程序继续运作而导致标记产生变动的那部分记录

d.筛选回收(Live Data Counting and Evacuation):筛选回收阶段对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。

和其他GC收集器相比,具有如下特点:

a.并行与并发

b.分代收集

c.空间整合,从整体上看采用标记-整理算法,局部(两个Region之间)基于复制算法实现

d.可预测的停顿

运行示意图如图3-11所示。


3.怎么查看GC日志

通过设置-XX:+PrintGCDetails可以让垃圾收集行为打印出日志。

16210.104: [GC [PSYoungGen: 6958K->64K(182272K)] 236019K->229125K(494720K), 0.0038080 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
16210.108: [Full GC (System) [PSYoungGen: 64K->0K(182272K)] [PSOldGen: 229061K->229062K(312448K)] 229125K->229062K(494720K) [PSPermGen: 143700K->143700K(144448K)], 0.3927203 secs]

如上所示为比较典型的GC日志。

最前面的数字16210.104和16210.108代表了GC发生的时间,这个数字的含义是从Java虚拟机启动以来经过的秒数。

GC日志开头的[GC和[Full GC说明这次垃圾收集的停顿类型,而不是用来区分新生代GC还是老年代GC的。如果有Full说明GC发生了STW(Stop-The-World)的。

[PSYoungGen,[PSOldGen,[PSPermGen代表的是区域名称与使用的GC收集器,比如PSYong新生代的Parallel Scavenge收集器,PSOldGen老年代的Parallel Old。

后面方括号内部的6958K->64K(182272K)含义是GC前该内存区域已使用的容量->GC后该内存区域已使用容量(该内存区域总容量)。

方括号之外的236019K->229125K(494720K)表示GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)。

 0.0038080 secs表示该内存区域GC所占用的时间,单位是秒。[Times: user=0.00 sys=0.00, real=0.00 secs] user,sys和real与Linux的time命令含义一致,分别代表用户态消耗的CPU时间、内核态消耗的CPU事件和操作从开始到结束所经过的墙钟时间(Wall Clock Time)。

CPU时间与墙钟时间的区别是,墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以user或sys时间超过real时间是正常的。















0 0
原创粉丝点击