JVM学习笔记

我就想看看我能写多久

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

对象已死吗

引用计数算法

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

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

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

可达性分析算法

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

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

再谈引用

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

生存还是死亡

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

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

回收方法区

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

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

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

垃圾收集算法

标记 - 清除算法

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

复制算法

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

现在的商业虚拟机都是采用这种收集算法来回收新生代。据研究表明,新生代中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) 收集器是一种以获取最短回收停顿时间为目标的收集器,是基于「标记 - 清除」算法实现的,整个过程分为四个步骤。

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

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

它有以下的缺点:

G1 收集器

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

内存分配和回收策略

对象优先在 Eden 分配

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

大对象直接进入老年代

虚拟机提供

Powered by Jekyll and Theme by solid