【JVM】四大引用类型分析 【JVM】四大引用类型分析【一】引用分类【1】强引用 Strong Reference默认引用1定义与作用2代码案例3强引用的场景1-普通局部变量引用方法内2-成员变量实例变量3-静态变量 /static 静态集合最容易内存泄漏4-数组中存储对象5-容器集合ArrayList/HashMap/HashSet 等6-ThreadLocal 存储的值极易内存泄漏7-方法参数、返回值传递8-内部类 / 匿名内部类 / Lambda 持有外部对象9-循环引用A 持有 BB 持有 A10-本地变量缓存、临时引用赋值11-JNI / 本地方法持有 Java 对象12-常量池、字符串常量引用4快速总结哪些属于强引用5开发注意事项【2】软引用 SoftReference缓存专用1定义与作用2代码案例3开发注意事项【3】弱引用 WeakReference临时关联、无强制存活1定义与作用2代码案例经典场景WeakHashMap3开发注意事项【4】虚引用 PhantomReference最弱仅用于堆外内存回收1定义与作用2代码案例3开发注意事项【二】四种引用对比总表【三】通用开发规范与避坑总结【1】内存缓存选型规范【2】通用内存泄漏风险点【3】GC 与引用开发最佳实践【4】问题延伸【四】ThreadLocal的引用分析案例【1】底层存储结构前置认知1Thread 线程对象2ThreadLocalMap自定义哈希表非 HashMap3外部业务变量开发者定义的 ThreadLocal 变量4结构关系【2】引用链路1正常使用时完整引用链路分两条2断开外部 ThreadLocal 引用tl null 后的引用链路1. GC 发生后的变化2. 两种分支情况3执行 threadLocal.remove () 后的引用链路彻底释放【3】设计思路1为什么 Key 要设计成弱引用2为什么 Value 必须是强引用不能用弱引用3这套引用设计天生存在的缺陷Value 内存泄漏4ThreadLocalMap 自带的自动清理机制兜底方案5官方推荐的兜底方案手动 remove ()【4】整套引用设计思路总结分层梳理【5】开发对应注意事项结合引用设计衍生【一】引用分类Java 从 JDK1.2 开始引入引用分级机制目的是精细化控制对象回收时机解决传统强引用无法灵活释放内存、缓存溢出、大对象内存泄漏等问题。共分为 4 类强度从高到低强引用 软引用 弱引用 虚引用。【1】强引用 Strong Reference默认引用1定义与作用代码中最普通的对象赋值只要存在强引用链GC 永远不会回收该对象OOM 也不会释放。作用正常业务对象持有保证核心业务对象存活回收规则只有所有强引用断开置为 null、跳出作用域GC 才会回收。2代码案例publicclassStrongRefDemo{publicstaticvoidmain(String[]args){// 强引用obj 持有对象Objectobjnewbyte[1024*1024*10];System.gc();// GC 后对象依旧存在强引用不会被回收System.out.println(obj);// 断开强引用链objnull;System.gc();// 无任何强引用下次GC直接回收堆内存}}3强引用的场景只要一条可达的强引用链指向堆对象就是强引用GC 不会回收下面分大类列举所有开发中会遇到的强引用场景并配示例。1-普通局部变量引用方法内方法中直接new赋值给变量作用域内全程强引用。voidtest(){// str 是强引用StringstrnewString(demo);byte[]bignewbyte[1024*1024];}生命周期方法执行期间有效释放时机方法执行完毕局部变量栈帧销毁引用消失手动提前释放str null;2-成员变量实例变量对象内部属性持有另一个对象只要实例本身可达属性就是强引用。classUser{// 实例成员强引用ListOrderorderListnewArrayList();}UserunewUser();// u可达 → orderList 强引用存活释放条件u null整个 User 对象不可达内部成员引用才失效。3-静态变量 /static 静态集合最容易内存泄漏static属于类类加载后常驻方法区只要类不卸载引用永久有效。// 全局静态缓存强引用永久持有publicstaticListObjectCACHEnewArrayList();// 静态对象publicstaticBigDataDATAnewBigData();风险放入大量大对象程序运行期间永远不会回收极易 OOM。4-数组中存储对象数组元素对内部对象都是强引用。Object[]arrnewObject[10];arr[0]newbyte[1024*1024];// 数组持有强引用只有数组本身失去所有强引用内部元素才会被释放。5-容器集合ArrayList/HashMap/HashSet 等所有普通集合内部存储都是强引用key、value 全部强持有。HashMapString,ObjectmapnewHashMap();map.put(k,newLargeImg());// key、value 均为强引用对比WeakHashMap只有 key 是弱引用value 依旧是强引用。6-ThreadLocal 存储的值极易内存泄漏ThreadLocalMap 的 key 是弱引用但value 是强引用。ThreadLocalBigFiletlnewThreadLocal();tl.set(newBigFile());线程池场景下线程复用不调用tl.remove()value 会一直强引用常驻堆。7-方法参数、返回值传递对象作为入参、返回值调用栈持有强引用。// arg 是强引用voidfunc(Objectarg){}ObjectgetObj(){returnnewObject();// 返回后接收变量持有强引用}8-内部类 / 匿名内部类 / Lambda 持有外部对象非静态内部类会隐式持有外部类实例的强引用Lambda 捕获外部变量也会生成强引用。classOuter{ListlistnewArrayList();// 非静态内部类隐式持有 Outer.this 强引用classInner{}}// Lambda 捕获外层变量产生强引用Runnablerun()-System.out.println(list.size());容易出现外部类本应回收但内部类 / Lambda 还在运行导致外部类无法释放。9-循环引用A 持有 BB 持有 A两个对象互相持有对方属于双向强引用链。现代 CMS/G1/ZGC 可达性分析可识别并回收但仍会增加 GC 开销。class A { B b; } class B { A a; } A a new A(); B b new B(); a.b b; b.a a; a null; b null; // 无外部强引用GC 可回收10-本地变量缓存、临时引用赋值多次赋值只要变量还在就是强引用Objecto1newObject();Objecto2o1;// o2 也是同对象的强引用11-JNI / 本地方法持有 Java 对象native 代码通过 JNI 保存全局引用会长期强持有 Java 堆对象不主动释放会内存泄漏。12-常量池、字符串常量引用// 常量池常驻强引用永久存在Stringsabc;如果把常量字符串作为 WeakHashMap 的 key常量池一直持有 keykey 永远不会被回收弱引用失效。4快速总结哪些属于强引用普通局部变量、实例成员变量static 静态变量、静态集合数组、HashMap/ArrayList 等普通容器的 key/valueThreadLocal 的 value内部类、匿名类、Lambda 捕获外部对象方法参数、返回值接收对象对象互相循环引用JNI 全局引用、字符串常量池对象5开发注意事项静态集合极易内存泄漏static ListObject cache new ArrayList()全局静态集合持有对象程序不退出永远不回收大量缓存直接 OOM局部变量及时置空方法内超大数组、大文件对象使用完手动xxxnull缩短引用生命周期避免长生命周期对象持有短期大对象比如全局缓存持有图片、文件字节数组ThreadLocal 用完必须remove()否则线程复用导致强引用常驻堆。【2】软引用 SoftReference缓存专用1定义与作用强度次于强引用内存充足时 GC 不回收内存不足、即将发生 OOM 前JVM 会自动回收软引用对象。搭配ReferenceQueue可监听回收事件。核心场景内存缓存图片缓存、本地资源缓存、本地二级缓存回收规则堆空闲内存充足 → 保留堆内存紧张 → 全部回收。2代码案例importjava.lang.ref.ReferenceQueue;importjava.lang.ref.SoftReference;publicclassSoftRefDemo{publicstaticvoidmain(String[]args){// 引用队列对象被回收后会入队ReferenceQueuebyte[]queuenewReferenceQueue();// 软引用包装大数组SoftReferencebyte[]softRefnewSoftReference(newbyte[1024*1024*20],queue);System.out.println(内存充足获取对象softRef.get());// 疯狂分配内存挤压堆空间触发软引用回收Listbyte[]listnewArrayList();while(true){list.add(newbyte[1024*1024*10]);}// 内存耗尽前 softRef.get() 返回 null对象已被回收}}3开发注意事项做本地缓存优先用SoftReference替代单纯HashMap自动控内存必须配合ReferenceQueue清理失效软引用否则 Reference 对象本身堆积内存泄漏高并发缓存场景建议封装工具类定期清理队列中已回收的软引用 Key不能用于必须常驻的业务数据内存紧张会丢失缓存业务需做好缓存击穿兜底JVM 参数可调整软引用回收策略-XX:SoftRefLRUPolicyMSPerMB控制空闲内存保留时长。【3】弱引用 WeakReference临时关联、无强制存活1定义与作用强度低于软引用只要发生 GC无论内存是否充足直接回收弱引用对象。核心场景WeakHashMap底层全是弱引用、临时监听、非强制缓存、关联元数据典型使用ThreadLocalMap、缓存元信息、避免强引用循环泄漏。2代码案例importjava.lang.ref.WeakReference;publicclassWeakRefDemo{publicstaticvoidmain(String[]args){WeakReferenceObjectweakRefnewWeakReference(newObject());System.out.println(GC前weakRef.get());System.gc();// 主动触发GCSystem.out.println(GC后weakRef.get());// null对象已回收}}经典场景WeakHashMap// key 是弱引用key无外部强引用时自动清除EntryWeakHashMapString,ObjectweakMapnewWeakHashMap();StringkeynewString(cache-key);weakMap.put(key,newbyte[1024*1024]);keynull;// 断开强引用System.gc();// map 自动清除该键值对不会常驻内存3开发注意事项WeakHashMapKey 必须是包装对象不能是常量字符串字符串常量池存在强引用不会回收弱引用对象回收不可控不能存储需要稳定读取的数据ThreadLocal 底层使用弱引用 key若线程不清理 value 仍会发生内存泄漏value 是强引用大量临时元数据、一次性缓存优先弱引用减少堆常驻对象。【4】虚引用 PhantomReference最弱仅用于堆外内存回收1定义与作用强度最低无法通过 get () 获取原始对象唯一作用对象被 GC 回收时收到回收通知用于资源清理。必须绑定ReferenceQueue无队列则无任何意义。核心场景堆外内存NIO DirectBuffer释放、文件句柄、Native 资源、自定义资源回收回收规则对象进入可达性分析不可达后放入队列开发者在队列中做资源释放。2代码案例importjava.lang.ref.PhantomReference;importjava.lang.ref.ReferenceQueue;publicclassPhantomRefDemo{publicstaticvoidmain(String[]args)throwsInterruptedException{ReferenceQueueObjectqueuenewReferenceQueue();ObjectobjnewObject();PhantomReferenceObjectphantomnewPhantomReference(obj,queue);System.out.println(phantom.get());// 永远返回null无法获取对象objnull;System.gc();// 阻塞等待对象回收通知Reference?refqueue.remove();System.out.println(对象已被GC可以释放底层native资源);}}底层 NIODirectByteBuffer就是依靠虚引用监控堆外内存对象回收时主动释放操作系统堆外内存避免堆外内存溢出。3开发注意事项业务代码极少手动使用JDK NIO、文件流底层封装虚引用不能持有业务对象仅做回收钩子队列处理线程要异步、低延迟避免阻塞 GC 回收链路禁止在虚引用回调中创建新强引用会导致对象复活、永久无法回收。【二】四种引用对比总表表格引用类型回收时机核心用途get () 是否返回对象强引用无任何强引用链才回收正常业务对象、核心数据一定返回软引用内存不足 OOM 前回收内存缓存、图片资源内存充足返回不足 null弱引用只要 GC 就回收WeakHashMap、临时元数据GC 后返回 null虚引用GC 标记后入队无法获取对象堆外 / Native 资源释放永远 null【三】通用开发规范与避坑总结【1】内存缓存选型规范永久不能丢的数据强引用 持久化可丢失、内存友好缓存SoftReference临时、无强依赖元数据WeakReference堆外 / 本地文件 / 原生资源依赖虚引用做后置清理。【2】通用内存泄漏风险点静态集合强持有大对象无过期清理ThreadLocal 使用后不 remove线程池复用导致 value 常驻缓存未使用软 / 弱引用无限膨胀 OOM堆外 DirectBuffer 未被正常回收虚引用线程阻塞导致堆外溢出循环强引用A 持有 BB 持有 A无外部引用时现代 GC 可回收但老版本会泄漏ReferenceQueue 不消费大量 Soft/WeakReference 实例堆积占用堆。【3】GC 与引用开发最佳实践缓存工具类统一封装软引用定时轮询 ReferenceQueue 清理失效引用大对象使用完毕手动置空缩短强引用生命周期线程池、ThreadLocal 遵循用完即清原则堆外内存场景尽量使用池化减少虚引用回收压力不依赖System.gc()强制回收仅做调试生产禁用弱引用 Key 避免常量池字符串、全局单例对象。【4】问题延伸WeakHashMap为什么 Key 弱引用、Value 强引用防止 value 反向强引用 key导致 key 无法被回收软引用和弱引用的使用场景区分软引用适合用户可容忍缓存丢失的资源弱引用适合生命周期跟随 key 的附属数据虚引用为什么不能 get 对象此时对象已完成标记清除内存随时会被回收不允许访问防止野指针。【四】ThreadLocal的引用分析案例【1】底层存储结构前置认知1Thread 线程对象每个Thread实例持有成员变量 threadLocals ThreadLocal本身不存数据数据存在当前线程 Thread 对象内部// Thread 类源码ThreadLocal.ThreadLocalMapthreadLocalsnull;生命周期线程创建时初始化线程销毁后整个threadLocals直接丢弃线程池场景线程长期存活threadLocals不会被销毁。2ThreadLocalMap自定义哈希表非 HashMapThreadLocalMap是定制哈希表内部存储数组Entry[] table核心存储单元是自定义Entry。Entry 自定义实现staticclassEntryextendsWeakReferenceThreadLocal?{Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);// key 交给父类 WeakReference 包装valuev;// value 直接强引用保存}}核心设计Entry 的 key WeakReference弱引用Entry 的 value 普通强引用 Object3外部业务变量开发者定义的 ThreadLocal 变量// 外部强引用tl 是栈上局部变量 / static静态变量ThreadLocalUsertlnewThreadLocal();tl.set(newUser());4结构关系thread——》ThreadLocalMap——》Entry数组——》key是threadLocal的弱引用、value存入的Object对象new User()【2】引用链路1正常使用时完整引用链路分两条1链路 1外部代码 → ThreadLocal 对象Key 本体【强引用链】栈局部变量 tl强引用 → ThreadLocal实例key本体只要开发者没有执行tl null这条强引用链一直存在。2链路 2Thread 线程 → ThreadLocalMap → Entry → KeyValue 混合引用链Thread线程对象强引用 ↓ threadLocalsThreadLocalMap强引用成员变量 ↓ Entry[]table 数组强引用持有每一个Entry ↓ Entry 对象 ├─ 父类WeakReferenceThreadLocal→ 弱引用指向 ThreadLocal实例Key └─ 字段 value → 强引用指向 业务数据对象Value3合并完整可达链正常场景线程Thread → ThreadLocalMap → Entry 弱引用 → ThreadLocalKey ← 外部变量tl强引用 强引用 → 业务对象UserValue此时KeyThreadLocal同时存在外部强引用 Entry 内弱引用GC 绝对不会回收Value 只有一条强引用链Thread - Map - Entry - value线程存活则 Value 永远存活。2断开外部 ThreadLocal 引用tl null 后的引用链路执行代码tl null;此时外部栈强引用链断裂只剩下 Entry 内部的弱引用指向 ThreadLocal KeyThread → ThreadLocalMap → Entry ├─ 弱引用 → ThreadLocal(Key)【无任何强引用了】 └─ 强引用 → User(Value)1. GC 发生后的变化因为 KeyThreadLocal只剩弱引用GC 会直接回收 ThreadLocal 实例此时entry.get()返回null该 Entry 变成空 key 残留 EntryThread → ThreadLocalMap → Entry ├─ 弱引用目标已被回收get()null └─ 强引用 → User(Value)依然存在2. 两种分支情况1分支 A线程后续继续调用 get/set/rehashThreadLocalMap 在读写时会执行expungeStaleEntry()探测清理发现entry.get() null手动执行entry.keynull;entry.valuenull;断开 Value 的强引用业务对象 User 失去引用链下一次 GC 回收。2分支 B线程池线程长期不再操作该 ThreadLocalMap最容易泄漏线程长期存活不再执行任何get/set不会触发自动清理残留 Entry 永久存在Value 的强引用链永远无法断开Thread -ThreadLocalMap -Entry -value(强引用)-User对象User 对象无法被 GC产生内存泄漏。3执行 threadLocal.remove () 后的引用链路彻底释放remove()会直接定位当前 ThreadLocal 对应的 Entry做两步清空Entry 的弱引用 key 置空Entry 的 value 字段置空引用链完全断裂Thread - ThreadLocalMap - EntrykeynullvaluenullKey、Value 都无任何引用GC 可一次性回收从根源杜绝泄漏。【3】设计思路1为什么 Key 要设计成弱引用1场景推演没有弱引用会发生严重内存泄漏假设 key 是强引用业务代码定义ThreadLocal tl new ThreadLocal();线程调用tl.set(obj)ThreadLocalMap.Entry强持有tl业务代码断开外部引用tl null;此时 Entry 内部还存在一条强引用链Thread - threadLocals - Entry - key(强引用) - tl对象tl永远无法被 GCEntry 永久残留在线程 map 中value 也跟着常驻堆2弱引用的解决方案key 被WeakReference包装外部tl null后不存在任何强引用指向 ThreadLocal 实例下一次 GC 会直接回收 ThreadLocal 对象当ThreadLocalMap扩容、set、get 操作扫描哈希槽时会发现entry.get() nullkey 已回收自动清空整条 Entrykeyvalue释放内存3设计目的总结让 ThreadLocal 对象本身能正常被垃圾回收避免 ThreadLocal 实例永久驻留在线程的 Map 里降低无手动清理时的内存泄漏概率。2为什么 Value 必须是强引用不能用弱引用很多人疑惑既然 key 用弱引用value 为什么不一起弱引用1业务逻辑层面value 是我们要存储的数据使用 ThreadLocal 的核心诉求在线程生命周期内持有数据。如果 value 是弱引用线程执行中途只要触发一次 GCvalue 直接被回收get()突然返回 null业务代码无感知出现诡异空指针、上下文丢失完全不符合线程隔离存储的设计目标。2生命周期绑定逻辑keyThreadLocal工具对象用完可丢弃允许 GC 回收value业务数据线程执行期间必须稳定存在需要强引用保活3反向引用风险如果 value 弱引用同时 key 弱引用线程执行中 GC 随时清空 value上下文直接丢失违背 ThreadLocal 线程私有存储的定位。3这套引用设计天生存在的缺陷Value 内存泄漏1完整泄漏链路线程池场景最严重线程池核心线程长期复用线程对象不会销毁ThreadLocal tl new ThreadLocal(); tl.set(大对象);业务代码执行完tl null;GC 回收 ThreadLocalkey 弱引用生效Entry 变成[keynull, value大对象]若该线程后续不再执行 get/set/removeThreadLocalMap 不会自动清理空 key 的 Entry线程长期存活Entry 常驻value 强引用无法释放 → 内存泄漏2触发条件使用线程池线程不销毁ThreadLocal 实例外部引用置空没有手动remove()该线程后续不再操作这个 ThreadLocalMap自动清理机制无法触发4ThreadLocalMap 自带的自动清理机制兜底方案源码中get() / set() / rehash()方法都会执行探测清理 expungeStaleEntry遍历哈希桶遇到entry.get() null的过期 Entry将 entry.key null将 entry.value null断开 value 强引用整个 Entry 置空帮助 GC 回收局限性只有访问 ThreadLocalMap 时才会清理如果线程休眠、阻塞长期不操作 map过期 Entry 会持续堆积。5官方推荐的兜底方案手动 remove ()无论强弱引用设计规范写法try{threadLocal.set(context);// 业务逻辑}finally{threadLocal.remove();// 主动删除当前Entry彻底断开key、value引用}执行 remove 会直接把对应 Entry 的 key、value 置空从根源杜绝泄漏不依赖 GC 和自动清理逻辑。【4】整套引用设计思路总结分层梳理1设计目标实现线程私有数据隔离允许 ThreadLocal 工具对象正常 GC不常驻内存保证线程运行期间存储的业务数据不被 GC 随意回收内置自动清理逻辑作为兜底缓解内存泄漏。2分层设计取舍Key 使用弱引用解决 ThreadLocal 对象本身无法回收的问题避免工具类对象永久占用 EntryValue 使用强引用保障线程上下文数据稳定存活防止 GC 随机清空业务数据内置过期 Entry 自动清理在读写 map 时主动清除 key 已回收的无效 Entry释放 value 强引用暴露 remove API给开发者提供主动释放手段解决线程池长期线程导致的堆积泄漏问题。【5】开发对应注意事项结合引用设计衍生线程池场景必须在 finally 执行 remove不能依赖弱引用自动清理不要在线程中存放超大对象大字节数组、大量缓存泄漏后内存占用极高不要将 ThreadLocal 定义为局部临时变量且不 remove极易产生过期 Entry若使用一次性短期线程无线程池执行完销毁线程对象回收时整个 ThreadLocalMap 直接释放泄漏风险极低弱引用仅解决 ThreadLocal 对象回收完全解决不了 value 泄漏不要误以为弱引用就能高枕无忧。