问题
JVM如何判断对象是否存活?
答案
核心概念
JVM判断对象是否存活主要有两种算法:引用计数法(Reference Counting)和可达性分析法(Reachability Analysis)。现代JVM主要使用可达性分析法,因为它能够解决循环引用等复杂情况下的对象存活判断问题。
引用计数法
1. 基本原理
引用计数法为每个对象维护一个引用计数器,当有引用指向对象时计数器+1,引用失效时计数器-1,计数器为0时对象可被回收。
// 简化的引用计数法示例
public class ReferenceCountingExample {
public static void main(String[] args) {
Object obj = new Object(); // obj引用计数 = 1
Object ref1 = obj; // obj引用计数 = 2
Object ref2 = obj; // obj引用计数 = 3
ref1 = null; // obj引用计数 = 2
ref2 = null; // obj引用计数 = 1
obj = null; // obj引用计数 = 0,可回收
}
}
2. 循环引用问题
引用计数法的主要缺点是无法处理循环引用:
public class CircularReferenceProblem {
static class Node {
Node next;
String name;
public Node(String name) {
this.name = name;
}
}
public static void demonstrateCircularReference() {
Node nodeA = new Node("A"); // nodeA引用计数 = 1
Node nodeB = new Node("B"); // nodeB引用计数 = 1
nodeA.next = nodeB; // nodeB引用计数 = 2
nodeB.next = nodeA; // nodeA引用计数 = 2
nodeA = null; // nodeA引用计数 = 1
nodeB = null; // nodeB引用计数 = 1
// 此时两个对象都无法访问,但引用计数都不为0
// 引用计数法无法回收这些对象,造成内存泄漏
}
}
可达性分析法
1. 基本原理
可达性分析法从一系列称为”GC Roots“的根对象开始,沿着引用链搜索,能被访问到的对象为存活对象,不可达的对象为可回收对象。
public class ReachabilityAnalysisExample {
// GC Roots 包括:
// 1. 虚拟机栈中引用的对象
public static void stackReferences() {
Object localObj = new Object(); // 局部变量,GC Root
processObject(localObj);
}
// 2. 方法区中静态属性引用的对象
private static Object staticObj = new Object(); // 静态变量,GC Root
// 3. 方法区中常量引用的对象
private static final String CONSTANT = "Hello"; // 常量,GC Root
// 4. JNI引用的对象
private native Object getNativeObject(); // Native方法引用,GC Root
public static void processObject(Object obj) {
// obj可以被stackReferences中的localObj访问到,因此存活
}
}
2. GC Roots 详细说明
public class GCRrootsExample {
// 1. 静态变量引用
private static List<Object> staticList = new ArrayList<>();
// 2. 常量引用
private static final String CONSTANT_STRING = "常量字符串";
public static void main(String[] args) {
// 3. 虚拟机栈引用(局部变量)
Thread thread = Thread.currentThread();
Object localVar = new Object();
// 4. JNI引用(通过本地方法引用的对象)
// nativeMethod(); // 调用本地方法可能创建JNI引用
// 5. 正在运行的线程
Runnable runnable = () -> {
Object threadLocal = new Object(); // 线程栈中的引用
};
new Thread(runnable).start();
// 从这些GC Roots开始,可达的对象都是存活的
staticList.add(localVar); // localVar通过staticList可达
}
}
3. 可达性分析过程
public class ReachabilityAnalysisProcess {
static class Node {
String name;
List<Node> children = new ArrayList<>();
public Node(String name) {
this.name = name;
}
public void addChild(Node child) {
children.add(child);
}
}
public static void analyzeReachability() {
// 构建对象引用图
Node root = new Node("Root"); // GC Root
Node child1 = new Node("Child1");
Node child2 = new Node("Child2");
Node grandChild = new Node("GrandChild");
// 建立引用关系
root.addChild(child1);
root.addChild(child2);
child1.addChild(grandChild);
// 设置引用为null,断开某些可达路径
child2 = null; // child2仍通过root可达,不会被回收
// 可达性分析结果:
// Root -> Child1 -> GrandChild (可达)
// Root -> Child2 (可达)
// 所有对象都可达,不会被回收
}
public static void unreachableObjects() {
Node root = new Node("Root");
Node isolated = new Node("Isolated"); // 未被任何GC Root引用
// 可达性分析结果:
// Root: 可达 (GC Root直接引用)
// Isolated: 不可达 (没有任何引用路径到达GC Root)
// Isolated将被标记为可回收
}
}
垃圾回收算法详解
1. 标记-清除算法(Mark-Sweep)
public class MarkSweepAlgorithm {
public static void demonstrateMarkSweep() {
// 第一阶段:标记
// 1. 从GC Roots开始遍历,标记所有可达对象
List<Object> reachableObjects = new ArrayList<>();
markReachableObjects(getGCRoots(), reachableObjects);
// 第二阶段:清除
// 2. 遍历堆内存,回收未标记的对象
sweepUnmarkedObjects(reachableObjects);
}
private static void markReachableObjects(List<Object> roots, List<Object> reachable) {
// 深度优先搜索或广度优先搜索
for (Object root : roots) {
if (!reachable.contains(root)) {
reachable.add(root);
// 递归标记root引用的所有对象
markReferences(root, reachable);
}
}
}
private static void markReferences(Object obj, List<Object> reachable) {
// 标记对象引用的所有子对象
// 实际实现会遍历对象的字段,递归标记引用的对象
}
private static void sweepUnmarkedObjects(List<Object> reachable) {
// 回收所有不在reachable列表中的对象
}
}
2. 复制算法(Copying)
public class CopyingAlgorithm {
public static void demonstrateCopying() {
// 将内存分为两个区域:From Space和To Space
MemorySpace fromSpace = new MemorySpace("From");
MemorySpace toSpace = new MemorySpace("To");
// 只在From Space中分配对象
allocateInFromSpace(fromSpace);
// GC过程:
// 1. 从GC Roots开始,标记存活对象
List<Object> liveObjects = findLiveObjects();
// 2. 将存活对象复制到To Space
for (Object obj : liveObjects) {
Object copied = copyObject(toSpace, obj);
updateReference(obj, copied); // 更新引用
}
// 3. 交换From Space和To Space
swapSpaces(fromSpace, toSpace);
// 4. 清空原来的From Space(现在是To Space)
toSpace.clear();
}
}
3. 标记-整理算法(Mark-Compact)
public class MarkCompactAlgorithm {
public static void demonstrateMarkCompact() {
// 第一阶段:标记(与标记-清除相同)
List<Object> liveObjects = markLiveObjects();
// 第二阶段:整理
// 1. 计算每个存活对象的新位置
Map<Object, Address> newPositions = calculateNewPositions(liveObjects);
// 2. 移动对象到新位置
for (Object obj : liveObjects) {
Address newPos = newPositions.get(obj);
moveObject(obj, newPos);
}
// 3. 更新所有引用指向新位置
updateAllReferences(newPositions);
// 4. 更新内存分配指针
updateAllocationPointer();
}
}
对象的最终状态
1. 对象的死亡过程
public class ObjectFinalization {
static class ResourceHolder {
private Resource resource;
public ResourceHolder(Resource resource) {
this.resource = resource;
}
// finalize()方法(已废弃,不推荐使用)
@Override
protected void finalize() throws Throwable {
try {
System.out.println("Finalizing ResourceHolder");
if (resource != null) {
resource.close();
resource = null;
}
} finally {
super.finalize();
}
}
}
public static void demonstrateFinalization() {
ResourceHolder holder = new ResourceHolder(new Resource());
holder = null; // 对象变为不可达
// 1. 对象被标记为不可达
// 2. 如果对象有finalize()方法,会被加入F-Queue队列
// 3. Finalizer线程执行finalize()方法
// 4. 如果finalize()执行后对象仍不可达,对象被真正回收
}
}
2. 引用队列与软引用、弱引用
public class ReferenceTypes {
public static void demonstrateReferenceTypes() {
// 软引用:内存不足时才回收
ReferenceQueue<Object> softQueue = new ReferenceQueue<>();
SoftReference<Object> softRef = new SoftReference<>(new Object(), softQueue);
// 弱引用:下次GC时就回收
ReferenceQueue<Object> weakQueue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<>(new Object(), weakQueue);
// 虚引用:无法通过虚引用获取对象,用于跟踪对象回收
ReferenceQueue<Object> phantomQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), phantomQueue);
// 可以通过引用队列监控对象回收状态
monitorReferenceQueues(softQueue, weakQueue, phantomQueue);
}
}
性能优化考虑
1. 减少GC压力的编程技巧
public class GCOptimization {
// 1. 避免创建不必要的对象
public String badPractice(String prefix, String suffix) {
StringBuilder sb = new StringBuilder(); // 每次调用都创建新对象
sb.append(prefix).append(suffix);
return sb.toString();
}
public String goodPractice(String prefix, String suffix) {
return prefix + suffix; // 可能被编译器优化,减少对象创建
}
// 2. 重用对象
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
public String formatDate(Date date) {
return DATE_FORMAT.format(date); // 重用DateFormat对象
}
// 3. 使用对象池
private static final ObjectPool<StringBuilder> BUILDER_POOL = new ObjectPool<>();
public String processWithPool(List<String> items) {
StringBuilder builder = BUILDER_POOL.acquire();
try {
for (String item : items) {
builder.append(item);
}
return builder.toString();
} finally {
BUILDER_POOL.release(builder); // 重用StringBuilder
}
}
}
面试要点总结
- 两种算法:引用计数法(有循环引用问题)vs 可达性分析法(现代JVM使用)
- GC Roots:虚拟机栈、静态变量、常量、JNI引用、运行线程等
- 垃圾回收算法:标记-清除、复制算法、标记-整理
- 对象死亡过程:不可达 → 标记 → 回收(可能经过finalize)
- 优化技巧:减少对象创建、重用对象、合理使用引用类型
- 性能影响:STW(Stop-The-World)、内存碎片、复制开销
关键理解:
- 可达性分析是判断对象存活的根本方法
- GC Roots确定分析起点,避免回收正在使用的对象
- 不同GC算法有不同的性能特征和适用场景
- 合理的编程实践可以显著减少GC压力
实际应用:
- 理解GC原理有助于编写内存友好的代码
- 可以通过GC日志分析内存使用情况
- 选择合适的GC策略提升应用性能
- 避免内存泄漏和过度创建对象
这个知识点是JVM内存管理的核心,对于性能调优和问题诊断都非常重要。