文章目录
-
- 一、垃圾判断的基本概念
-
- 1.1 什么是垃圾对象
- 1.2 判断对象为垃圾的意义
- 二、垃圾判断的主要算法
-
- 2.1 引用计数算法(Reference Counting)
-
- 实现原理
- 优点
- 缺点
- 循环引用示例
- 2.2 可达性分析算法(Reachability Analysis)
-
- 实现原理
- GC Roots包括
- 实现伪代码
- 三、Java中的引用类型与垃圾判断
-
- 3.1 强引用(Strong Reference)
- 3.2 软引用(SoftReference)
- 3.3 弱引用(WeakReference)
- 3.4 虚引用(PhantomReference)
- 四、HotSpot虚拟机的具体实现
-
- 4.1 枚举根节点(GC Roots)
- 4.2 可达性分析的具体过程
- 4.3 不同收集器的实现差异
- 五、判断过程中的特殊处理
-
- 5.1 记忆集与卡表(Remembered Set & Card Table)
- 5.2 写屏障(Write Barrier)
- 5.3 并发标记的挑战与解决方案
- 六、实际应用中的注意事项
-
- 6.1 内存泄漏的常见模式
- 6.2 判断对象为垃圾的时机
- 6.3 监控与调试工具
- 七、总结:不同实现方式的对比
在Java中,垃圾收集器(Garbage Collector, GC)负责自动管理内存,识别和回收不再使用的对象。判断对象是否是垃圾是垃圾收集的基础,本文将全面剖析Java中判断对象是否为垃圾的各种方法、实现原理以及不同实现方式的区别。
一、垃圾判断的基本概念
1.1 什么是垃圾对象
在Java中,垃圾对象指的是程序中不再被任何活动对象引用的对象,这些对象已经无法被程序访问,占用的内存可以被安全回收。
1.2 判断对象为垃圾的意义
二、垃圾判断的主要算法
Java虚拟机主要使用可达性分析算法来判断对象是否为垃圾,同时历史上也曾使用过引用计数算法。
2.1 引用计数算法(Reference Counting)
实现原理
- 每个对象维护一个引用计数器
- 当被引用时计数器加1
- 当引用失效时计数器减1
- 计数器为0时判定为垃圾
// 引用计数伪代码实现
class ReferenceCountedObject {
private int count = 0;
public void addReference() {
count++;
}
public void removeReference() {
count—;
if (count == 0) {
reclaim(); // 回收对象
}
}
private void reclaim() {
// 释放资源
}
}
优点
- 实现简单
- 判断高效,可以立即回收垃圾
缺点
- 无法解决循环引用问题:这是Java未采用此算法的主要原因
- 计数器维护开销大
循环引用示例
class Node {
Node next;
public static void main(String[] args) {
Node a = new Node();
Node b = new Node();
a.next = b;
b.next = a;
a = null;
b = null;
// 此时a和b已无法访问,但因互相引用导致计数器不为0
}
}
2.2 可达性分析算法(Reachability Analysis)
实现原理
- 从一组称为GC Roots的根对象开始
- 通过引用链遍历所有可达对象
- 未被遍历到的对象判定为垃圾
graph TD
GC Roots –> A
GC Roots –> B
A –> C
B –> D
D –> E
F –> G
style F stroke:#f00,stroke-width:2px
style G stroke:#f00,stroke-width:2px
%% 说明
classDef garbage fill:#fdd,stroke:#f00;
class F,G garbage;
上图中F和G无法从GC Roots到达,因此被判定为垃圾。
GC Roots包括
实现伪代码
void markReachableObjects() {
Set<Object> marked = new HashSet<>();
Queue<Object> toVisit = new LinkedList<>();
// 1. 标记GC Roots
for (Object root : getGCRoots()) {
marked.add(root);
toVisit.add(root);
}
// 2. 遍历引用链
while (!toVisit.isEmpty()) {
Object obj = toVisit.poll();
for (Object ref : getReferences(obj)) {
if (!marked.contains(ref)) {
marked.add(ref);
toVisit.add(ref);
}
}
}
// 3. 回收未标记对象
for (Object obj : heapObjects()) {
if (!marked.contains(obj)) {
reclaim(obj);
}
}
}
三、Java中的引用类型与垃圾判断
Java提供了四种引用类型,它们对垃圾判断和回收行为有不同影响:
强引用 | 默认 | 永远不会被回收 | 普通对象引用 |
软引用 | SoftReference | 内存不足时回收 | 内存敏感缓存 |
弱引用 | WeakReference | 下次GC时回收 | 规范化映射 |
虚引用 | PhantomReference | 随时可能回收 | 对象回收跟踪 |
3.1 强引用(Strong Reference)
Object obj = new Object(); // 强引用
- 只要强引用存在,对象就不会被回收
- 是造成内存泄漏的主要原因之一
3.2 软引用(SoftReference)
SoftReference<Object> softRef = new SoftReference<>(new Object());
- 内存充足时不会被回收
- 当内存不足时(OOM前),会回收软引用对象
- 适合实现内存敏感缓存
3.3 弱引用(WeakReference)
WeakReference<Object> weakRef = new WeakReference<>(new Object());
- 无论内存是否充足,只要发生GC就会被回收
- 常用于WeakHashMap等场景
3.4 虚引用(PhantomReference)
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
- 无法通过虚引用获取对象
- 主要用于跟踪对象被回收的状态
四、HotSpot虚拟机的具体实现
4.1 枚举根节点(GC Roots)
HotSpot使用准确式GC(Exact GC),需要知道哪些位置存放着引用。实现方式:
// HotSpot中OopMap的简化表示
class OopMap {
intptr_t* _pc; // 指令地址
BitMap _map; // 引用位置位图
// …
};
4.2 可达性分析的具体过程
初始标记(Initial Mark):
- 暂停所有线程(Stop-The-World)
- 标记直接从GC Roots可达的对象
- 时间很短
并发标记(Concurrent Mark):
- 恢复应用线程
- 从初始标记的对象开始遍历整个对象图
- 与应用线程并发执行
最终标记(Final Mark):
- 再次暂停所有线程
- 处理并发标记期间变化的引用
- 使用增量更新(Incremental Update)或原始快照(Snapshot-At-The-Beginning)算法
4.3 不同收集器的实现差异
Serial | 单线程STW标记 | 客户端模式 |
ParNew | 多线程并行标记 | 新生代收集 |
CMS | 并发标记,减少停顿 | 老年代低延迟 |
G1 | 分区增量标记 | 大堆平衡吞吐与延迟 |
ZGC | 并发标记,染色指针 | 超大堆低延迟 |
五、判断过程中的特殊处理
5.1 记忆集与卡表(Remembered Set & Card Table)
解决跨代引用问题:
- 记忆集:记录从非收集区域指向收集区域的引用
- 卡表:是记忆集的一种实现,将堆划分为512字节的卡页
// HotSpot中卡表的简化表示
byte* _card_table;
byte* _card_table_start;
byte* _card_table_end;
5.2 写屏障(Write Barrier)
维护卡表一致性的机制:
// 写屏障伪代码
void post_write_barrier(oop* field, oop new_value) {
if (cross_generation_ref(field, new_value)) {
card_table.mark_dirty(field);
}
}
5.3 并发标记的挑战与解决方案
问题:并发标记期间对象图可能变化
解决方案:
#mermaid-svg-9LV1Z1Y5ZQPDedGg {font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .error-icon{fill:#552222;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .marker.cross{stroke:#333333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .cluster-label text{fill:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .cluster-label span{color:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .label text,#mermaid-svg-9LV1Z1Y5ZQPDedGg span{fill:#333;color:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .node rect,#mermaid-svg-9LV1Z1Y5ZQPDedGg .node circle,#mermaid-svg-9LV1Z1Y5ZQPDedGg .node ellipse,#mermaid-svg-9LV1Z1Y5ZQPDedGg .node polygon,#mermaid-svg-9LV1Z1Y5ZQPDedGg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .node .label{text-align:center;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .node.clickable{cursor:pointer;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .arrowheadPath{fill:#333333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .cluster text{fill:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg .cluster span{color:#333;}#mermaid-svg-9LV1Z1Y5ZQPDedGg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9LV1Z1Y5ZQPDedGg :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
并发标记问题
增量更新
原始快照
记录新引用
记录删除引用
六、实际应用中的注意事项
6.1 内存泄漏的常见模式
即使使用可达性分析,仍可能因以下情况导致内存泄漏:
// 典型内存泄漏示例
class LeakExample {
static List<Object> cache = new ArrayList<>();
void populateCache() {
for (int i = 0; i < 1000; i++) {
cache.add(new byte[1024 * 1024]); // 1MB
}
}
}
6.2 判断对象为垃圾的时机
6.3 监控与调试工具
jmap:查看堆内存分布
jmap -histo <pid>
jvisualvm:可视化内存分析
jvisualvm
Eclipse MAT:内存分析工具
java -jar mat/MemoryAnalyzer.jar
GC日志分析:
-Xlog:gc*:file=gc.log
七、总结:不同实现方式的对比
引用计数 | 简单 | 高 | 否 | 简单环境、非Java语言 |
可达性分析 | 复杂 | 中等 | 是 | Java等现代语言 |
并发标记 | 非常复杂 | 较低 | 是 | 低延迟应用 |
分区标记 | 复杂 | 高 | 是 | 大堆内存应用 |
Java选择可达性分析作为主要垃圾判断算法,是因为:
随着Java虚拟机的发展,垃圾判断算法也在不断优化,从最初的完全STW到现在的并发标记、增量标记等技术,在保证正确性的同时不断提高性能,减少停顿时间。
评论前必须登录!
注册