当前位置: 首页 > article >正文

细说Java 引用(强、软、弱、虚)和 GC 流程(一)

一、引用概览

1.1 引用简介

JDK1.2中引入了 Reference 抽象类及其子类,来满足不同场景的 JVM 垃圾回收工作:

  • SoftReference

    • 内存不足,GC发生时,引用的对象(没有强引用时)会被清理;
    • 高速缓存使用,内存不够,就回收,如需要读取大量的本地图片;
  • WeakReference

    • GC发生时,引用的对象(没有强引用 和 软引用 时)会被清理;
    • WeakHashMapThreadLocalMap 中 key 均是弱引用;
    • jdk动态代理中缓存代理类的WeakCache;
  • PhantomReference

    • GC发生时,引用的对象(没有强引用 和 软引用时)会被清理;
    • 本质是用来跟踪被引用对象是否被 GC 回收;
    • 堆外内存(DirectByteBuffer )清理;
    • 在静态内部类中,经常会使用虚引用。例如:一个类发送网络请求,承担 callback 的静态内部类,则常以虚引用的方式来保存外部类的引用,当外部类需要被 JVM 回收时,不会因为网络请求没有及时回应,引起内存泄漏;
    • MySQL使用虚引用来解决IO资源回收问题;
    • 虚引用往往作为一种兜底策略,避免用户忘记释放资源,引发内存泄露
  • FinalReference

    • 由其唯一子类Finalizer来处理重写了 Object.finalize() 方法的实例对象,jdk 1.9 已经废弃该方法;
    • 因为必须执行重写了 Object.finalize() 的方法后才能执行GC回收实例对象,会拖慢GC速度,finalize()中操作耗时的话,GC基本无法进行;

前面三种引用我们编码时可以使用,也是我们所熟知的软引用、弱引用虚引用FinalReference 是由 JVM 使用的,下面图片显示了FinalReference 类修饰符并非public

除此而外,平常编程使用 new 创建的对象均为强引用

在这里插入图片描述

1.2 引用内存分布

1.2.1 编码示例

在这里插入图片描述

  • 虚引用创建时必须传入引用队列(ReferenceQueue),软引用和弱引用可以不传;
  • 通过Reference.get() 获取引用的对象时,虚引用永远返回 null;

1.2.2 内存分布

在这里插入图片描述

1.3 引用使用流程

1.3.1 引用生命周期

在这里插入图片描述

  • 正常使用
    0、创建对象,图示{1};
    1、创建引用队列,创建引用对象,图示{2};
    2、使用完毕后,{1} 会断开,对象没有强引用了;
  • 引用清理介入
    3、GC清理对象时,发现有引用(软、弱、虚、FinalReference);
    3.0、引用为FinalReference 时,执行步骤 4;
    3.1、内存不足,清理对象实例;内存充足时,清理弱引用和虚引用所引用的对象实例;
    3.2、如果对象实例被清理,继续往下走,否则终止;
    4、GC线程将引用对象添加到 pending 队列中,同时唤醒阻塞的 ReferenceHandler 线程;
    5、ReferenceHandler 线程消费 pending 队列;
    6、消费后的引用对象添加到用户传入的引用队列中;
    6.1、如果创建引用时没有传入引用队列就终止;
    7、用户线程消费引用队列。
ReferenceHandler 线程

Reference 类静态加载 ReferenceHandler 线程:优先级最高的守护线程
在这里插入图片描述
在这里插入图片描述

1.3.2 引用状态流转

在这里插入图片描述

1.4 GC 引用

1.4.1 引用处理原码

