JVM 学习笔记 - 垃圾回收篇

一、如何判断对象可以回收

 

  1. 引用计数法

  • 对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。
  • 对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点:无法处理循环引用的情况,如下图,A、B 对象都需要被回收,但是两者互相引用导致两个各自的引用计数器都不为0,无法被回收。这也是 Java 虚拟机不使用这个算法的原因。

JVM 学习笔记 - 垃圾回收篇

  2. 可达性分析算法

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。
  • 扫描堆中的对象,看是否能够沿着 GC Root 对象为起点的引用链找到该对象,找不到,表示可以回收。
  • 哪些对象可以作为 GC Root?
    • 虚拟机栈中引用的对象
    • 本地方法栈内JNI(通常说的本地方法)引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 所有被同步锁synchronized持有的对象
    • Java虚拟机内部的引用

  3. 四种引用

   1. 强引用

  • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

   2. 软引用

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时才会再次触发垃圾回收,回收软引用对象
  • 可以配合引用队列来释放软引用自身

   3. 弱引用

  • 仅有所引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
  • 可以配合引用队列来释放弱引用自身

   4. 虚引用

  • 必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
  • 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知

   5. 终结器引用

  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象

二、垃圾回收算法

   1. 标记清除算法

  • 定义:Mark Sweep,从 GC Roots 数组出发,标记被引用的对象。然后哪些没被标记就会在清除阶段被回收(对象其实并没有被清除,只是使用空闲列表的方式记录这些被回收对象的引用地址)
  • 速度较快
  • 会造成内存碎片

JVM 学习笔记 - 垃圾回收篇

   2. 标记整理算法

  • 定义:Mark Compact,跟标记清除算法一样,只不过多了压缩存活对象的步骤(清除完后对存活的对象移动到一块连续的内存空间中),目的是为了防止内存碎片
  • 速度慢
  • 没有内存碎片

JVM 学习笔记 - 垃圾回收篇

   3. 复制算法

  • 定义:Copy,把一块内存区域分为等份的两块(from、to),to 区总是空的。当 from 区满的时候就会将 from 区中存活的对象移动到 to 区,然后回收 from 区的所有对象,最后交换两个区域(from <=> to)
  • 不会有内存碎片
  • 需要占用双倍内存空间

JVM 学习笔记 - 垃圾回收篇

 三、分代垃圾回收

JVM 学习笔记 - 垃圾回收篇

  •  对象首先分配在伊甸园区域
  • 新生代空间不足时,触发 minor gc,伊甸园和 from 存活的对象使用 copy 复制到 to 中,存活的对象年龄加1并且交换 from to
  • minor gc 会引发 stop the world,暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行
  • 当对象年龄超过阈值时,会晋升至老年代,最大年龄是15(4 bit)
  • 当老年代空间不足,会先尝试触发 minor gc,如果空间仍不足,那么触发 full gc,STW 的时间更长

相关 VM 参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或 -XX:MaxHeapSize=size
新生代大小 -Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size )
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenuringThreshold=threshold
晋升详情 -XX:+PrintTenuringDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC

GC 分析

设置上下面的 JVM 参数,在启动程序后就会在控制台打印 GC 日志

public class Code_10_GCTest {

    private static final int _512KB = 512 * 1024;
    private static final int _1MB = 1024 * 1024;
    private static final int _6MB = 6 * 1024 * 1024;
    private static final int _7MB = 7 * 1024 * 1024;
    private static final int _8MB = 8 * 1024 * 1024;

    // -Xms20m -Xmx20m -Xmn10m -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
        list.add(new byte[_512KB]);
        list.add(new byte[_6MB]);
    }

}

四、垃圾回收器

  1. 串行

  • 单线程
  • 堆内存较小,适合个人电脑
-XX:+UseSerialGC=serial + serialOld

JVM 学习笔记 - 垃圾回收篇

  2. 吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 让单位时间内,STW 的时间最短

JVM 学习笔记 - 垃圾回收篇

-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio // 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 200ms
-XX:ParallelGCThreads=n

  3. 响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark

JVM 学习笔记 - 垃圾回收篇

  4. G1

  • 定义: Garbage First
  • 适用场景:
    • 同时注重吞吐量和低延迟(响应时间)
    • 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域
    • 整体上是标记-整理算法,两个区域之间是复制算法
  • 相关参数:
JDK8 并不是默认开启的,所需要参数开启

-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time

   (1) G1 垃圾回收阶段

JVM 学习笔记 - 垃圾回收篇

    (2) Young Collection

  • 会 STW

JVM 学习笔记 - 垃圾回收篇

   (3) Young Collection + CM

  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用对空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
XX:InitiatingHeapOccupancyPercent=percent (默认45%)

JVM 学习笔记 - 垃圾回收篇

    (4) Mixed Collection

        会对 E、S、O 进行全面垃圾回收

  • 最终标记(Remark)会 STW
  • 拷贝存活(Evacuation)会 STW
-XX:MaxGCPauseMills=xxms    // 用于指定最长的停顿时间

