0.学习准备

  1. 参考资料
    参考书籍《深入理解Java虚拟机》
    参考视频《深入理解JVM》(目前学习)
  2. 简单目录:
    • GC简介
    • GC算法
      • 引用计数法
      • 标记清除算法
      • 标记压缩算法(标记整理)
      • 复制算法(新生代整合)
      • 分代思想
    • 可触及性(可达性)
    • Stop-The-World
    • 串行收集器
    • 并行收集器
    • CMS收集器
    • 参数总结
  3. 关于G1收集器的内容等过段时间看书深入再去学习。
    视频中没有讲解关于G1收集器的内容。

1.GC的概念

  1. GC:Garbage Collection,垃圾收集
    C语言是程序员自己申请和释放(管理)空间,Java中则是虚拟机来实现
    (垃圾是指程序运行过程中产生的一些无用的对象,这些对象仍然占据了一定的内存空间,无用的对象需要及时回收以确保程序有足够的空间)
  2. Java中,GC的对象是堆空间和永久区(受GC管理)。
    JVM中有单独一个线程根据特定的算法来监控,扫描,释放无用的对象。
    最主要的目的是为了防止程序员的操作而导致的内存泄露。
  3. GC的思想最早出现在1960年时的List语言。
  4. GC如何去管理内存空间–GC算法

2.引用计数法

1)引用计数法简介:

  1. 老牌的垃圾回收算法:
    给对象添加一个计数器,非0可用,0不可用。(差不多就是这种原理)
  2. 通过引用计算来进行垃圾回收。
    实现简单,判定效率也高,大部分情况下是一种不错的算法。
    (不过在JVM中不常用)
  3. 在以下技术中都有应用案例:
    • 微软的COM
    • ActionScript3的FlashPlayer
    • Python

2)引用计数法的执行过程:

  1. 对于一个对象A,只要有任何一个对象去引用了A,则A的引用计数器就会加1,引用失效时A的引用数器就会减1,只要一个对象的程序计数器为0则该对象就不可用了(死了)。
  2. 引用计数法的简单回收过程:(虚线代表引用失效)

3)引用计数法的问题:

  1. 引用计数法的问题:
    • 引用和区引用都伴随着加减,影响性能
    • 很难处理循环引用
  2. 循环引用的简单示意图:
  3. 使用引用计数法处理循环引用的过程:

    三个循环引用的对象对根对象来说不可达(应该被回收),但是因为每个对象的引用计数器都是1,所以都不会被回收。

3.标记-清除算法与标记-压缩算法

1)标记清除算法:

  1. 标记清除算法是现代垃圾回收算法的思想基础。
    分为两阶段:标记阶段和清除阶段。
  2. 一种可行的实现:
    • 标记阶段:从根节点开始标记所以从根节点开始的可达对象。未被标记的对象则视为垃圾对象。
    • 清除阶段:清除所有的未被标记的对象。
  3. 标记清除算法的示意图:
  4. 标记-清除算法的不足之处:
    • 效率问题:标记和清除过程的效率都不高
    • 空间问题:清除之后产生大量不连续的内存碎片

2)标记压缩算法(标记整理算法):

  1. 标记压缩算法适用于存活对象较多的场合,如老年代。
    是对标记清除算法的优化。
  2. 简单分为三个阶段:
    • 标记阶段:和标记清除一样,从根节点标记所有可达对象
    • 压缩阶段:将所有存活(标记)对象压缩(移动)到内存的一端
    • 清除阶段:清理边界外的所有空间(对象)
  3. 标记-压缩算法的示意图:

4.复制算法及整合使用

1)复制算法简介:

  1. 与标记-清除算法相比,复制算法是一个相对高效的回收算法
  2. 不适用于存活对象多的场合(老年代)
  3. 基本思想:
    将原有的内存空间分为两块,每次只使用其中一块。
    垃圾回收时将正在使用的内存块的存活对象复制到另一块未使用的内存块中。
    清空当前正在使用的内存块中的所有对象,并交换两个内存块的角色,完成垃圾回收。
  4. 复制算法的示意图:
  5. 复制算法最大的问题:浪费空间