void ReferenceProcessor::process_discovered_references(
  BoolObjectClosure*           is_alive,
  OopClosure*                  keep_alive,
  VoidClosure*                 complete_gc,
  AbstractRefProcTaskExecutor* task_executor) {
  NOT_PRODUCT(verify_ok_to_handle_reflists());
 
  assert(!enqueuing_is_done(), "If here enqueuing should not be complete");
  // Stop treating discovered references specially.
  disable_discovery();
 
  bool trace_time = PrintGCDetails && PrintReferenceGC;
  // Soft references
  {
    TraceTime tt("SoftReference", trace_time, false, gclog_or_tty);
    process_discovered_reflist(_discoveredSoftRefs, _current_soft_ref_policy, true,
                               is_alive, keep_alive, complete_gc, task_executor);
  }
 
  update_soft_ref_master_clock();
 
  // Weak references
  {
    TraceTime tt("WeakReference", trace_time, false, gclog_or_tty);
    process_discovered_reflist(_discoveredWeakRefs, NULL, true,
                               is_alive, keep_alive, complete_gc, task_executor);
  }
 
  // Final references
  {
    TraceTime tt("FinalReference", trace_time, false, gclog_or_tty);
    process_discovered_reflist(_discoveredFinalRefs, NULL, false,
                               is_alive, keep_alive, complete_gc, task_executor);
  }
 
  // Phantom references
  {
    TraceTime tt("PhantomReference", trace_time, false, gclog_or_tty);
    process_discovered_reflist(_discoveredPhantomRefs, NULL, false,
                               is_alive, keep_alive, complete_gc, task_executor);
  }
 
  // Weak global JNI references. It would make more sense (semantically) to
  // traverse these simultaneously with the regular weak references above, but
  // that is not how the JDK1.2 specification is. See #4126360. Native code can
  // thus use JNI weak references to circumvent the phantom references and
  // resurrect a "post-mortem" object.
  {
    TraceTime tt("JNI Weak Reference", trace_time, false, gclog_or_tty);
    if (task_executor != NULL) {
      task_executor->set_single_threaded_mode();
    }
    process_phaseJNI(is_alive, keep_alive, complete_gc);
  }
}

1.4.2 引用GC日志

增加JVM启动参数: -XX:+PrintReferenceGC -XX:+PrintGCDetails, 打印各种引用对象的详细回收时间。

涉及系统存在大量引用回收时,GC耗时显著增加,可以通过增加参数 -XX:+ParallelRefProcEnabled 开启 ( JDK8版本默认关闭的,在JDK9+之后默认开启 ) 并行处理引用来快速优化GC,具体根因后面可以继续分析。

日志样例如下:
2023-06-04T10:28:52.886+0800: 24397.548: [GC concurrent-root-region-scan-start]:开始扫描并发根区域。
2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-root-region-scan-end, 0.0545027 secs]:并发根区域扫描结束,持续时间为0.0545027秒。
2023-06-04T10:28:52.941+0800: 24397.602: [GC concurrent-mark-start]:开始并发标记过程。
2023-06-04T10:28:53.198+0800: 24397.859: [GC concurrent-mark-end, 0.2565503 secs]:并发标记过程结束,持续时间为0.2565503秒。
2023-06-04T10:28:53.199+0800: 24397.860: [GC remark]: G1执行remark阶段。
2023-06-04T10:28:53.199+0800: 24397.860: [Finalize Marking, 0.0004169 secs]:标记finalize队列中待处理对象,持续时间为0.0004169秒。
2023-06-04T10:28:53.199+0800: 24397.861: [GC ref-proc]: 进行引用处理。
2023-06-04T10:28:53.199+0800: 24397.861: [SoftReference, 9247 refs, 0.0035753 secs]:处理软引用,持续时间为0.0035753秒。
2023-06-04T10:28:53.203+0800: 24397.864: [WeakReference, 963 refs, 0.0003121 secs]:处理弱引用,持续时间为0.0003121秒。
2023-06-04T10:28:53.203+0800: 24397.865: [FinalReference, 60971 refs, 0.0693649 secs]:处理虚引用,持续时间为0.0693649秒。
2023-06-04T10:28:53.273+0800: 24397.934: [PhantomReference, 49828 refs, 20 refs, 4.5339260 secs]:处理final reference中的phantom引用,持续时间为4.5339260秒。
2023-06-04T10:28:57.807+0800: 24402.468: [JNI Weak Reference, 0.0000755 secs]:处理JNI weak引用,持续时间为0.0000755秒。
2023-06-04T10:28:57.821+0800: 24402.482: [Unloading, 0.0332897 secs]:卸载无用的类,持续时间为0.0332897秒。
[Times: user=4.60 sys=0.31, real=4.67 secs]:垃圾回收的时间信息,user表示用户态CPU时间、sys表示内核态CPU时间、real表示实际运行时间。
2023-06-04T10:28:57.863+0800: 24402.524: [GC cleanup 4850M->4850M(9984M), 0.0031413 secs]:执行cleanup操作,将堆大小从4850M调整为4850M,持续时间为0.0031413秒。

