垃圾收集器与内存分配策略
Peng's Blog 只记录和技术相关的东西

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

2017-08-20
JVM
 

概述

第一门垃圾收集(Garbage Collection)语言是1960年的Lisp。

什么内存需要回收呢?Java运行时内存区域里面的:程序计数器、虚拟机栈、本地方栈这三个区域会随着线程而生随着线程而灭,线程结束之后,内存就自然跟着回收了,所以这三块区域不需要考虑。

但是,Java堆(Heap)和方法区(Non-Heap)就不一样了,其中垃圾回收主要是针对堆,不过方法区也有涉及。

判断对象是否存活

引用计数法

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

实现简单,但有很严重的问题。它很难解决对象之间的互相循环引用。所以,现在的主流Java虚拟机里面是没有选用引用计数算法来管理内存的。

可达性分析算法

屏幕快照 2017-08-20 下午5.34.42

将 “GC Roots” 的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为“引用链”。如果,GC Roots到这个对象不可达,那么就证明此对象是不可用的,可以被JVM回收掉。

在Java语言中,可作为GC Roots的对象的主要包括:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中的JNI(即Native方法)引用的对象;

引用

判断对象是否存活都与”引用“有关。

在jdk1.2之后,引用分为了四类:

  • 强引用,类似 Object obj = new Object(),只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;
  • 软引用,描述一些还有用但不是必须的对象。当系统快发生OutOfMemoryError的时候,会把这些对象列进回收范围之中进行第二次回收;
  • 弱引用,也是用来描述非必须对象的,强度比软引用更弱。被软引用关联的对象只能生存到下一次垃圾收集发生之前;
  • 虚引用,是最弱的一种引用。无法通过虚引用来获取一个对象实例。这东西存在的唯一目的就是:能在这个对象被回收器回收时收到一个系统通知.. 666

生存还是死亡

大概就是说,不是”非死不可“,当真正宣告一个对象”死亡“,至少要经过两次标记。你可以在finalize()之前调用isActive()来抢救它。但只能被抢救一次,第二次无效。

作者说这东西不要用。用try-finally好多了。

回收方法区

很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范也说过不要求在里面使用垃圾收集。因为性价比比较低,在堆中,尤其是新生代,一次垃圾回收一般能回收70%~95%的空间,而方法区的垃圾回收效率远远低于这个值;

永久代的垃圾回收主要收两个东西:废弃常量和无用的类。

判断废弃常量很简单,就看当前系统有没有任何一个xx常量叫那个名字的,没有那就说明废弃了。

判断对象是一个”无用的类“比较麻烦,要满足下面三个条件:

  • 该类所有的实例都已经被回收,也就是说Java堆中不存在该类的任何实例;
  • 加载该类的 ClassLoader 已经被回收;
  • 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;

垃圾回收算法

这个是重点!!!

标记-清除算法

Mark-Sweep算法。算法分为”标记“和”清除“两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。至于哪些要被标记.. 那看前面的介绍吧。

有两个不足:

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

2、空间问题:标记清除之后会产生大量的不连续内存碎片

屏幕快照 2017-08-20 下午5.57.44

复制算法

复制算法只要是为了解决效率问题,它把内存划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块,然后把已经使用过的内存空间一次性地清理掉。

屏幕快照 2017-08-20 下午5.57.54

这个算法主要用于新生代。因为新生代都是些存活时间比较短的线程,一下子能回收90%的样子。

标记-整理算法

复制算法会浪费50%的空间,所以这个是在标记-清除和复制的基础上优化的。标记过程和“标记-清除”算法一样,但后续步骤不再是直接清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

屏幕快照 2017-08-20 下午5.58.03

这个一般用在老年代,也就是那群存活时间较长的内存里。

分代收集算法

新生代:主要存放新生的对象,生存周期较弱

老年代:主要存放应用程序中生命周期较长的内存对象

在新生代中,每次垃圾收集都能发现大量对象死去,只有少量存活。所以选择 复制 算法

在老年代中,对象存活率高,没有额外空间对它进行分配担保,所以必须使用 标记-清理 或者 标记 - 整理 算法。

HotSpot的算法实现

枚举根节点

就是找 GC Roots。

