JVM-垃圾回收

如何判断对象可以回收

引用计数法

  • 当一个对象被引用时,引用计数+1。计数=0时,则回收。但是,这种方式存在一个问题,那就是当两个对象循环引用时,这两个对象则无法回收。pthon使用
    在这里插入图片描述

可达性分析算法

  • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看是否能够沿着 GC Root对象 为起点的引用链找到该对象,找不到,表示可以回收
  • 哪些对象可以作为 GC Root ?
    • 可以通过Memory Analyzer(MAT)分析看哪些时根对象
    • 生成分析文件:jmap -dump:format=b,live,file=文件名.bin 进程id

四种引用

  1. 强引用
    • 只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收。
  2. 软引用(SoftReference)
    • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象。也就是说,没有直接的强引用引用该对象时,该对象可能会被垃圾回收。
    • 可以配合引用队列来释放软引用自身。(当我的软引用对象被回收掉时,软引用自身也为一个对象。如果在软引用创建时分配了一个引用队列,如果软引用对象被回收,则软引用自身会进入引用队列。)
  3. 弱引用(WeakReference)
    • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象。也就是说,只要发生了垃圾回收,该对象就会被回收。
    • 可以配合引用队列来释放弱引用自身。与软引用类似
  4. 虚引用(PhantomReference)
    • 必须配合引用队列使用,主要配合 ByteBuffer 使用。被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存。
  5. 终结器引用(FinalReference)
    • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize方法,第二次 GC 时才能回收被引用对象。
  • 引用示意图
    在这里插入图片描述

软引用示例

  • java。该方法没有直接通过List存储byte,而是通过一个SoftReference来间接引用byte[]。
	public static void soft() {
        // list --> SoftReference --> byte[]

        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());

        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
  • 运行结果。在第一个循环内,我们都可以正常取到list直接的byte值。而在再次循环时,发现已取不到
