深入理解Java虚拟机:JVM高级特性与最佳实践(二):垃圾回收算法与垃圾回收器

原创
2015/11/08 11:03
阅读数 175

经过半个世纪的发展,内存的动态分配与内存回收技术已经相当成熟,一切看起来已经进入自动化时代,那么我们为什么还要去关注GC和内存分配呢?原因很简单:当需要排查各种内存溢出、内存泄漏的时,当垃圾收集器成为系统更高并发量的瓶颈时,我们就需要对这些“自动化的技术”实施必要的监控和调节。

引用计数算法:

很多教科书判断对象是否存活的算法是这个样子的:给对象添加一个引用计数器,每当有一个地方引用它时,计数器就加一,当引用失效时,计数器就减一,任何时刻计数器为0的对象就是不可能被使用的。

但是java 语言中没有使用引用计数器算法来管理内存,其主要的原因是它很难解决对象之间的相互循环引用的问题。

比如 对象a 和 b 中都有属性 c,a.c= b ,b.c=a  除此之外,两个对象再无任何其他引用,实际上这两个对象不可能继续被访问,但是他们都互相引用着对方,导致他们的计数器都不为0,,于是引用计数器算法无法通知GC收集器回收他们。

根搜索算法:

在主流的商业语言中,java 和 C#都是使用的是根搜索算法来判定一个对象是否存活。这个算法的基本思路就是经过一系列的名为“GC Roots Tracing”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径为引用链,当一个对象到GC Roots没有任何引用链相连(用图论的话说就是从GC Roots到这个对象不可达)时,则证明此对象是不可引用的。

在java 语言里,可作为GC ROOTs 的对象包括下面这几种:

虚拟机栈中引用的对象

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

方法区中常量引用的对象

本地方法栈中引用的对象


在谈引用

在jdk 1.2 之后 java 对引用的概念进行了扩充:将引用分为 强引用、软引用、弱引用、虚引用

强引用:是程序代码普遍存在的,类似于Object obj = new Object();这类的引用只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。

软引用:用来描述一些还有用,单非必须的对象,对于软引用关联着的对象,系统在将要发生内存溢出异常之前,将会把这些对象列入入回收范围之中并且进行第二次回收。

弱引用:被弱引用关联的对象只能生存到下一次垃圾收集发生之前。

虚引用:一个对象是否有虚引用的存在。完全不会对生存时间构成影响,也无法通过虚引用来获取一个对象实例。


生存还是死亡?

在根搜索算法中不可达的对象,也并非是非死不可的,这时候他们处于"缓刑"阶段,要真正宣告一个对象死亡。至少要经历两次标记过程:如果对象在通过根搜索后发现没有与GC Roots相连的引用链,那他将会进行第一次标记并且进行一次筛选,筛选的条件是对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过。虚拟机将这两种情况都视为"没有必要执行"。

   如果这个对象被判定为有必要执行 finalize()方法,那么这个对象将会被放置在一个名为 F-Queue的队列之中,并在稍后由一条虚拟机自动建立的,低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待他运行结束,这样做的原因是。如果一个对象在finalize()方法中执行缓慢,或者发生了死循环(更极端的情况)将会可能导致F-Queue 队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃,finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己-只要重新余引用链上的任何一个对象建立关联即可,譬如把自己 复制给摸个变量或者对象的成员变量,那在第二次标记时他将被移除“即将回收”的集合,如果对象这时候没有逃脱。那么他就是真的离死不远了。

   任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,他的finalize()方法不会被再次执行。finalize()能做的所有工作,使用try-finally 或者其他方式都可以做的更好、更及时。


回收方法区:

    很多人认为:方法区(或者HotSpot虚拟机中的永久带)是没有垃圾回收的。

    实际上永久带的垃圾收集主要回收两部分内容:废弃常量 和无用的类。

    关于废弃常量不用多说。

    但是要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才算是“无用的类”:

1、该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例

2、加载该类的classLoader 已经被回收

3、该类对应的Class 对象没有任何地方被引用。无法在任何地方通过反射访问该类的方法

虚拟机可以对上述3个条件的无用类进行回收。这里说的仅仅是“可以”。而不是和对象一样,不使用了就必然会回收,是否对类进行回收,HotSpot 虚拟机提供了-Xnoclassgc 参数进行控制,还可以使用 -verbose:class 及 -xx:+TraceClassLoading、-xx:+TraccClassLoading  可以在Product版本的虚拟机使用。但是 -XX:+TraceClassLoading参数需要fastdebug版的虚拟机支持

        在大量使用反射、动态代理、cglib等bytecode框架的场景、一级动态生成Jp和OSGi这类频繁定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。


垃圾收集算法:

1、标记-清除算法:

    首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。他的标记过程中 实际上即时上面说的finaLize()的过程。他的主要缺点一个是效率问题。另外一个是空间问题,标记清除后会产生大量不连续的内存碎片。

