JVM学习笔记

Mar 1, 2016


我就想看看我能写多久

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

对象已死吗

引用计数算法

该算法给对象添加一个引用计数器,每当一个地方引用它时,计数器加1,当计数器为0的时候就被判断为不可能再被使用的。

但是JVM没有使用引用计数算法,最主要的原因是它存在对象间相互引用问题。

比如说A.x = B, B.x = A,那么当他们两个都指向null时,引用计数还是不为0,无法启动GC。

可达性分析算法

主流语言都是通过可达性分析 (Reachability Analysis)来判断对象是否存活。 这个算法通过一系列被称为GC Roots的对象作为起点,向下搜索,走过的路成为引用链 (Reference Chain)。当一个对象到GC Roots没有任何引用的时候不可达。 就是判断超级源点的图的连通性。

Java中,可作为GC Roots的对象包括:

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

再谈引用

Java把引用分为强引用、软引用、弱引用、虚引用,强度依次减弱。

  • 强引用就是代码之中普遍存在的,类似Object o = new Object()这样的引用。只要强引用还存在,GC就不会回收被引用的对象。
  • 软引用用来描述一些还有用但并非必须的对象,在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收。
  • 被弱引用关联的对象只能活到下一次 GC 之前。当 GC 开始时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
  • 虚引用只是用来在这个对象被回收时收到一个系统通知。

生存还是死亡

一个对象真正死亡,至少要经历两次标记。 如果对象不可达,会被第一次标记并进行筛选,条件是该对象有没有必要执行finalize方法。 如果该方法没被重写或者已经被调用过,对象就会被放入「即将回收」的集合里,否则放入 F-Queue 队列中等待执行finalize方法,如果对象能在该方法中成功与一个在 GC Roots上的对象建立起引用,就能逃脱被回收的命运。

finalize代价高昂,我们应该避免使用。 这里有点问题啊,如果没重写finalize,就只进行一次标记了,作者怎么说至少两次。

回收方法区

JVM 规范中说过可以不要求虚拟机在方法区实现垃圾收集。

永久代的垃圾收集主要回收废弃常量无用的类。 要判断一个类是「无用的类」,需要同时满足以下三个条件:

  • 该类所有的实例都已经被回收。
  • 加载该类的ClassLoader已经被回收。
  • 该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类方法。

虚拟机可以对满足上面条件的无用类进行回收。

垃圾收集算法

标记 - 清除算法

它先标记出所有需要回收的对象,标记完后统一回收。 不足:

  • 效率不高。
  • 标记清除之后会产生大量不连续的碎片,不好分配较大的对象。

复制算法

把内存分成大小相等的两块,每次只使用其中一块。当这一块的用完了,把还存活着的对象移动到另一块上,清理这一块的空间。 这种算法的代价是只能使用原来一半的内存。

现在的商业虚拟机都是采用这种收集算法来回收新生代。据研究表明,新生代中98%的对象是「朝生夕死」的,所以不需要按照1:1的比例划分内存。 所以将内存划分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。回收时,把那两块活着的对象复制到剩下那一块。 HotSpot 虚拟机默认的Eden和Survivor的比例是8:1,也就是每次新生代可用内存有10%被浪费。 但是我们不能担保每次只有不多于10%的对象存活,所以当空间不够用时,那些存活的对象通过分配担保(Handle Promotion)进入老年代。

标记 - 整理算法

因为复制算法复制操作多的话效率不加,所以老年代一般不用。 用的是标记 - 整理算法。 标记过程和标记 - 清除一样,然后把所有存活的对象向一端移动,直接清理掉端边界以外的内存。

分代收集算法

这个应该不是一种具体的算法,只是根据对象的不同存活周期将内存分为不同的几块,然后看情况选用具体的算法。

HotSpot 的算法实现

枚举根结点

在执行可达性分析的时候,我们要「Stop The World」,也就是把全部的线程都阻塞,这样才能准则他分析出对象的引用关系。 虚拟机通过OopMap的数据结构来直接得知引用的位置。

