深入理解Java虚拟机----第三章:垃圾收集器与内存分配策略

目录

第一章:走进Java
第二章:Java内存区域与内存溢出异常
第三章:垃圾收集器与内存分配策略
第四章:虚拟机性能监控与故障处理
第五章:调优案例分析与实战
第六章:类文件结构
第七章:虚拟机类加载机制
第八章:虚拟机字节码执行引
第九章:类加载及其执行子系统的案例与实战
第十章:早期(编译器)优化
第十一章:晚期(运行期)优化
第十二章:Java内存模型与线程
第十三章:线程安全与锁优化

第三章:垃圾收集器与内存分配策略

3.1概述

程序计数器、虚拟机栈、本地方法栈 3 个区域随线程生灭(因为是线程私有),栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作。而 Java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期才知道那些对象会创建,这部分内存的分配和回收都是动态的,垃圾回收期所关注的就是这部分内存。

3.2对象已死吗

3.2.1引用计数法

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

  • 实现简单、判定效率高
  • 无法解决循环引用的问题
  • 主流虚拟机没有采用

3.2.2可达性分析算法

通过一系列称作“GC Roots”的对象作为起始点,向下搜索,搜索走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,则证明不可用

可作为GC Roots的对象:

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

3.2.3再谈引用

  • 强引用:类似于 Object obj = new Object(); 创建的,只要强引用在就不回收。
  • 软引用:SoftReference 类实现软引用。在系统要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。
  • 弱引用:WeakReference 类实现弱引用。对象只能生存到下一次垃圾收集之前。在垃圾收集器工作时,无论内存是否足够都会回收掉只被弱引用关联的对象。
  • 虚引用:PhantomReference 类实现虚引用。无法通过虚引用获取一个对象的实例,唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

3.2.4生存还是死亡

  • 不可达对象要经历两次标记才宣告死亡
  • 发现没有与 GC Roots 相连接的引用链,第一次标记筛选判断是否需要执行finalize()方法
  • 当对象没有覆盖finalize()或者已经被虚拟机调用,则不需要执行
  • 有必要执行,则放到F-Queue的队列中,稍后由虚拟机自建的、低级别的线程Finalizer去执行———触发操作,不保证运行结束
  • finalize()方法是对象逃脱死亡的最后一次机会
  • 如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可

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

如果这个对象被判定为有必要执行 finalize() 方法,那么这个对象竟会放置在一个叫做 F-Queue 的队列中,并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。这里所谓的“执行”是指虚拟机会出发这个方法,并不承诺或等待他运行结束。finalize() 方法是对象逃脱死亡命运的最后一次机会,稍后 GC 将对 F-Queue 中的对象进行第二次小规模的标记,如果对象要在 finalize() 中成功拯救自己 —— 只要重新与引用链上的任何一个对象简历关联即可。

finalize() 方法只会被系统自动调用一次。

3.2.5回收方法区

回收废弃常量:判断没有该常量的引用。

回收无用的类:要以下三个条件都满足

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

3.3 垃圾收集算法

3.3.1标记-清除算法

算法分为标记和清除两个阶段,首先标出需要回收的对象,标记完成后统一回收

不足:效率,标记和清除效率都不高;空间,产生内存碎片

3.3.2复制算法

将内存按容量分为大小相等的两块,每次只使用一块

当一块用完了,将活着的对象复制到另一块上,然后把已使用过的内存清理掉

不足:内存缩小一半,造成内存浪费

解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代。

3.3.3标记-整理算法

适合老年代

过程与 ”标记-清除“ 算法一样,不同的是不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存

3.3.4分代收集算法

根据存活对象划分几块内存区,一般是分为新生代和老年代。然后根据各个年代的特点制定相应的回收算法

新生代:每次垃圾回收都有大量对象死去,只有少量存活,选用复制算法比较合理。

老年代:老年代中对象存活率较高、没有额外的空间分配对它进行担保。所以必须使用 标记 —— 清除或者 标记 —— 整理 算法回收。

3.4HotSpot的算法实现

3.5垃圾收集器

HopSpot虚拟机的垃圾收集器

3.5.1 Serial 收集器

  • 最基本,发展最悠久的
  • 单线程收集器,使用一个 CPU 或一条收集线程去完成收集工作
  • 暂停其它所有的工作线程直到收集结束
  • 应用:最高的单线程收集效率,Client模式下的默认收集器
  • 新生代,复制算法

3.5.2 ParNew 收集器

  • Serial 收集器的多线程版本
  • 多条线程进行垃圾收集
  • 并行Parallel:指多条垃圾收集线程并行工作,此时用户线程处于等待状态
  • 并发:Concurrent:指用户线程和垃圾回收线程同时执行(不一定是并行,有可能是交叉执行),用户进程在运行,而垃圾回收线程在另一个 CPU 上运行。
  • Server模式下的默认收集器
  • 只有Serial、ParNew能和CMS能够配合使用
  • 新生代,复制算法

