读书笔记 ---- 《深入理解Java虚拟机》---- 第2篇:垃圾回收算法

上一篇:Java内存区域与内存溢出异常:https://blog.csdn.net/pcwl1206/article/details/83990008

第2篇:垃圾回收算法

一、判断对象是否存活的算法

1、引用计数法

2、可达性分析算法

3、再谈引用

4  生存还是死亡

5、回收方法区

二、垃圾回收算法

1、标记 — 清除算法

2、复制算法

3、标记—整理算法

4、分代收集算法

1  新生代

2  老年代

3  永久代

三  启动Java垃圾回收

四  什么是Stop the World

参考及推荐



首先明确GC需要完成的3件事情:

1、哪些内存需要回收?

2、什么时候回收?

3、如何回收?

Java虚拟机的运行时数据区中,程序计数器、虚拟机栈和本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来就已知的,因此这三个区域的内存分配和回收都具有确定性,在几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

Java堆方法区则不一样,一个接口中的多个实现类需要的内存有可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道会创建哪些对象,这部分内存的分配和回收都是动态的。垃圾回收关注的就是这部分内存。

一、判断对象是否存活的算法

常用的有:引用计数法可达性分析算法

1、引用计数法

算法思想:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。

引用计数法的实现简单,判断效率也很高,在大部分情况下都是一个不错的算法,但是引用计数法对于对象之间的相互循环引用的问题难以解决,因此Java虚拟机中并未采用这种方法。

2、可达性分析算法

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

如下图所示的object5、object6、object7到GC Roots都是不可达的,所以他们会被判定为是可回收的对象。 

在Java语言中,可作为GC  Roots的对象包括下面几种:

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

3、再谈引用

无论是通过引用计数法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判断对象是否存活都与“引用”有关。

我们希望能够描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存空间在进行垃圾收集后还非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的场景。

Java将引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。

  1. 强引用:就是指在程序代码之中普遍存在的,类似“Object  obj = new Object()”这类的引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象;
  2. 软引用:用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常;
  3. 弱引用:也是用来描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象;
  4. 虚引用:称为幽灵引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

4  生存还是死亡

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

如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个叫做F-Queue的对列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何一个对象建立关联即可,比如:把自己(this关键字)赋值给某个类变量或者对象的成员变量,那再第二次标记时它将被移除“即将回收”的集合;如果这个时候还没有逃脱,那基本上它就真的被回收了。

5、回收方法区

方法区在HotSpot虚拟机中被称为永久代,很多人认为该部分内存是没有垃圾回收的,Java虚拟机也没有对此做出规定,但是方法区中的废弃常量无用的类还是要回收的以保证永久代不会发生内存溢出。

在方法区中进行垃圾收集的“性价比”一般比较低,在堆中,尤其在新生代中,常规应用进行一次垃圾收集一般可以回收70%~95%的空间,而永久代的垃圾收集器效率远低于此。

废弃常量:例如:常量池中一个字符串“abc”,但是没有任何String对象引用它,也没有其他地方引用它;

无用的类:需要满足下面3个条件:

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

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

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


二、垃圾回收算法

1、标记 — 清除算法

最基础的收集算法是“标记—清除”(Mark-Sweep)算法,分为“标记”“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。标记的过程在上面【4生存还是死亡】中讲过了。

两个不足:

1、效率问题:标记和清除两个过程的效率都不高;

2、空间问题:标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

后面要讲的垃圾回收算法也都是基于标记—清除算法的思路并对其不足进行改进而得到的。

2、复制算法

为了解决效率问题,“复制”算法出现了,它将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。

缺点:这种算法的代价是将内存缩小为原来的一半,代价比较高。

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

当然98%的对象可回收只是一般场景下的依据,没有办法保证每次回收都不多于10%的对象存活,当Survivor空间不够用的时候,需要依赖老年代进行分配担保,即:如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

3、标记—整理算法

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

根据老年代的特点,提出了“标记—整理”算法,标记过程仍与“标记—清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

4、分代收集算法

根据内存中对象的存活周期不同,将内存划分为几块,Java的虚拟机中一般把堆内存划分为新生代和老年代,当创建对象时一般在新生代中分配内存空间,当新生代垃圾收集器回收几次之后依然存活的对象会被移动到老年代内存中,当大对象在新生代中无法找到足够的连续内存时,也直接在老年代中创建。

现在的Java虚拟机根据新生代和老年代的特点选择最合适的垃圾回收算法。