2)复制算法整合标记清理思想:

  1. 示意图:
  2. 说明:
    • 复制算法适用于年轻代(新生代)
    • 大对象直接进入老年代(老年代为年轻代做担保)
    • 多次GC未被清除的对象自动进入老年代
    • 复制其余对象到新的内存空间,清空原来的空间
  3. 结合-XX:+PrintGCDetails日志分析:
    • 上面的三个内存块分别代表的是新生代的:eden,from,to区域
    • 新生代可用空间(第一行)=eden+复制区的其中一块内存(from/to)
    • 新生代可用空间一般比分配的空间小一些:from/to中有一块不可用
      ((28d80000-28e80000)/1024>13824k)

3)分代思想:

  1. 依据对象的存活时间分代,短命对象归为新生代,长命对象归为老年代(15岁)。
  2. 不同的代使用不同的算法:
    • 新生代:对象存活时间短,使用复制算法(Minor GC)
    • 老年代:大量对象存活,适合标记-清理算法或者标记-整理算法(Full GC)
  3. 堆的构成如下:
  4. 注意:
    所有的算法都需要识别一个垃圾对象,因此需要给出一个可触及性(可达性)的定义。

5.可触及性(可达性)

1)一些概念:

  1. 可触及的:
    • 从根对象可以触及到这个对象(到达这个对象)
  2. 可复活的:
    • 一旦引用被释放,就是可复活状态
    • 在finalize()中可能复活该对象(GC前会调用finalize()方法)
    • finalize()方法只会调用一次
  3. 不可触及的:
    • finalize()之后可能会进入不可触及的状态
    • 不可触及的对象不能复活
    • 可以被回收

2)代码示例:

  1. 类的代码如下:
  2. 测试代码如下:
  3. 测试结果:
  4. 结果分析:
    类实现了finalize方法,使用一个自引用进行复活。
    第一次GC前调用了finalize方法复活了,没有被回收。
    第二次GC没有调用finalize方法,对象被回收。
  5. 如果在不在第二次赋值为null(finalize结束后一直不释放引用),那么这个对象永远不会被回收。

3)finalize方法的缺陷:

  1. 经验告诉我们:
    • 避免使用finalize(),操作不慎可能导致错误。
  2. 主要的缺陷有:
    • 优先级低,何时被调用不确(原因就是GC何时调用不确定)
    • 何时进行GC也不确定
  3. 改进:使用try-catch-finally来替代
    在finally中释放资源

4)根对象:

  • 栈中的引用对象
  • 方法区静态成员或者常量引用的对象(全局变量)
  • JNI本地方法栈中的对象

6.Stop-The-World

1)Stop-The-World简介:

  1. Java中的一种全局暂停的现象:
    全局停顿,所有的Java代码停止运行,本地方法可以运行,但是不能和JVM交互。
  2. 引起该现象的原因:
    • GC(大部分情况,系统判断产生)
    • 死锁
    • Dump线程(操作失误)
    • Dump堆(操作失误)
  3. 为什么会全局停顿:
    在清理垃圾时又有新的垃圾产生,只有将程序停止,才能真正的打扫干净。
    GC线程清理垃圾的时候,将所有的线程停止,保证没有新的垃圾产生(可以比较顺利地进行)
    新生代比较快,而老年代一次GC的时间很长,会导致一些问题。
  4. 危害:
    • 长时间服务停止,没有响应(老年代GC)
    • 遇到HA(高可用)系统,可能会引起主从切换,严重危害生产环境。