1.5 案例分析

1.5.0 高速缓存

1.5.1 WeakHashMap

  • WeakHashMap

    线程不安全, 通过 Collections.synchronizedMap来生成一个线程安全的map

public class WeakHashMap<K,V>
    extends AbstractMap<K,V>
    implements Map<K,V> {
    //……
    Entry<K,V>[] table;
    
    /**
     * Reference queue for cleared WeakEntries
     */
    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
}
  • WeakHashMap.Entry

    1、弱引用对象;
    2、传递了引用队列;
    3、没有引用队列消费线程, 通过WeakHashMap中大多数方法(get()put()size()等)来消费引用队列,从而释放Entry

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;

        /**
         * Creates new entry.
         */
        Entry(Object key, V value,
              ReferenceQueue<Object> queue, 
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
1.5.1.2 WeakhashMap使用场景
  • 缓存系统:Tomcat的工具类里的 ConcurrentCache
package org.apache.tomcat.util.collections;


import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

public final class ConcurrentCache<K,V> {

    private final int size;

    private final Map<K,V> eden;

    private final Map<K,V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }

    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            synchronized (longterm) {
                v = this.longterm.get(k);
            }
            if (v != null) {
                this.eden.put(k, v);
            }
        }
        return v;
    }

    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            synchronized (longterm) {
                this.longterm.putAll(this.eden);
            }
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}
  • 诊断工具:在阿里开源的Java诊断工具Arthas中使用了WeakHashMap做类-字节码的缓存。
/**
* 类-字节码缓存
* Class: Class
* byte[]: bytes of Class
**/ 
private final static Map<Class<?>, byte[]> classBytesCache = new WeakHashMap<>();

1.5.2 ThreadLocal

1.5.2.1 ThreadLocal 内存分配图如下所示

在这里插入图片描述

1.5.2.2 ThreadLocal 涉及对象及垃圾回收过程
  • ThreadLocalMap

    1、ThreadLocalMap 是 Thread 内部的成员变量,即图示{0}为强引用;
    2、ThreadLocalMap 是一个map,key是 WeakReference<ThreadLocal<?>>,即 key 是一个弱引用对象,图示{2};value 是一个强引用,图示{3};
    3、当Thread 销毁之后对应的 ThreadLocalMap 也就随之销毁;

  • ThreadLocalMap.Entry

    1、弱引用对象;
    2、没有传递引用队列;

	static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
  • ThreadLocal

    • 正常使用
      0、创建ThreadLocal对象,即图示{1};
      1、通过方法set(obj) 时会将值 obj 填充进当前线程的ThreadLocalMap,即图示{2}、{3};
    • 弱引用清理介入
      2、使用完毕后,{1} 断开;
      3、GC时发现ThreadLocal对象只存在弱引用,可以直接回收ThreadLocal对象;
