java中的引用类型
强引用
-
最常见的引用类型
-
只要强引用存在,垃圾回收器就不会回收该对象
-
使用=操作符创建的就是强引用
-
几乎所有对象引用都是强引用
Object obj = new Object(); // obj是一个强引用
软引用
-
用于描述一些还有用但是非必须的对象
-
在内存不足的时候,垃圾回收器会回收软引用指向的对象
-
适用于缓存场景
-
场景:缓存大对象、如图片、音频等资源
import java.lang.ref.SoftReference;
SoftReference<Object> softRef = new SoftReference<>(new Object());
import java.lang.ref.SoftReference;
import java.util.HashMap;
import java.util.Map;public class ImageCache {
private Map<String, SoftReference<byte[]>> cache = new HashMap<>();public byte[] getImage(String key) { SoftReference<byte[]> ref = cache.get(key); if (ref != null) { byte[] image = ref.get(); // 获取实际对象 if (image != null) { return image; // 对象还在内存中 } } // 对象已被GC回收,重新加载 byte[] image = loadImageFromDisk(key); cache.put(key, new SoftReference<>(image)); return image; } private byte[] loadImageFromDisk(String key) { // 模拟从磁盘加载 return new byte[1024 * 1024]; // 1MB的图片数据 }}
弱引用
-
强度比软引用更弱
-
只要发生垃圾回收,就会回收弱引用指向的对象
-
常用于避免内存泄漏,如ThreadLoacl
-
用于避免内存泄漏,实现自动清理的映射表
-
ThreadLoacl、监听器注册、缓存键等
import java.lang.ref.SoftReference;
SoftReference<Object> softRef = new SoftReference<>(new Object());
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;public class ObserverManager {
private List<WeakReference<Observer>> observers = new ArrayList<>();public void addObserver(Observer observer) { observers.add(new WeakReference<>(observer)); } public void notifyObservers(String message) { // 清理已回收的观察者 observers.removeIf(ref -> ref.get() == null); for (WeakReference<Observer> ref : observers) { Observer obs = ref.get(); if (obs != null) { obs.update(message); } } }}
interface Observer {
void update(String message);
}
虚引用
-
最弱的引用类型
-
无法通过虚引用获取对象实例
-
主要用于跟踪对象被垃圾回收的状态
-
必须与引用队列(ReferenceQueue)一起使用
-
跟踪对象呗垃圾回收的活动
-
管理堆外内存、资源清理等
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;public class ResourceCleaner {
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();static class TrackedResource { private byte[] buffer; // 占用大量内存的资源 public TrackedResource() { this.buffer = new byte[1024 * 1024]; // 1MB } @Override protected void finalize() throws Throwable { System.out.println("Finalizing TrackedResource"); super.finalize(); } } public static void main(String[] args) { TrackedResource resource = new TrackedResource(); // 创建虚引用并关联到引用队列 PhantomReference<TrackedResource> phantomRef = new PhantomReference<>(resource, queue); resource = null; // 移除强引用 // 触发GC System.gc(); // 等待对象被加入引用队列 try { Thread.sleep(1000); if (queue.poll() != null) { System.out.println("对象已被垃圾回收"); // 在这里可以执行额外的资源清理工作 } } catch (InterruptedException e) { e.printStackTrace(); } }}
垃圾回收的基本原理
引用计数算法
- 每个对象都有一个引用计数器
- 每当有一个地方引用该对象的时候,计数器值加1
- 当引用失效的时候,技术其值减1
- 计数器为0的对象被认为是可回收的
- 缺点:无法解决循环引用的问题
可达性分析算法(目前主流实现)
- 从一系列成为"GC Roots"的对象作为起始点进行搜索
- 走过的路径被称为引用链(Reference Chain)
- 当一个对象没有被任何引用链连接的时候,则证明此对象不可达
- 不可达的对象被认为可以回收
- 以GC Roots为起点,向下搜索所有可达对象
- 不可达的对象被认为是可回收的
- 解决了循环引用的问题
- GC Roots包括以下几种对象:
-
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即native方法)引用的对象
- 标记引用链可达的,其他未标记的是可以删除的
JVM内存结构与分代回收
基于一下经验观察:
- 绝大多数对象都是朝生夕死(弱分代假说)
- 熬过越多次垃圾收集过程的对象就越难消亡(强分代假说)
- 跨代引用相对于同代引用只占少数(跨代引用假说)
因此将内存内存区域划分:
- 新生代:采用复制算法,因为大部分对象很快就会死亡
-
- Eden区:新创建的对象的初始存放位置
- Survivor区:包含From和To两个大小相等的区域
- 大部分对象在Eden区创建,经过Minor GC后存活的对象进入Survivor区
- 老年代:采用标记-清除、标记-整理算法,因为对象存活率很高
-
- 存放经过多次Minor GC依然存活的对象
- 或者大对象直接进入老年代
- 老年代满了会触发Major GC(Full GC)
- 永久代/元空间
-
- 存放类信息、常量、静态变量等
- java8之后改为元空间,使用本地内存
垃圾回收算法
标记-清除算法
- 阶段1:标记出所有需要回收的对象
- 阶段2:同意回收被标记的对象
- 缺点:效率低,产生碎片内存
复制算法
- 将内存分为两块、每次只使用其中一块
- 将存活的对象复制到另一块上,然后清理当前块
- 优点:效率高、无碎片;
- 缺点:内存利用率只有百分之五十
- 主要用于新生代
标记-整理算法
- 阶段1:标记所有需要回收的对象
- 阶段2:将存活对象向另一端移动,然后清理边界外的内存
- 用于老年代
内存分配策略
对象有限在Eden分配
- 大多数情况下,对象在新生代Eden区中分配
- 当Eden区没有足够空间进行分配的时候,发起一次Minor GC
大对象直接进入老年代
- 需要大量连续内存空间的对象成为大对象
- 避免大对象在Eden和Surivor区之间复制
长期存活的对象将进入老年代
- 对象每经历一次Minor GC后仍然存活,年龄+1
- 达到一定年龄阈值(默认15)的对象进入老年代
动态对象年龄判定
- 如果Survivor空间中相同年龄所有对象大小综合大于Survivor空间一半
- 年龄大于或等于该年龄的对象可以直接进入老年代
垃圾收集器类型
新生代收集器
- Serial收集器:单线程,适用于客户端模式
- ParNew收集器:多线程版本的Serial
- Parallel Scavenge收集器:关注吞吐量
老年代收集器
- Serial Old收集器:单线程,标记-整理算法
- Parallel Old收集器:多线程,标记-整理算法
- CMS收集器:关注停顿时间,标记-清楚算法
- G1收集器面向服务端应用,可预测的停顿时间模型
CMS收集器工作步骤
1、初始标记:标记GC Roots能直接关联的对象,STW
2、并发标记:根据可达性分析标记对象
3、重新标记:修正并发期间发生变化的对象标记,STW
4、并发清楚:清理未标记的对象
G1收集器特点
- 将堆划分为多个Region
- 追求可预测的停顿时间
- 支持并发与并行
- 支持分代收集
常见的垃圾回收器以及配置
垃圾回收器类型以及配置参数
Serial收集器(串行收集器)
- 适用场景:客户端模式下的简单应用
- 配置参数:-XX:+UseSerialGC
- 特点:单线程工作,使用复制算法
ParNew收集器(并行收集器)
- 适用场景:多核环境下的新生代收集
- 配置参数::-XX:+UseParNewGC
- 特点:多线程并行工作,使用复制算法
Parallel Scavenge收集器(吞吐量优先收集器)
- 适用场景:注重吞吐量的应用
- 配置参数:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC
- 特点:关注吞吐量,新生代适用复制算法
Serial Old收集器
- 适用场景:客户端模式或老年代收集
- 配置参数:-XX:+UseSerialOldGC
- 特点:单线程,使用标记-整理算法
Parallel Old收集
- 适用场景:注重吞吐量的服务端应用
- 配置参数:-XX:+UseParallelOldGC
- 特点:多线程,使用标记-整理算法
CMS收集器(Concurrent Mark Sweep)
- 适用场景:注重响应时间的应用
- 配置参数:-XX:+UseConcMarkSweepGC
- 特点:低延迟,使用标记-清除算法
G1收集器(Garbage First)
- 适用场景:大堆内存、低延迟要求的应用
- 配置参数:-XX:+UseG1GC
- 特点:可预测的停顿时间,分Region管理内存
JVM默认垃圾收集器选择
- java8以及以前:服务器模式下默认使用Parallel GC
- java9以后:默认适用G1收集器
- 客户端模式:通常适用Seria收集器
如何查看当前使用的垃圾收集器
# 查看当前JVM使用的垃圾收集器
java -XX:+PrintCommandLineFlags -version
# 在运行中的Java进程查看
jstat -gc <pid>
# 使用jinfo查看
jinfo -flag +PrintCommandLineFlags <pid>
垃圾收集器的搭配关系
- 新生代+老年代
-
- Serial + Serial Old
- ParNew + CMS
- Parallel Scavenge + Parallel Old
- G1(统一管理整个堆)
实际配置示例
# 使用G1收集器
java -XX:+UseG1GC -Xmx4g -Xms4g MyApp
# 使用CMS收集器
java -XX:+UseConcMarkSweepGC -XX:+UseParNewGC MyApp
# 使用并行收集器
java -XX:+UseParallelGC -XX:ParallelGCThreads=4 MyApp
选择垃圾收集器的考虑因素
- 应用类型:是否注重响应时间或吞吐量
- 堆内存大小:小堆(<4GB)、中堆(4-16GB)、大堆(>16GB)
- CPU核心数:多核环境适合并行收集器
- 停顿时间要求:是否允许长时间的STW
- 总之,垃圾回收器是可以根据应用需求进行配置的,开发者可以根据应用特点选择最合适的垃圾回收器
GC触发时机
- Minor GC:当Eden区满时触发,回收新生代
- Major GC/Full GC:
-
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC进入老年代的平均大小大于老年代的可用内存
- System.gc()被显式调用
常见的调优参数
- -Xms:设置堆最小值
- -Xmx:设置堆最大值
- -XX:NewRatio:设置老年代与新生代的比例
- -XX:SurvivorRatio:设置Eden区与Survivor区比例
- -XX:+UseG1GC:使用G1收集器
STW
STW是指在执行垃圾回收时,JVM暂停所有正在执行的应用程序线程,等待垃圾回收操作完成后在恢复这些线程的过程
特点
- 全局性:所有应用程序线程都会被暂停
- 临时性:暂停时间通常很短,但会影响应用程序响应
- 必要性:为确保垃圾回收过程中的内存一致性而必须采取的措施
发生时机
- 标记阶段:如CMS的初始标记和G1的初始标记
- 重新标记阶段:如CMS的重新标记和G1的最终标记
- Minor GC:大部分新生代垃圾回收时
- Full GC:老年代垃圾回收时
影响
- 应用程序在STW期间完全停止响应
- 对实时性要求高的应用,STW会影响用户体验
- STW时间越长,对应用的影响越大
各种垃圾回收器的STW
- Serial/Parallel:STW时间较长,但吞吐量高
- CMS:STW时间较短,主要在初始标记和重新标记阶段
- G1:STW时间可预测,通过控制Region数量控制停顿
- ZGC/Shenandoah:极短的STW,接近实时性
总结
STW是垃圾回收过程中不可避免的现象,现代JVM一直在女里减少STW的时间和频率,以提高应用程序的响应性能
元空间和堆的垃圾回收机制比较
特性比较
堆
- 存储对象实例,即程序中创建的各种对象
- 是GC的主要区域,包括新生代和老年代
- 使用java的垃圾回收器进行管理(如G1、CMS、parallel等)
- 回收算法包括 标记-清除、复制、标记-整理等
元空间
- java8引入,代替了永久代(PermGen)
- 存储类的元数据信息,如类定义、方法信息、常量池等
- 位于本地内存(native Memory)而不是java堆
- 由本地内存管理,而非javaGC管理
元空间垃圾回收机制
触发条件
- 元空间大小达到阈值时触发回收
- 通常伴随Full GC一起发生
- 类卸载(Class Unloading)是元空间回收的主要形式
回收过程
- 主要是类的卸载,当类不再被引用时候,其元数据就会被回收
- 元空间的内存由操作系统管理,使用malloc/free分配和释放
- 与堆的GC算法不同,元空间不适用传统的标记-清楚等算法
具体差异对比
|--------|--------|--------|
| 特性 | 特性 | 特性 |
| 存储内容 | 存储内容 | 存储内容 |
| 对象实例 | 对象实例 | 对象实例 |
| GC机制 | GC机制 | GC机制 |
| 专用GC算法 | 专用GC算法 | 专用GC算法 |
| 类卸载机制 | 类卸载机制 | 类卸载机制 |
| 管理方式 | 管理方式 | 管理方式 |
相关JVM参数
元空间相关参数
- -XX:MetaspaceSize:初始元空间大小
- -XX:MaxMetaspaceSize:最大元空间大小
- -XX:MinMetaspaceFreeRatio:最小元空间空闲比例
- -XX:MaxMetaspaceExpansion:元空间扩展上限
类卸载相关参数
- -XX:+CMSClassUnloadingEnabled:启用CMS类卸载
- -XX:+DisableExplicitGC:禁用System.gc()
实际表现
- 堆的GC会直接影响应用程序的性能和STW时间
- 元空间的GC通常与Full GC同时发生,影响相对较小
- 元空间不足会导致OutOfMemoryError:Metaspace错误
- 元空间的回收主要是类加载器不再引用某个类时的类卸载
- 总结来说元空间和堆使用不同的垃圾回收机制,元空间的回收主要通过类卸载机制实现,而堆则使用标准的java垃圾回收算法