安全点

当开始GC时,线程都跑到最近的安全点(Safepoint)上停下来。 这里分为抢先式中断和主动式中断。

抢先式中断是在 GC 发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上。 现在已经没有 JVM 采用这种方法了。 主动式中断不直接对线程操作,在安全点上设置一个标志,当线程发现标志为真的时候自己中断。

安全区域

当有些线程被阻塞的时候,便不能响应 JVM 的中断请求。 安全区域是指在一段代码片段之中,引用关系不会发生变化。 当线程进入Safe Region时,标识自己进入了,然后发起 GC 时就不用管标识为Safe Region的线程了。线程要离开安全区域时,首先检查是否完成 GC,除非完成才能继续执行。

垃圾收集器

Serial 收集器

Serial收集器是最基本的收集器,是一个单线程收集器。它在收集时,必须暂停其他所有工作线程,直到收集结束。 优点:简单高效。 它没有线程交互的开销,获得最高的单线程收集效率。

ParNew 收集器

ParNew收集器就是Serial收集器的多线程版本。它是在 Server 模式下运行的虚拟机中首选的新生代收集器。因为除了Serial收集器之外,只有它能和CMS收集器配合工作。

Parallel Scavenge 收集器

Parallel Scavenge收集器是一个新生代收集器,使用复制算法、并行多线程。 他和ParNew收集器不一样的地方是他的关注点和其他收集器不同。

CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge 收集器的目标则是达到一个可控的吞吐量。 吞吐量就是运行用户代码的时间和 CPU 总消耗时间的比值。

他还能自适应调节。

Serial Old 收集器

老年代的收集器,用来搭配 Prallel Scavenge收集器。

CMS 收集器

CMS (Concurrent Mark Sweep) 收集器是一种以获取最短回收停顿时间为目标的收集器,是基于「标记 - 清除」算法实现的,整个过程分为四个步骤。

  • 初始标记 (CMS initial mark)
  • 并发标记 (CMS concurrent mark)
  • 重新标记 (CMS remark)
  • 并发清除 (CMS concurrent sweep)

其中初始标记和重新标记还是要「Stop The World」的。

初始标记是找出 GC Roots,并发标记是顺着 GC Roots 标记的过程。 重新标记是为了找出因为并发标记期间用户线程继续运行导致标记产生变动的标记记录。

它有以下的缺点:

  • CMS 收集器对 CPU 资源非常敏感。 它默认启动的回收线程数是(CPU数量 + 3)/4。当 CPU 数量不足2个时,要分出一半的运算能力去收集线程。
  • CMS 收集器无法处理浮动垃圾 (Floating Garbage),可能出现Concurrent Mode Failure失败而导致另一次Full GC的产生。 由于 CMS 并发清理阶段用户线程还在运行,这时产生的垃圾将无法清理,也因此需要预留一点空间给用户线程使用。在JDK 1.6中,CMS 收集器启动的阈值已经提升至 92%。 要是CMS 收集器运行期间预留的内存无法满足需要,就会提示上面那个错误,这时虚拟机将临时启用Serial Old收集器进行老年代的垃圾收集,这样停顿时间就很长了。
  • CMS是基于标记 - 清除算法实现的,会产生空间碎片。如果过多时,会给大对象分配造成麻烦,提前触发 Full GC。

G1 收集器

G1 (Garbage First)收集器是最新的收集器。 新生代和老年代就不是隔离的了,是一部分 Region 集合。 G1能建立可预测的停顿,因为它能有计划地避免在整个 Java 堆中进行全区域的垃圾收集,跟踪各个区域里的垃圾堆积的价值大小,后台维护一个优先列表。 不过目前还是没有大范围使用。

内存分配和回收策略

对象优先在 Eden 分配

大多数情况下,对象在Eden分配,如果空间满了将触发一次Minor GC

大对象直接进入老年代

虚拟机提供