1.5.2.3 ThreadLocal 思考
  • ThreadLocalMap 的 key 是强引用不可以吗?

    答:当线程一直存活时(实际使用时会用线程池,线程大概率不会销毁),图示{2}为强引用时,垃圾ThreadLocal对象无法回收,造成内存泄漏;

  • ThreadLocalMap 的 value 为啥不设计为弱引用?

    答:当 value 只有弱引用时,GC时会直接回收value,当使用key来获取value时,value返回null,这显然有问题;

  • ThreadLocalMap 什么情况下会导致导致内存泄漏?

    答:当线程一直存活时,key 因为是弱引用,GC会回收,但ThreadLocalMap的 value 却是强引用,会阻止GC回收;

    时间一长,ThreadLocalMap会存在很多 key 为 null,value 不为 null的情况;
    这种情况调用ThreadLocal.get()ThreadLocal.set()ThreadLocal.remove()有概率(遇到hash冲突) 将之前的 key 为 null 的 entry 清理;

    所以,使用完毕ThreadLocal,一定要记得执行remove()方法。

    综上,使用完ThreadLocalThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocal调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

  • ThreadLocal 的正确使用姿势

    • 定义为类的静态变量 private static final ThreadLocal<Integer>,如官方文档所示,通过静态方法ThreadId.get() 来使用,这样使得 key 永远存活(ThreadLocal实例在内存只有一份);
    • 使用完毕后通过实例方法ThreadLocal.remove() 来移除 Entry,从而避免ThreadLocalMap中的value 产生内存泄漏。
      在这里插入图片描述
  • ThreadLocalMap 内部Hash冲突使用的是线性探测,并非HashMap的拉链法。

    HashMap 是性能优先,尽可能的保证元素的高效访问。ThreadLocalMap 性能不是第一要素;如果数组元素比较密集的话,ThreadLocalMap 不管是 set 还是 get 都会不可避免地扫描很多节点,这肯定会影响性能。但是换来的收益,就是 ThreadLocalMap 可以在扫描节点时主动发现过期节点(key 为 null )且清理掉,尽可能的避免内存泄漏。

    ThreadLocal 中一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是斐波那契数 也叫 黄金分割数。hash增量为这个数字,使得 hash 码能均匀的分布在2的N次方的数组里, 即 Entry[] table,所以ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突;

    ThreadLocal 往往存放的数据量不会特别大,而且 key 是弱引用又会被垃圾回收,所以,线性探测法会查询更快,同时也更省空间。

1.5.2.4 ThreadLocal 使用场景
  • HikariPool 数据库连接池高性能原因之一:

    连接池从连接池中获取连接时对于同一个线程在ThreadLocal中添加了缓存,同一线程获取连接时没有并发操作。

  • 全局 Token 管理:自定义拦截器,把Token放入ThreadLocal 后续通过ThreadLocal ,获取用户信息;

  • org.slf4j.MDC(Mapped Diagnostic Context) 去埋点TraceId,跟踪多个服务调用,巧妙实现链路跟踪(MDC 底层依赖ThreadLocal);

  • 不同层数据库连接读取,用于完成一个事务;

  • ThreadLocal 用于同一个线程内,对于父子线程使用InheritableThreadLocal,线程池使用阿里巴巴的TransmittableThreadLocal组件

1.5.3 堆外内存(DirectByteBuffer )回收

  • HeapByteBuffer 在堆内存分配;

    ByteBuffer.allocate();

  • DirectByteBuffer 在堆外分配;

    ByteBuffer.allocateDirect();

1.5.3.1 DirectByteBuffer 内存分配图如下所示

在这里插入图片描述