2、复制算法:

   这种算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了。就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。

现在的商业虚拟机都采用这是采用这种收集算法来回收新生代,IBM的专门研究表明,新生代中的对象98%都是朝生夕死的,所以不需要按照1:1 的比例来划分内存空间,而是姜内存氛围一块较大的Eden 空间和两块娇小的Survivor 空间,每次使用Eden 和其中的一块 Survicor。当回收时,姜Eden 和Survivor 中还存活这的对象一次性的拷贝到另外一块Survivor 空间上,最后清理掉Eden 和刚才用过的Survivor 的空间。HotSpot 虚拟机默认Eden 和Survivor 的小小比例是 8:1,也就是每次新生代中内存空间为整个新生代容量的90%(80%+10%),只有10%的内存空间会被浪费掉。

如果另外一块Survivor 空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。

标记-整理算法:

    复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会遍低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以对应被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记过程仍然与标记-清除算法一样,但是后续步骤不是直接将对可回收对象进行清理,而是让所有存活的对象都向领一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:

  当代商业虚拟机的垃圾收集都采用的是“分代收集算法” ,他根据对象的存活周期的不同,将内存化为几块,一般是把java堆分为新生代和老年代。这样就可以根据各个年代的特点采用最合适的收集算法。

新生代选用复制算法,老年代使用标记-清理算法 或者 标记-整理算法。


垃圾收集器

如果说收集算法是内存回收的方法论。垃圾收集器就是内存回收的具体实现。

sei rui 哦

Serial 收集器

曾经在 jdk 1.3 是虚拟机新生代收集的唯一选择,这个收集器是一个单线程的收集器、这个单线程的收集器不仅是说明它只会使用一个cpu 或一条收集线程去完成垃圾收集工作,更重要的是他进行垃圾收集时,必须暂停其他所有的工作线程。直到它收集结束。

ParNew 收集器

ParNew 收集器其实就是Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,和 Serial 收集器可用的所有控制参数、收集算法、对象分配规则、回收策略等都与它一样。两种收集器也共用了相当多的代码。


Parallel scavenge 收集器

他也是一个新生代收集器。使用复制算法。是并行的多线程收集器,这个收集器的特点是 :CMS 等收集器的关注点尽可能低缩短垃圾收集时用户线程的停顿时间,而Parallel  Scavenge 收集器的目标是达到一个可控制的吞吐量,所谓吞吐量是cpu用于运行用户代码的时间与cpu总消耗时间的比值,即吞吐量 = 运行用户代码时间/(用户代码时间+垃圾收集时间) 虚拟机总共运行了100分钟,其中垃圾回收花掉1分钟,那么吞吐量就是99%。

停顿时间越短就越适合需要与用户交互的程序,良好的相应速度能提升用户的体验。而高吞吐量则可以最高效的利用cpu时间,尽快的完成程序的运算任务,主要适合在后台运算而不太需要太多交互的任务、


Serial Old 收集器


是Serial 收集器的老年代版本,他同样是一个单线程收集器,使用 “标记-整理”算法

parallel Old 收集器

parallel 是  parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法、


CMS 收集器

cms 收集器 是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的java 应用程序都集中在互联网或B/s系统的服务端上,这类应用尤其重视服务的响应速度,希望系统时间最短,给用户带来良好的体验。cms 符合这类需求。

cms 收集器基于“标记-清除”算法实现的。运作过程分为4个过程:

1、初始标记

2、并发标记

3、重新标记

4、并发清除


优点:并发收集-低停顿  默认启动的回收线程数是 (cpu数量+3)/4 当cpu 在4个以上的时候,并发回收时垃圾回收线程最多占用不超过25%的cpu资源,但是当cpu 不足4个时,那么cms对用户程序的影响就可能变得很大。

在并发标记和并发清理的时候让GC线程、用户线程交替运行,尽量减少GC 线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些。

CMS无法处理浮动垃圾。由于CMS并发清理阶段用户线程还在运行着,版孙程序的运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程中之后,CMS 无法在本次收集中处理掉它们,只好留下,等待下一次清理掉。

收集结束时会产生大量的空间碎片。 因为他采用“标记-清除算法”

G1 收集器

基于“标记-清理”算法实现的收集器。不会产生空间碎片。

他非常精确的控制停顿,既能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾回收上的时间不得超过N毫秒。

G1将整个java 堆(包括新生代和老年代)划分为多个大小固定的独立区域,并且跟踪这些区域里面的垃圾堆积程度,在后台维护一个优先列表,每次根据允许的收集时间,有限回收垃圾最多的区域。





展开阅读全文
加载中

作者的其它热门文章

打赏
0
6 收藏
分享
打赏
0 评论
6 收藏
0
分享
返回顶部
顶部