正文 垃圾收集器关注的是 Java 堆和方法区,因为这部分内存的分配和回收是动态的。只有在程序处于运行期间时才能知道会创建哪些对象,也才能知道需要多少内存。 虚拟机栈和本地方法栈则不需要过多考虑回收的问题,因为栈中每一个栈帧分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收具有确定性。 一、 ..

《深入理解 Java 虚拟机》读书笔记:垃圾收集器与内存分配策略

正文

垃圾收集器关注的是 Java 堆和方法区,因为这部分内存的分配和回收是动态的。只有在程序处于运行期间时才能知道会创建哪些对象,也才能知道需要多少内存。

虚拟机栈和本地方法栈则不需要过多考虑回收的问题,因为栈中每一个栈帧分配多少内存基本上是在类结构确定下来时就已知的,因此这几个区域的内存分配和回收具有确定性。

一、对象已死吗

垃圾收集器在对堆进行回收前,第一件事就是要确定堆中对象哪些还“存活”着,哪些已“死去”(即不可能再被任何途径使用的对象)。

1、 引用计数算法

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

优点:实现简单,判定效率高。

缺点:很难解决对象之间相互循环引用的问题。

2、可达性分析算法

通过一系列被称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连时,则此对象不可用。

Java 语言中,可作为 GC Roots 的对象:

可达性分析算法中不可达的对象,至少要经历两次标记过程,才会被回收。

  1. 发现没有与 GC Roots 相连的引用链时,进行第一次标记。
  2. 当对象覆盖了 finalize() 方法,并且没有被调用过时,将会被放入一个叫做 F-Queue 的队列中,稍后 GC 将对 F-Queue 中的对象进行第二次标记。如果在 finalize() 方法中,对象没有重新与引用链上的一个对象建立关联,那么将会被回收。

3、四种引用

无论是引用计数算法,还是可达性分析算法,判断对象是否存活都与“引用”有关。Java 中有 4 种引用,按强度由强至弱依次为:强引用、软引用、弱引用、虚引用。

4、回收方法区

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

如何判定废弃常量:

如何判定无用的类:

二、垃圾收集算法

1、标记-清除算法

分为“标记”和“清除”两个阶段。首先标记出所有需要回收的对象,然后再统一回收所有被标记的对象。

该算法会产生大量不连续的内存碎片,因而在分配较大对象时,可能会由于无法找到足够的连续内存而不得不提前触发一次 GC。

2、复制算法

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

该算法的代价是始终会有一块内存被“浪费”掉。

由于新生代的对象 98% 是“朝生夕死”,因此并不需要按 1:1 的比例来划分内存空间。现在的商业虚拟机,是将内存划分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象复制到另一块 Survivor 上,最后清理掉 Eden 和使用过的 Survivor。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1。

分配担保机制:
当另一块 Survivor 没有足够空间来存放存活对象时,则需要其他内存(老年代)进行分配担保,将对象移入其他内存(老年代)。

3、标记-整理算法

首先标记出所有需要回收的对象,然后将所有存活对象向一端移动,最后直接清理掉端边界以外的内存。

4、分代收集算法

根据对象存活周期的不同,将 Java 堆划分为新生代和老年代,然后根据各个年代的特点采用最适当的收集算法。

三、HotSpot 的算法实现

1、枚举根节点

可达性分析时,需要枚举 GC Roots 节点,以便标记出所有的不可用对象。

可作为 GC Roots 的节点主要在全局引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中。如果逐个检查里面的引用,会消耗很多时间。因此,目前主流的 Java 虚拟机使用准确式 GC 来完成 GC Roots 枚举。

Stop The World(STW):
可达性分析期间,不可以出现对象引用关系还在不断变化的情况。因此 GC 时,必须停顿所有 Java 执行线程,此时整个执行系统看起来就像被冻结某个时间点上。

准确式 GC:
虚拟机可以直接得知哪些地方存放着对象引用,因此 STW 时,不需要一个不漏地检查所有执行上下文和全局的引用位置。

HotSpot 中准确式 GC 的实现:
HotSpot 使用一组称为 OopMap 的数据结构来记录对象的引用位置。这样,GC 在扫描时就可以直接得知对象的引用位置信息。

类加载完成时,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来记录到 OopMap 中。JIT 编译过程中,也会在 OopMap 中记录下栈和寄存器中哪些位置是引用。

2、安全点

HotSpot 只在特定的位置上记录了 OopMap,这些位置称为安全点。

程序执行时,只有到达安全点才能停顿下来进行 GC。因为只有到达安全点,才能访问到 OopMap 记录。

如何在 GC 时让线程跑到最近的安全点再停顿下来:

3、安全区域

安全区域是指一段代码片段中,引用关系不会发生变化。在这个区域中的任意地方开始 GC 都是安全的。可以把安全区域看做是被扩展了的安全点。

为什么需要安全区域:
当线程没有分配 CPU 时间时,将无法响应 JVM 的中断请求,跑到安全点中断挂起,JVM 也不太可能等待线程重新被分配 CPU 时间。这种情况就需要安全区域来解决。

安全区域的使用:

  1. 线程执行到安全区域的代码时,标识自己进入了安全区域。
  2. JVM 发起 GC 时,不用管进入安全区域的线程。
  3. 线程要离开安全区域时,必须检查系统是否完成了根节点枚举(或整个 GC 过程)。如果完成了,线程就继续执行,否则必须等待,直到收到可以离开安全区域的信号。

四、垃圾收集器

1、Serial 收集器

2、ParNew 收集器

3、Parallel Scavenge 收集器

自适应调节策略:
虚拟机根据当前系统的运行情况收集性能监控信息,动态调整虚拟机参数以提供最合适的停顿时间或最大的吞吐量。

4、Serial Old 收集器

5、Parallel Old 收集器

6、CMS 收集器

CMS 运作过程:

  1. 初始标记:标记 GC Roots 能直接关联到的对象,需要 STW。
  2. 并发标记:进行 GC Roots Tracing 的过程,即可达性分析。
  3. 重新标记:修正并发标记期间引用关系发生变化的那一部分对象的标记记录,需要 STW。
  4. 并发清除:清除垃圾对象。

CMS 的缺点:

7、G1 收集器

G1 特点:

Region:
G1 将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的,而是一部分 Region(不需要连续)的集合。

可预测的时间停顿模型:
G1 之所以能建立可预测的时间停顿模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。

G1 跟踪各个 Region 的垃圾堆积的价值大小(回收所获得的空间大小及所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(Garbage-First 名称的由来)。

G1 运作过程:

  1. 初始标记:标记 GC Roots 能直接关联的对象,并修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象。需要 STW。
  2. 并发标记:进行可达性分析。
  3. 最终标记:修正并发标记期间引用关系发生变化的那一部分对象的标记记录。需要 STW。
  4. 筛选回收:对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间制定回收计划。

五、内存分配与回收策略

1、对象优先在 Eden 分配

2、大对象直接进入老年代

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

4、动态对象年龄判定

5、空间分配担保

  • 阅读
    33 引用 • 133 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    102 引用 • 110 回帖
回帖
请输入回帖内容...