1.5.3.2 DirectByteBuffer 涉及对象及堆外内存释放过程
  • Deallocator

    0、是一个 Runnable 对象;
    1、记录了堆外内存地址、大小;
    2、负责清理堆外内存。

  • Cleaner

    0、是一个虚引用(PhantomReference)对象
    1、ReferenceHandler 线程(Reference 类加载时会创建)触发清理;
    2、内部有 前驱和后继,方便组成双向链表(图示线路{5}),链表头 first 是Cleaner 类的静态变量;避免在 DirectByteBuffer 对象前被GC;
    3、线程安全。

  • DirectByteBuffer

    • 正常使用
      0、记录了堆外内存地址、大小;
      1、正常使用时通过图示线路{1}、{2}读写堆外内存;
      2、使用完毕后,{1} 会断开,变成不可达对象;
    • 虚引用清理介入
      3、GC 发现 DirectByteBuffer 对象有虚引用{4},同时清理 DirectByteBuffer 对象,断开{3},{4};
      4、GC 线程将虚引用对象 Cleaner 放入到 pending 队列,同时唤醒ReferenceHandler线程;
      5、ReferenceHandler 线程消费 pending 队列,拿到 Cleaner 对象后,调用Cleaner.clean()方法;
      6、Cleaner.clean()首先断开{5},然后内部调用 Deallocator.run() 方法清理堆外内存(依赖unsafe.freeMemory());
      7、Cleaner 对象和Deallocator 在后续GC时可以回收;
    • ReferenceHandler 线程 优先级最高(可查阅 1.3.1 引用生命周期处代码片段),只要GC触发后,就可以释放堆外内存;
    • 尴尬的是,GC时机不确定,那堆外内存释放的时间也不确定了?
    • 不怕,下一次分配堆外内存时,发现内存不足,会触发System.gc()
    • 所以,下一次分配是啥时候呢?不确定,哈哈。

1.5.4 FinalReference 回收

1.5.4.1 FinalReference 内存分配图如下所示

在这里插入图片描述

  • Finalizer

    • FinalReference 唯一的子类,用于执行 重写的java.lang.Object.finalize()方法;
1.5.4.2 Finalizer 内存释放过程
  • 正常创建对象

    1、创建对象(该对象覆写了方法:Object.finalize() ,记为FinalReferenceObj),图示{1};
    2、JVM 将该对象注册到Finalizer上,即创建 FinalReference 引用的对象 Finalizer,引用指向之前创建的对象,图示{2};
    3、同时,将Finalizer 实例对象加入到 unfinalized 链表中, 图示{7};

  • FinalReference 引用介入释放

    4、FinalReferenceObj 使用完毕,断开{1};
    5、GC时发现对象FinalReferenceObjFinalReference引用,暂停回收FinalReferenceObj对象;
    6、gc线程将Finalizer 对象放入 pending 队列;
    7、ReferenceHandler线程消费 pending 队列,取出Finalizer 对象,加入到引用队列中;
    8、FinalizerThread线程消费引用队列,取出Finalizer 对象,找到FinalReferenceObj 对象,执行其覆写的方法:Object.finalize()
    9、断开 FinalReference 引用,图示{2},后续GC可以回收FinalReferenceObj 对象;
    10、从 unfinalized 链表移除Finalizer 对象,图示{7};后续GC可以回收Finalizer 对象;

    FinalizerThread线程 优先级较低(可以查阅1.5.4.3 Finalizer代码简析 ),所以执行 finalize() 方法会延迟(如果finalize()方法内部也有耗时操作,那就是雪上加霜了),导致最终累积大量垃圾,造成GC耗时,拖垮系统。

1.5.4.3 Finalizer代码简析
final class Finalizer extends FinalReference<Object> {
	
	// …… 
	
    // 1.0 unfinalized实际上是一个双向链表,在add方法被调用后,就会将当前对象加入到unfinalized链表。
    // 2.0 当前创建对象在虚拟机内仅该unfinalized链表持有一份引用
    // 3.0 当执行完重写的java.lang.Object#finalize方法后,才会重列表移除;避免FinalReference 引用在实例对象前被GC; 
    // 这里会拖垮GC效率,发现GC完了一次,压根没有释放内存
    // 4.0 本质就是给GC的对象一个强引用
    private static Finalizer unfinalized = null;
    private Finalizer next, prev;
    