2)全局停顿测试:(了解即可)

  1. 测试线程:每0.1秒打印一条信息
  2. 测试代码与测试参数:
  3. GC日志分析:

    从3153开始分别暂停了0.28秒和0.71秒。
    可以在右侧的GC日志中找到相对应的GC(根据GC处理的真实时间)。
    共三次时间较长的GC:0.28秒,0.28秒和0.43秒(第二次和第三次中间没有间隔,所以未输出任何信息)

7.串型垃圾收集器(回收器)

1)串型收集器简介:

  • 最古老,最稳定
  • 效率高
  • 可能会产生较长的停顿时间
  • 如何使用:-XX:+UseSerialGC
    • 新生代/老年代都会使用串行回收
    • 新生代使用复制算法(Serial串行收集器)
    • 老年代使用标记-压缩(整理)算法(Serial Old串行收集器)

2)示意图与日志示例:

  1. 示意图:简单的流程
  2. 示例日志输出:
  3. 串行回收器线程运行时应用程序线程暂停。
  4. 实际是Serial+Serial Old收集器

8.并行垃圾收集器(回收器)

1)ParNew收集器:

  1. 是Serial(新生代收集器)的并行版本。
  2. 参数:-XX:+UseParNewGC
    • 新生代并行(ParNew),复制算法
    • 老年代串行(Serial Old),标记-压缩(整理)算法
  3. ParNew是多线程的收集器,需要多核支持。
  4. 限制线程数量的参数:-XX:+ParallelGCThreads
  5. ParNew示意图:

    日志输出示例:
  6. 并行收集器并不是在所有的情况下都比串行收集器快。

2)Parallel收集器:

  1. 分为两个:
    • Parllel Scavenge:新生代并行收集器
    • Parllel Old:老年代并行收集器
  2. 类似ParNew,也是一个并行收集器:
    • 新生代复制算法
    • 老年代标记压缩算法
  3. Parllel Scavenge和ParNew差不多,但是无法和CMS收集器(后面介绍)配合使用。
  4. 和一般的收集器不同,它更加关注吞吐量(其他的则是关注如何缩短等待时间)
  5. 参数使用:
    -XX:+UseParllelGC:使用Parllel Scavenge+老年代串行收集器
    +XX:UseParllelOldGC:使用Parllel Scavenge+Parllel Old
  6. 示意图:

    日志输出示例:

3)并行回收器的相关设置参数:

  1. 停顿时间参数:-XX:MaxGCPauseMills
    • 最大停顿时间,单位毫秒
    • GC尽量保证回收时间不超过这个值
  2. 吞吐量参数:-XX:GCTimeRatio
    • 0~100取值范围
    • 垃圾收集总时间占程序运行CPU总时间的比
    • 默认为99,即允许1%的时间做GC
    • 时间越短,每次需要回收的对象越多,吞吐量越大,GC停顿时间越长。
  3. 这两个参数是矛盾的,不能同时设置(停顿时间和吞吐量)
    这两项不能同时调优

9.CMS收集器

1)CMS收集器简介:

  1. Concurrent Mark Sweep:并发标记清除垃圾收集器
    以获取最短回收停顿时间为目标的收集器
  2. 使用标记-清除算法,和标记-压缩算法相比:
    并发阶段会降低吞吐量
  3. 是一个老年代收集器(新生代使用ParNew)
  4. 使用参数:+XX:+UseConcMarkSweepGC
  5. GC并发与并行的区别:
    • 并行:多条垃圾收集线程并行工作,此时用户线程仍然属于等待状态
    • 并发:垃圾收集线程和用户线程同时(时间段)执行。