这节提到了一点,因为垃圾回收这项工作必须要在一个能确保一致性的快照中进行,所以GC必须停顿所有的Java执行线程。怎么理解呢?就比如.. 你妈帮你打扫卫生,这时候你基本上啥都做不了,不可能她边打扫你边扔对吧?

安全点

OopMap这个要看一下,是一种很厉害的数据结构,用来记录那些地方存在着对象引用。

所谓的安全点就是:程序执行时并非在所有地方都能停顿下来开始GC,只有到达安全点时才能暂停让GC程序开始。

Safepoint。

它还需要考虑一个问题,就是如何在GC发生时让所有线程都“跑”到最近的安全点上再停顿下来。有两种策略:

  • 抢先时中断:不需要线程的执行代码追东配合,在GC发生时,把所有线程全部中断,如果发现某个线程不在安全点上,那么恢复线程,让它”跑“到安全点上。。嗯,现在几乎没有用这种方法的。
  • 主动式中断:设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。

安全区域

当线程处于sleep状态或者Blocked状态,这时候线程就无法响应JVM的中断请求了,没办法”走“到安全的地方去中断挂起。这种情况的话,不可能让CPU分给他们时间去走。嗯,所以需要安全区域来解决。

安全区域是指在一断代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。

其实Safe Region就是被扩展了的Safepoint,一个是点,一个是段。

垃圾收集器

嗯,上面全是说如何去发起内存回收的问题,但回收是如何进行的呢,这就需要垃圾回收器来做了。

新生代一般用的是“复制”算法,老年代用的是”标记-清除“/”标记-整理“算法。

屏幕快照 2017-08-20 下午6.24.37

上面表示用于新生代,下面用于老年代。

连线代表着可以两个搭配使用。

Serial收集器

最老的一种,单线程,工作的时候暂停其它所有线程,直到它收集垃圾结束。

ParNew收集器:

主要用于新生代的收集,比较强大了,但不能配合 JDK1.5 的CMS老年代收集器。所以,嗯,地位比较尴尬。

Parallel Scavenge 收集器

也是新生代收集器,并行的多线程收集器。主要是吞吐量优先。跟ParNew的一大区别在于它能自适应调整各种JVM参数。

Serial Old收集器

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

Parallel Old收集器

JDK1.6才开始提供,这样的话,Parallel Scavenge才不尴尬。之前,新生代如果选择了Parallel Scavenge收集器,那么老年代除了Serial Old收集器之外没有别的选择了。但Serial Old比较慢。有了这东西,Parallel Scavenge才发挥出效果了。

CMS收集器

Concurrent Mark Sweep 并发标记清除。以获取最短回收暂停时间为目标的收集器。主要实现的话就是将垃圾回收的过程细分成四个小部分,把两个时间较长的换成跟用户线程并发执行,核心且时间短的还是要暂停所有线程。但整体上看还是并发的。

缺点:用的标记-清除算法,所以可能会产生大量的空间碎片。

G1收集器

Garbage-First

这东西搞了差不多10年,在Java7的时候才投入使用。

主要的特点:

并行与并发、分代收集(保留了分代的概念,不过其实它可以去掉这个概念了)、空间整理(与”标记-清除“算法不同,它从整体上看是”标记-整理“算法,从局部上看是”复制“算法。这意味着它没有空间碎片的产生)、可预测的停顿。能够避免全堆扫描(用Remembered Set实现)。

仔细将一下它的内存划分,首先里面堆的内存布局不再划分成新生代和老年代。它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

为什么叫G1?

因为G1跟踪各个Region里面的垃圾堆积的价值的大小(回收所获得的空间大小以及回收所需的时间等)。在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,这就是为什么叫Garbage-First。


内存分配和回收策略

主要是指在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能直接分配在老年代(比如特别大的那种)。

分配的规则不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾回收器组合,还有JVM中和内存相关的参数的设置。

对象优先在Eden分配

当Eden区没有足够的空间进行分配时,JVM将发起一次Minor GC。

大对象直接进入老年代

最典型的大对象就是那种很长的字符串以及数组。

长期存活的对象将进入老年代

JVM给每个对象定义了一个对象年龄(Age)计数器。熬过一次Minor GC,年龄加1。默认是到15就晋升到老年代。当然也可以通过参数设置。


Comments

评论功能暂停使用,如需跟作者讨论请联系底部的GitHub