如上图所示的Java堆内存:需要说明的是永久代空间Perm在Java8中已经被移除了。

  1. 新生代(Young Generation)
    • Eden空间(Eden space,任何实例都通过Eden空间进入运行时内存区域)
    • S0 Survivor空间(S0 Survivor space,存在时间长的实例将会从Eden空间移动到S0 Survivor空间)
    • S1 Survivor空间 (存在时间更长的实例将会从S0 Survivor空间移动到S1 Survivor空间)
  2. 老年代(Old Generation)实例将从S1提升到Tenured(终身代)
  3. 永久代(Permanent Generation)包含类、方法等细节的元信息

1  新生代

新生代使用复制和标记-清除垃圾收集算法,研究表明,新生代中98%的对象是朝生夕死的短生命周期对象,所以不需要将新生代划分为容量大小相等的两部分内存,而是将新生代分为Eden区,Survivor from和Survivor to三部分,其占新生代内存容量默认比例分别为8:1:1,其中Survivor from和Survivor to总有一个区域是空白,只有Eden和其中一个Survivor总共90%的新生代容量用于为新创建的对象分配内存,只有10%的Survivor内存浪费,当新生代内存空间不足需要进行垃圾回收时,仍然存活的对象被复制到空白的Survivor内存区域中,Eden和非空白的Survivor进行标记-清理回收,两个Survivor区域是轮换的。

新生代中98%情况下空白Survivor都可以存放垃圾回收时仍然存活的对象,2%的极端情况下,如果空白Survivor空间无法存放下仍然存活的对象时,使用内存分配担保机制,直接将新生代依然存活的对象复制到年老代内存中,同时对于创建大对象时,如果新生代中无足够的连续内存时,也直接在年老代中分配内存空间。

Java虚拟机对新生代的垃圾回收称为Minor GC,次数比较频繁,每次回收时间也比较短。

使用java虚拟机-Xmn参数可以指定新生代内存大小。

2  老年代

老年代中的对象一般都是长生命周期对象,对象的存活率比较高,因此在老年代中使用标记-整理垃圾回收算法

Java虚拟机对老年代的垃圾回收称为MajorGC/Full  GC,次数相对较少,每次回收的时间比较长。

当新生代中无足够空间为对象创建分配内存,老年代中内存内存回收也无法回收到足够的内存空间,并且新生代和老年代空间无法再扩展时,堆就会产生OutOfMemoryError异常。

Java虚拟机用-Xms参数可以指定堆最小内存大小,-Xmx参数可以指定堆最大内存大小,这两个参数分别减去Xmn参数指定的新生代内存大小,可以计算出老年代最小和最大内存容量。

3  永久代

Java虚拟机内存中的方法区在Sun  HotSpot虚拟机中被称为永久代,是被各个线程共享的内存区域,它用于存储被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。永久代垃圾回收比较少,效率也比较低,但是永久代(方法区)中的废弃常量无用的类还是要回收的以保证永久代不会发生内存溢出。

永久代也使用标记—整理算法进行垃圾回收,Java虚拟机参数:-XX:PermSize和-XX:MaxPermSize可以设置永久代的初始大小和最大容量。


三  启动Java垃圾回收

作为一个自动的过程,程序员不需要在代码中显示地启动垃圾回收过程。System.gc()Runtime.gc()用来请求JVM启动垃圾回收。

虽然这个请求机制提供给程序员一个启动 GC 过程的机会,但是启动由 JVM负责。JVM可以拒绝这个请求,所以并不保证这些调用都将执行垃圾回收。启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用。JVM将这个选择留给了Java规范的实现,不同实现具体使用的算法不尽相同。

毋庸置疑,垃圾回收过程是不能被强制执行的。


四  什么是Stop the World

Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

GC时的Stop the World(STW)是大家最大的敌人。但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象。

JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation,比如分派GC,thread dump等,这些任务,都需要整个Heap,以及所有线程的状态是静止的,一致的才能进行。所以JVM引入了安全点(Safe Point)的概念,想办法在需要进行VM Operation时,通知所有的线程进入一个静止的安全点。

除了GC,其他触发安全点的VM Operation包括:

1. JIT相关,比如Code deoptimization, Flushing code cache ;

2. Class redefinition (e.g. javaagent,AOP代码植入的产生的instrumentation) ;

3. Biased lock revocation 取消偏向锁 ;

4. Various debug operation (e.g. thread dump or deadlock check);


上一篇:Java内存区域与内存溢出异常:https://blog.csdn.net/pcwl1206/article/details/83990008

参考及推荐:

1、JVM垃圾回收基本原理和算法:https://blog.csdn.net/a724888/article/details/77981592

2、垃圾回收算法:https://blog.csdn.net/chjttony/article/details/7883068

猜你喜欢

转载自blog.csdn.net/pcwl1206/article/details/84061589