    private Finalizer(Object finalizee) {
        super(finalizee, queue);
        add(); // 将当前对象加入到unfinalized链表。
    }
    /**
    * register方法仅会被虚拟机所调用,而且,只有重写了java.lang.Object#finalize方法的类才会被作为参数调用Finalizer#register方法。
    **/
    /* Invoked by VM */
    static void register(Object finalizee) {
        new Finalizer(finalizee);
    }
    
    // 启动 FinalizerThread 线程消费引用队列里的FinalReference,
    // 就是执行重写的java.lang.Object#finalize方法,然后从unfinalized 队列中移除,方便 GC。
    static {
        // ……
        Thread finalizer = new FinalizerThread(tg);
        // 该线程的优先级并不能保证
        finalizer.setPriority(Thread.MAX_PRIORITY - 2); 
        finalizer.setDaemon(true);
        finalizer.start();
    }
	// 自定义引用队列
    private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
    // ……

	/**
	* 执行obj.finalize()方法
	**/
	private void runFinalizer(JavaLangAccess jla) {
		// ……
		try {
			// 拿到覆写finalize()方法的对象,再次建立强引用
            Object finalizee = this.get();
            assert finalizee != null;
            if (!(finalizee instanceof java.lang.Enum)) {
	            // 执行obj.finalize()方法
                jla.invokeFinalize(finalizee);
                // 将刚刚的强引用释放;
                finalizee = null;
            }
        } catch (Throwable x) { }

		// 解除 FinalReference
        super.clear();
	}

	// ……
}
1.5.4.3 Java 程序启动时的线程

java程序启动时就有 finalizer 线程(FinalizerThread)、ReferenceHandler 线程,如下图示:

在这里插入图片描述

二、引用堆积引发GC耗时血案

  • RPC 使用短连接调用,导致 Socket 的 FinalReference 引用较多,致使 YoungGC 耗时较长

    • rpc项目中的长连接与短连接的思考

    解决:
    1、增加参数-XX:+ParallelRefProcEnabled 可以缓解;
    2、通过将短连接改成长连接,减少了 Socket 对象的创建,从而减少 FinalReference,来降低 YoungGC 耗时。

  • Mysql连接断开兜底策略使用ConnectionPhantomReference,导致大量PhantomReference 堆积,引起GC耗时严重

    • 案例一
    • 案例二
    • 案例三
    • 案例四

    解决:
    1、增加参数-XX:+ParallelRefProcEnabled 可以缓解;
    2、升级MySQL jdbc driver到8.0.22+,开启disableAbandonedConnectionCleanup 可以根治;


http://www.kler.cn/a/553132.html

相关文章:

  • DeepSeek + Claude 提升效果
  • win32汇编环境,窗口程序中使用月历控件示例二
  • deepseek写的文章如何自动下载保存
  • 动态网格图片展示中的自适应逻辑
  • 基于Django快递物流管理可视化分析系统(完整系统源码+数据库+详细开发文档+万字详细论文+答辩PPT+详细部署教程等资料)
  • 安卓基础(Socket)
  • 开目3DCAPP系列:三维制造成本分析与估算软件3DDFC
  • 轻量化VLM架构工作调研
  • pandas连接mysql数据库
  • 讯方·智汇云校华为官方授权培训机构
  • 海康 Java SDK 升级 JNA 版本
  • Weblogic 反序列化漏洞深度剖析与复现
  • 单片机原理与运用
  • 编译linux SDK
  • 同步异步日志系统-设计模式
  • 使用 Mammoth.js 渲染 Word 文档为 HTML:详细教程
  • linux查看程序占用的本地端口
  • 【雅思博客05】New Guy in Town
  • 撕碎QT面具(7):container控件被spacer挤扁,无法进行控件添加的处理方案。
  • 计算机网络抄手 运输层