JVM 学习笔记 - 垃圾回收篇

 在对老年代进行回收时,会对所有老年代的 region 按需要回收的对象从多到少进行排序,然后选择垃圾最多的一部分 region 进行垃圾回收。这也是 G1(Garbage First:垃圾优先)的含义所在。

   (5) Full GC

  • SerialGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足发生的垃圾收集 - full gc
  • CMS
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足
  • G1
    • 新生代内存不足发生的垃圾收集 - minor gc
    • 老年代内存不足

   (6) Young Collection 跨代引用

  • 在 Young Collection 时,如果需要遍历老年代的所有对象来判断各个对象是否引用了年轻代的对象,这样效率就很低。所以在老年代中存在一个卡表,每一个脏卡就对应一个老年代对象,如果某个老年代对象有引用年轻代的某个对象,那么对应的脏卡就会指向这个年轻代的对象(通过异步的方式完成)。而在年轻代中也会有一个 Remembered Set(记忆集)来记录每个年轻代对象都被哪些老年代对象引用,这样就可以提高年轻代垃圾收集的效率。

JVM 学习笔记 - 垃圾回收篇

    (7) Remark(重新标记阶段)

  • 黑色:存活的对象
  • 灰色:正在标记阶段的对象
  • 白色:需要被回收的对象

JVM 学习笔记 - 垃圾回收篇

 假设在并发标记阶段标记了某个对象是垃圾(白色),但是在这个对象被回收之前,被用户用一个存活的对象强引用了,而如果没有并发标记,这样在垃圾回收的时候就会把这个对象给回收掉,这显然不会理(被强引用的对象不能被回收)。所以有了 remark,就是如果检测到有刚才说的这种情况,就会有一个写屏障,将这个被强引用的对象放到一个集合里面。等到 remark 阶段,就会去重新标记集合里面的每一个对象,这样的话如果对象被强引用了,就会变为黑色,后面就不会被回收了。

   (8) JDK 8u20 字符串去重

过程

  • 将所有新分配的字符串(底层是 char[] )放入一个队列
  • 当新生代回收时,G1 并发检查是否有重复的字符串
  • 如果字符串的值一样,就让他们引用同一个字符串对象
  • 注意,其与 String.intern() 的区别
    • String.intern() 关注的是字符串对象
    • 字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串标

优缺点

  • 节省了大量内存
  • 新生代回收时间略微增加,导致略微多占用 CPU
-XX:+UseStringDeduplication

   (9) JDK 8u40 并发标记类卸载

  • 在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类

   (10) JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,就称为巨型对象
  • G1不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉

JVM 学习笔记 - 垃圾回收篇

    (11) JDK 9 并发标记起始时间的调整

  • 并发标记必须在堆空间占满前完成,否则退化为 FulGC
  • JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
  • JDK 9 可以动态调整
  • -XX:InitiatingHeapOccupancyPercent 用来设置初始值
  • 进行数据采样并动态调整
  • 总会添加一个安全的空挡空间

五、垃圾回收调优

// 查看虚拟机参数命令
D:\JavaJDK1.8\bin\java  -XX:+PrintFlagsFinal -version | findstr "GC"

  1. 调优领域

  • 内存
  • 锁竞争
  • cpu 占用
  • io
  • gc

  2. 确定目标

低延迟/高吞吐量? 选择合适的GC

  • CMS G1 ZGC
  • ParallelGC
  • Zing

  3. 最快的 GC

首先排除减少因为自身编写的代码而引发的内存问题

  • 查看 Full GC 前后的内存占用,考虑以下几个问题
    • 数据是不是太多?
      • resultSet = statement.executeQuery(“select * from 大表 limit n”)
    • 数据表示是否太臃肿
      • 对象图
      • 对象大小 16 Integer 24 int 4
    • 是否存在内存泄漏
      • static Map map …
      • 第三方缓存实现

  4. 新生代调优

  • 新生代的特点

    • 所有的 new 操作分配内存都是非常廉价的
      • TLAB thread-lcoal allocation buffer
    • 死亡对象回收零代价
    • 大部分对象用过即死(朝生夕死)
    • Minor GC 所用时间远小于 Full GC
  • 新生代内存越大越好么?

    • 不是
      • 新生代内存太小:频繁触发 Minor GC ,会 STW ,会使得吞吐量下降
      • 新生代内存太大:老年代内存占比有所降低,会更频繁地触发 Full GC。而且触发 Minor GC 时,清理新生代所花费的时间会更长
    • 新生代内存设置为内容纳[并发量*(请求-响应)]的数据为宜
  • 幸存区需要能够保存 当前活跃对象+需要晋升的对象
  • 晋升阈值配置得当,让长时间存活的对象尽快晋升
-XX:MaxTenuringThreshold=threshold
-XX:+PrintTenuringDistrubution

  5. 老年代调优

以 CMS 为例:

  • CMS 的老年代内存越大越好
  • 先尝试不做调优,如果没有 Full GC 那么已经,否者先尝试调优新生代。
  • 观察发现 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent

看到这里记得点个赞哦!!!

上一篇:FPGA选型


下一篇:javascript-BOM-页面加载事件