[B@6d6f6e28
1
[B@135fbaa4
2
[B@45ee12a7
3
[B@330bedb4
4
[B@2503dbd3
5
循环结束:5
null
null
null
null
[B@2503dbd3
  • 结果分析。通过分析,发现在第三次结果后已经进行新生代垃圾回收 PSYongGen。但是在第四次结果后,进行了两次Full GC。第一次全面的垃圾回收Full GC结果后发现内存依旧不足,因此进行了第二次Full GC,将软引用所引用的对象全部回收。从结果上来看,就是将前四次添加的对象byte[]全部回收。
[B@6d6f6e28
1
[B@135fbaa4
2
[B@45ee12a7
3
[GC (Allocation Failure) [PSYoungGen: 2091K->488K(6144K)] 14379K->13008K(19968K), 0.0285725 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[B@330bedb4
4
[GC (Allocation Failure) --[PSYoungGen: 4696K->4696K(6144K)] 17216K->17336K(19968K), 0.0018520 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 4696K->4541K(6144K)] [ParOldGen: 12640K->12501K(13824K)] 17336K->17042K(19968K), [Metaspace: 3443K->3443K(1056768K)], 0.0038756 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) --[PSYoungGen: 4541K->4541K(6144K)] 17042K->17066K(19968K), 0.0005470 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 4541K->0K(6144K)] [ParOldGen: 12525K->640K(8704K)] 17066K->640K(14848K), [Metaspace: 3443K->3443K(1056768K)], 0.0042042 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[B@2503dbd3
5
循环结束:5

软引用+引用队列示例

  • java
ublic class Demo2_4 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        List<SoftReference<byte[]>> list = new ArrayList<>();

        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();

        for (int i = 0; i < 5; i++) {
            // 关联了引用队列, 当软引用所关联的 byte[]被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }

        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while( poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }

        System.out.println("===========================");
        for (SoftReference<byte[]> reference : list) {
            System.out.println(reference.get());
        }
    }
}

弱引用示例

  • java
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();

        }
        System.out.println("循环结束:" + list.size());
    }
}

垃圾回收算法

标记清除

在这里插入图片描述

  • 如上图所示。分为两个阶段,第一个阶段先标记,看哪些是可以被垃圾回收的。第二个阶段就开始清除,释放当前空间。
  • 清除:并不是将当前空间的每个内存进行清零操作。只是将当前空间的起始地址记录,放入空闲分区表中。当分配新对象时,就开始进行查表。
  • 速度较快,但是容易出现内存碎片。

标记整理

在这里插入图片描述

  • 标记+整理。将清理垃圾过程中,将可用的对象向前移动,使内存更为紧凑,因此避免了内存碎片的产生。但是对象在整理的过程中需要移动,如果有一些局部变量引用了该对象,那么就需要改变引用地址,因此操作比较复杂,速度较慢。

复制

在这里插入图片描述

  • 结束后,交换两个的位置。也就是说,TO总是空间的一块空间。

分代垃圾回收

  • 在实际中,JVM不会单独的采用一种算法,而是通过多种算法结合协同工作。*具体的实现就是虚拟机里的分代垃圾回收机制。

垃圾回收步骤

在这里插入图片描述

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

相关VM参数

在这里插入图片描述

测试

  • java代码。
public class Demo2_1 {
    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 -XX:-ScavengeBeforeFullGC
    public static void main(String[] args) throws InterruptedException {
//        new Thread(() -> {
//            ArrayList<byte[]> list = new ArrayList<>();
//            list.add(new byte[_8MB]);
//            list.add(new byte[_8MB]);
//        }).start();
//
//        System.out.println("sleep....");
//        Thread.sleep(1000L);
    }
}
  • 在主方法内没有任何代码。进行输出测试
Heap
 def new generation   total 9216K, used 2181K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  26% used [0x00000000fec00000, 0x00000000fee216c8, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,   0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
 Metaspace       used 3359K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 369K, capacity 388K, committed 512K, reserved 1048576K

垃圾回收器

分类

串行

  • 特点
    • 单线程。在垃圾回收时,其它线程均暂停。
    • 堆内存较小,适合个人电脑
  • 配置:-XX:+UseSerialGC = Serial + SerialOld
    • serila:工作在新生代,采用复制算法
    • SerialOld:工作在老年代,采用标记+整理算法
  • 回收过程
    在这里插入图片描述
    假设有多核CPU都在运行,发现堆内存不足触发垃圾回收。则线程需要在安全点停下来,此时完成垃圾回收工作比较安全。

吞吐量优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 在单位时间内,尽可能让STW 的时间最短。垃圾回收时间占比最低,这样就称吞吐量高。(一次0.2秒,可能在一个小时的单位时间内只发生了两次,因此 0.2 + 0.2 = 0.4。)
  • 配置:
    • -XX:+UseParallelGC ~ -XX:+UseParallelOldGC :新生代,复制算法;老年代,标记+整理算法;开启一个另一个也会开启。
    • -XX:ParallelGCThreads=n :线程数
    • -XX:+UseAdaptiveSizePolicy:动态的调整edan space与survice space的比例
    • -XX:GCTimeRatio=ratio:调整吞吐量的目标,公式1/(1+ratio)。
    • -XX:MaxGCPauseMillis=ms:最大暂停ms,最大值为200。与吞吐量目标冲突
      在这里插入图片描述
  • 代表收集器:Parallel Scavenge收集器。

响应时间优先

  • 多线程
  • 堆内存较大,多核 cpu
  • 尽可能让单次 STW 的时间最短。
  • 配置:
    • XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld:老年代,使用CMS垃圾回收器,并发的,标记+清除;新生代,复制算法。(加粗表示在空间碎片太多不满足要求时,可能新生代会退化为serialOld进行垃圾回收
    • XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads:并行线程数与并发线程数
    • XX:CMSInitiatingOccupancyFraction=percent:垃圾回收时,可能会产生新的垃圾,可能需要一些空间来保留这些垃圾。这个参数就是控制何时进行垃圾回收。(假设值为80,则老年代内存占了80%时就进行垃圾回收。)
    • XX:+CMSScavengeBeforeRemark:在垃圾回收前进行一次新生代垃圾回收。
      在这里插入图片描述
      CPU运行,老年代发生了内存不足,线程都到达了安全点1暂停。CMS垃圾回收器开始工作,执行一个初始标记动作,其余线程阻塞,到达安全点2。初始标记结束后,其余线程继续运行,垃圾回收可以并发标记将其余垃圾标记出来,到达安全点3。在垃圾回收时可能有些干扰,因此并发标记结束后进行重新标记,到达安全点4。等重新标记结束后,用户线程又可以继续运行,清理线程开始清理。
  • 代表收集器:CMS收集器。

Garbage First:垃圾回收器

适用场景

  • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms。可以在用户线程工作时,垃圾回收线程同时执行。
  • 超大堆内存,会将堆划分为多个大小相等的 Region。每个区域都可以独立的作为edan space,survice,老年代。
  • 整体上是标记+整理算法,两个区域之间是复制算法

相关配置

-XX:+UseG1GC //启动开关
-XX:G1HeapRegionSize=size //设置区域大小
-XX:MaxGCPauseMillis=time //设置GC目标

G1的垃圾回收阶段

在这里插入图片描述

Young Collection

在这里插入图片描述

  • 当edan space沾满,会进行新生代垃圾回收,触发STW。
    在这里插入图片描述
  • 新生代垃圾回收,会将幸存的对象以拷贝的方式放入幸存区。
    在这里插入图片描述
  • 当幸存区的对象较多,或者幸存的对象超过一定时间,会触发新生代垃圾回收。幸存区一部分对象(满足时间的)会存入老年区。幸存区不够年龄对象和一部分新生代垃圾回收所处理的对象的会拷贝到另一个幸存区。

Young Collection + CM

  • 初始对象:标记根对象。并发标记:根据根对象的引用链找到其他对象
  • 在 Young GC 时会进行 GC Root 的初始标记
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会 STW),由下面的 JVM 参数决定
  • -XX:InitiatingHeapOccupancyPercent=percent(默认45%)
    在这里插入图片描述

Mixed Collection

  • 会对 E、S、O 进行全面垃圾回收
    • 最终标记(Remark)会 STW
    • 拷贝存活(Evacuation)会 STW
      在这里插入图片描述
  • 根据暂停时间,先收回一部分价值较高的老年区。复制的区域少了,就可以达到所设置的暂停时间。

Full GC minor gc 区分

  • SerialGC
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足发生的垃圾收集 - full gc
  • ParallelGC
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足发生的垃圾收集 - full gc
  • CMS
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足
  • G1
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足
    • 当垃圾回收的速度跟不上产生的速度时,会退化为SericalGC,发生FULL GC。

Young Collection 跨代引用

YC过程:首先找到根对象,根据可达性分析找到存活的对象,存活对象复制到幸存区。

  • 新生代回收的跨代引用(老年代引用新生代)问题

    • 当老年代引用了新年代,对应的卡标记为脏卡。在做GCroot遍历时不用找老年代,只需找对应的脏卡。较少了搜索范围,提高的效率。
      在这里插入图片描述
  • 卡表与 Remembered Set(记录对应的区域有外部的引用,也就是记录有哪些脏卡)

  • 在引用变更时通过 post-write barrier + dirty card queue 更新脏卡。

  • 更新 Remembered Set 是异步的,会通过concurrent refinement threads 更新 Remembered Set。

Remark 重标记

  • pre-write barrier + satb_mark_queue
    在这里插入图片描述
    在这里插入图片描述
  • 当对象的引用发生更改时,写屏障(与多线程的不一样)的代码就会执行。

G1垃圾回收器的优化

JDK 8U20字符串去重

  • 优点:节省大量内存
  • 缺点:略微多占用了 cpu 时间,新生代回收时间略微增加
  • 配置:-XX:+UseStringDeduplication
  • 思考:重复的创建会导致空间占用较大
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
  • G1将所有新分配的字符串放入一个队列
  • 当新生代回收时,G1并发检查是否有字符串重复
  • 如果它们值一样,让它们引用同一个 char[]
  • 注意,与 String.intern() 不一样
    • String.intern() 关注的是字符串对象
    • 而字符串去重关注的是 char[]
    • 在 JVM 内部,使用了不同的字符串表

JDK 8u40 并发标记类卸载

  • 在之前的版本中,类加载后一直存在。有些类不再使用还占用着内存,对于空间优化及其不利。G1优化了这部分。
  • 所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类的实例不再被使用,一个类加载器的所有类都不再使用,则卸载它所加载的所有类。
  • -XX:+ClassUnloadingWithConcurrentMark 默认启用

JDK 8u60 回收巨型对象

在这里插入图片描述

  • 一个对象大于 region 的一半时,称之为巨型对象
  • G1 不会对巨型对象进行拷贝
  • 回收时被优先考虑
  • G1 会跟踪老年代所有 incoming 引用,这样老年代 incoming 引用为0 (如图所示,也就是当老年代卡表中对应的这个矩形对象引用为0时,图中表示就为无线段链接)的巨型对象就可以在新生代垃圾回收时处理掉

JDK9 并发标记起始时间的调整

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

JDK 9 更高效的回收

  • 官方文档:https://docs.oracle.com/en/java/javase/12/gctuning

垃圾回收调优

  • 预备知识
    • 掌握 GC 相关的 VM 参数,会基本的空间调整
    • 掌握相关工具
    • 明白一点:调优跟应用、环境有关,没有放之四海而皆准的法则
  • 查看当前虚拟机的配置参数
    • “JDK所在文件夹\bin\java” -XX:+PrintFlagsFinal -version | findstr “GC”
      在这里插入图片描述

调优领域

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

确定目标

  • 【低延迟】还是【高吞吐量】,选择合适的回收器
  • 低延迟:CMS,G1,ZGC
  • 高吞吐量:ParallelGC
  • Zing

最快的 GC:不发生GC

  • 查看 FullGC 前后的内存占用,考虑下面几个问题
    • 数据是不是太多?假设:resultSet = statement.executeQuery(“select * from 大表 limit n”)
  • 数据表示是否太臃肿?
    • 对象图
    • 对象大小 Object 16 Integer 24 int 4
  • 是否存在内存泄漏?
    • 有可能会发生内存泄漏:static Map map =
    • 建议使用:软引用、弱引用、第三方缓存实现

新生代调优

  • 新生代的特点
    • 所有的 new 操作的内存分配非常廉价。每个线程都会在edan space中分配一块私有的区域(TLAB:thread-local allocation buffer,可减小内存分配时的并发冲突),当new一个对象时,首先会检查这个TLAB中有没有可用的内存。若有,则优先在这个区域进行对象的分配。
    • 死亡对象的回收代价是零。当新生代发生垃圾回收,用了复制算法,edan space 与幸存区都清空,因此回收代价为零。
    • 大部分对象用过即死。
    • Minor GC 的时间远远低于 Full GC

新生代内存是否是越大越好

  • 越大越好吗?
    -Xmn
    Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery).GC is performed in this region more often than in other regions. If the size for the young generation is too small, then a lot of minor garbage collections are performed. If the size is too large, then only full garbage collections are performed, which can take a long time to complete. Oracle recommends that you keep the size for the young generation greater than 25% and less than 50% of the overall heap size.
  • 调整方式:
    • 理想情况:新生代能容纳所有【并发量 * (请求-响应)】的数据。
    • 幸存区大到能保留【当前活跃对象+需要晋升对象】。(VM会动态的根据幸存区空间大小调整晋升的阈值,可能会提前将某些对象晋升到老年区。)
    • 晋升阈值配置得当,让长时间存活对象尽快晋升。
      • -XX:MaxTenuringThreshold=threshold
      • -XX:+PrintTenuringDistribution //显示晋升的详细信息
        在这里插入图片描述

老年代调优

  • 以 CMS 为例
    • CMS 的老年代内存越大越好
    • 先尝试不做调优,如果没有 Full GC 那么说明老年代内存足够。就算发生了 FULL GC ,也可先尝试调优新生代。
    • 观察发生 Full GC 时老年代内存占用,将老年代内存预设调大 1/4 ~ 1/3。
    • -XX:CMSInitiatingOccupancyFraction=percent //老年代的空间占用到达阈值时,CMS进行垃圾回收。一般75%-80%

内存调优案例

  • 案例1:Full GC 和 Minor GC频繁
    • 若新时代空间紧张,当大量对象被创建,新生代的空间被快速占满。这也会导致大量不到晋升阈值的对象从幸存区晋升到了老年代,触发老年代FULL GC 的频繁发生。
    • 解决方法:先增大新生代内存。
  • 案例2:请求高峰期发生 Full GC,单次暂停时间特别长 (CMS)
    • 查看GC日志,分析CMS的哪一个阶段花费的时间较长。
    • 重新标记时会扫描整个堆内存,一般都是重新标记阶段花费时间较长,因此可以考虑在重新标记之前进行一次垃圾回收,减少进行标记的数量。
      • XX:+CMSScavengeBeforeRemark:在垃圾回收前进行一次新生代垃圾回收。
        在这里插入图片描述
  • 案例3:老年代充裕情况下,发生 Full GC (CMS jdk1.7)
    • 永久代实现的方法区,会因为永久代的空间不足产生 FULL GC
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值