3.5.3Parallel Scavenge收集器

  • 新生代收集器,采用复制算法

  • 并行的多线程收集器

  • 目的达到一个可控制的吞吐量

  • 新生代,复制算法

3.5.4 Serial Old收集器

  • 单线程
  • 使用标记—-整理算法
  • 主要目的在给client模式下的虚拟机使用
  • server模式下用途
    • jdk1.5及其以前的与Parallel Scavenge配合使用
    • 作为CMS的后备预案
  • 老年代,标记–整理算法

3.5.5 Parallel Old收集器

  • Parallel Scanvenge的老年代版本
  • 使用多线程和标记—-整理
  • JDK1.6中开始使用
  • “吞吐量优先”收集器,在注重吞吐量以及CPU资源敏感的场合,优先考虑Parallel Scavenge、Parallel Old收集器组合

3.5.6 CMS(Concurrent Mark Sweep)收集器

  • 以获取最短回收停顿时间为目标的收集器
  • 基于标记–清除算法

四个步骤

  • 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
  • 并发标记(CMS concurrent mark):进行 GC Roots Tracing(追踪)
  • 重新标记(CMS remark):修正并发标记期间的变动部分
  • 并发清除(CMS concurrent sweep)

初始标记、重新标记扔“Stop The World”,但是耗时短;

并发标记、并发清理耗时长,但收集器线程可以与用户线程一起工作;

所以,总的来说CMS收集器的内存回收过程是与用户线程一起并发的。

优点:并发收集、低停顿

缺点:对CPU资源敏感;无法处理浮动垃圾;空间碎片

3.5.7G1收集器

面向服务端应用的垃圾收集器

特点

  • 并行与并发:多CPU、多核环境,充分缩短停顿时间
  • 分代收集
  • 空间整合:整体 ”标记–整理“算法,局部(两个Region之间)”复制算法“。
  • 可预测的停顿:建立可预测的停顿时间模型。

其他收集器进行收集的范围都是整个新生代或者老年代,而G1把JAVA堆分为多个Region,根据价值大小(回收获得的空间和所需要时间的经验值)回收

如不计算维护 Remembered Set的操作,G1 的步骤为:

  • 初始标记:标记一下 GC Root能直接关联到的对象,并修改TAMS的值,停顿
  • 并发标记:从GC Root开始对堆中对象进行可达性分析,并发
  • 最终标记:修正并发期间导致标记产生变动的那部分标记记录,停顿但并行
  • 筛选回收:对各个Region回收价值和成本排序,根据期望的GC停顿时间回收

3.5.8 GC日志

3.6内存回收和分配策略

两个问题:给对象分配内存,回收分配给对象的内存

3.6.1对象优先在Eden区分配

大多数情况下,对象在新生代Eden区中分配。当没有足够的Eden时,虚拟机进行一次Minor GC

打印内存回收日志:-XX:PrintGCDetails

  • 新生代 GC (Minor GC):发生在新生代的垃圾回收动作,频繁,速度快。
  • 老年代 GC (Major GC / Full GC):发生在老年代的垃圾回收动作,出现了 Major GC 经常会伴随至少一次 Minor GC(非绝对)。Major GC 的速度一般会比 Minor GC 慢十倍以上。

3.6.2大对象直接进入老年代

大对象:大量连续内存空间的Java对象,如很长的字符串以及数组。经常出现大对象容易导致还有不少空间时就提前触发垃圾收集以获取足够的连续空间

通过设置参数-XX:PretenureSizeThreshold,大于该参数的垃圾收集的时候直接进入老年代,避免在Eden区和两个Survivor进行大量的内存复制

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

虚拟机为每个对象定义了年龄计数器,如果对象在Eden区出生,每熬过一次Minor GC,年龄 + 1。

年龄阈值-XX:MaxTenuringThreshold

3.6.4动态对象年龄判断

为了适应不同内存状况,虚拟机不是必须要求对象年龄达到年龄阈值才晋升老年代。在Survivor区相同年龄大小的所有对象大小的总和大于Survivor的一半,则年龄大于或等于该年龄的对象直接进入老年代

3.6.5空间分配担保

发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果成立则Minor GC是安全的。不成立则虚拟机会查看 HandlePromotionFailure设置值是否允许担保失败,如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,就尝试进行一次 冒险的Minor GC;如果小于或HandlePromotionFailure

设置不允许冒险,则改为 Full GC。

当出现大量对象在Minor GC后仍然存活,就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_36969257/article/details/81490135