2)CMS收集器的执行过程:

  1. CMS的算法运行过程主要分为下面四个阶段:(主要是标记过程)
    • 初始标记:(全局停顿)
      • 标记根对象可以直接关联到的对象
      • 速度快
    • 并发标记:(和用户线程一起执行)
      • 标记所有对象(是或不是垃圾)
    • 重新标记:(全局停顿)
      • 并发标记时用户线程运行可能产生或复活垃圾对象
      • 所以在正式清理前需要做修正
    • 并发清除:(和用户线程一起执行)
      • 清除所有标记为垃圾的对象
  2. 在关键的部分还是会产生全局停顿的,只是将全局停顿的时间降到了最少。
  3. CMS执行过程示意图:
  4. GC日志示例:

    可以看到加黑的部分就是并发清理的过程。

3)CMS收集器的特点及一些缺陷:

  1. 尽可能的降低全局停顿时间
  2. 会影响系统整体的吞吐量和性能
    • 用户线程运行的过程中,分一半CPU去做GC,系统在做GC过程中的性能就会下降一半。
    • 吞吐量也会随之下降,因为全局停顿时间减少了
  3. 清理不彻底,有内存不连续:
    • 清理阶段仍然会有用户线程运行,会产生新的垃圾
    • 标记-清理算法清理完之后内存区间是分散的
  4. 和应用程序并发执行,导致无法在空间快满时进行清理:
    • -XX:CMSInitiatingOccupancyFactory参数设置一个阈值
    • 并发执行导致实际GC可用内存可能小于这个阈值(其余部分被应用程序占用)
    • 如果预留空间不够,会引起Concurrent mode failure
    • 错误日志如下:
  5. 解决上述faliuer的一种可行方案:使用串行回收器做后备
    一旦CMS失败,那么就启用串行收集器。(可能会停顿较长时间)

4)CMS相关GC参数:

  1. 为解决CMS使用的标记-清除算法所带来的内存碎片问题,JVM提供了CMS对应的GC参数来对内存空间进行整理。
  2. -XX:+UseCMSCompactAtFullCollection
    • 是否在每一次Full GC之后进行一次整理
    • 整理的过程是独占的,会造成全局暂停
  3. -XX:+CMSFullGCsBeforeCompaction
    • 设置几次Full GC之后进行一次整理
  4. -XX:ParallelCMSThreads
    • 设定CMS的线程数量
    • 不宜设置太大,小于可用CPU的数量
  5. 为了减轻GC压力需要注意的问题:
    • 软件如何设计架构
    • 代码如何编写
    • 堆空间如何分配

10.GC参数整理:

  • -XX:+UseSerialGC:在新生代和老年代使用串行收集器
  • -XX:SurvivorRatio:设置eden区大小和survivor区大小的比例
  • -XX:NewRatio:设置新生代和老年代的比
  • -XX:+UseParNewGC:新生代使用ParNew(并行收集器)
  • -XX:+UseParallelGC:在新生代使用Parllel Scavenge并行收集器
  • -XX:+UseParallelOldGC:老年代使用Parallel Old并行收集器
  • -XX:ParallelGCThreads:设置用于垃圾回收的线程数
  • -XX:+UseConcMarkSweepGC:新生代使用并行收集器(ParNew),老年代使用CMS+串行(后备)收集器
  • -XX:ParallelCMSThreads:设置CMS的线程数量
  • -XX:CMSInitiatingOccupancyFactory:阈值,老年代使用多少空间时进行回收(未到达之前也会回收)
  • -XX:+UseCMSCompactAtFullCollection:是否在每一次Full GC(完整垃圾回收)之后进行一次内存整理
  • -XX:CMSFullGCsBeforeCompaction:设定多少次Full GC之后进行一次内存碎片整理
  • -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
  • -XX:CMSInitiatingPermOccupancyFactory:永久区的占用率达到某一百分比时启动CMS回收(未到达之前也会回收)
  • -XX:CMSInitiatingOccupancyOnly:只有到达该阈值才会进行垃圾回收(未到达之前不回收)

11.注意

  • 系统性能根本所在是应用架构
  • GC参数只是属于微调,对系统性能的提升不会很大
  • GC参数设置不合理会产生大的延时,影响性能

× 请我吃糖~
打赏二维码