前言
Java不秃,面试不慌!
欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。
GC如何判断对象可以被回收
想象一下,你的电脑里有个**“清道夫”**,它专门负责清理那些没人要的对象,腾出空间给新对象。那么,这个清道夫到底是怎么判断谁该走、谁能留呢?有两种主要的方法:
1. 引用计数法:
“你还有人记得你吗?”
每个对象都有一个粉丝数(引用计数),有人用它,粉丝数+1;
没人用了,粉丝数-1;
粉丝归零了,完喽,GC 清道夫就会把它扫地出门!
但这方法有个坑——“互捧”问题!
class A {
B b;
}
class B {
A a;
}
如果 A
和 B
互相指着对方,即:
A a = new A();
B b = new B();
a.b = b;
b.a = a;
就像两个过气明星互相点赞,明明没人真的关注他们,但他们的粉丝数一直不掉到 0!
这就导致 GC 误以为它们还有人要,迟迟不收拾它们,结果就内存泄漏了!
2. 可达性分析法(更聪明的清道夫)
“你还跟大佬有关系吗?” GC 不再傻乎乎地数粉丝,而是改用可达性分析,它从一群“核心大佬”(GC Roots)出发,看看对象有没有跟他们建立联系。
谁是GC的大佬(GC Roots)?
- 本地变量表中的对象(比如你代码里
int a = 10; Object obj = new Object();
这些) - 类的静态变量(
static
变量,哪怕没人用,它们也可能一直在) - 常量池里的对象(比如
"Hello"
这种字符串常量) - Native 方法里的对象(那些在 JNI 里活着的对象)
如果一个对象和大佬们没有任何联系,就说明它是个孤魂野鬼,没人管了,GC 清道夫就会准备收拾它。
3. “自救”机制:你有一次翻盘的机会!
但!GC 也不是铁面无情,它还给你一次机会,那就是 finalize()
方法。
GC 的回收流程
- 先检查对象是不是 GC Roots 不可达,如果是,就准备回收它。
- 如果对象重写了
finalize()
方法,它就会被放进一个叫 F-Queue 的队列里,让一个低优先级线程跑finalize()
。 - 在
finalize()
里,对象还能“自救”,只要它重新和 GC Roots 建立联系(比如this
被某个静态变量引用),它就能复活! - 但是,这辈子只有一次翻盘机会! 第二次再进 F-Queue,GC 直接无情地回收。
但是 finalize()
很坑,别用!
- 它执行慢,谁也不知道什么时候才会被执行。
- 可能导致程序卡顿,因为清理垃圾时它要等
finalize()
先跑完。 - 代码逻辑会变复杂,万一
finalize()
里再创建引用,可能导致对象活过来,结果该回收的没回收,浪费内存。
建议:忘了它吧,反正 Java 也不推荐你用!
彩蛋:一个“诈尸”案例
public class FinalizeEscape {
private static FinalizeEscape instance;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("哦豁!我复活了!");
instance = this; // 重新被引用,成功逃脱GC
}
public static void main(String[] args) throws InterruptedException {
instance = new FinalizeEscape();
instance = null; // 让它变成垃圾
System.gc(); // 请求 GC(不一定马上执行)
Thread.sleep(500); // 等待 GC 执行
if (instance != null) {
System.out.println("还活着!");
} else {
System.out.println("死透了!");
}
instance = null;
System.gc();
Thread.sleep(500);
if (instance != null) {
System.out.println("诈尸成功!");
} else {
System.out.println("彻底凉了!");
}
}
}
运行结果:
哦豁!我复活了!
还活着!
彻底凉了!
原因:
- 第一次 GC 执行时,
finalize()
让对象复活。 - 第二次 GC 时,它就没有机会了,直接凉透。
JVM 内存:有的地方大家挤着住,有的地方独享VIP!
Java 虚拟机(JVM)里的内存就像一个大房子,里头有几个不同的区域。
有的地方是所有线程共享的公共区域(像是大宿舍),有的地方是每个线程自己独享的(像是私人单间)。
🏢 线程共享的“公共宿舍”
1. 堆区(Heap)——大通铺,所有对象都住这!
这里是所有对象和数组的大本营,所有线程都能在这里创建和访问对象。
但你得小心多线程并发的问题,比如多个线程同时抢对象,可能会导致线程安全问题(比如ArrayList
不是线程安全的)。
GC(垃圾回收) 在这片区域里辛勤打扫卫生,回收没人用的对象。
2. 方法区(Method Area)——Java 课堂的黑板
这里存放的是类的元信息(比如类的字段、方法、常量池、静态变量等)。
所有线程都能访问这块区域!
在 JDK 1.8 之前,这块区域叫 永久代(PermGen),后来被 元空间(Metaspace) 取代。
🛏️ 线程独享的“私人单间”
1. 栈(Stack)——线程的“行李箱”
每个线程自己带着一个栈,里面装着它要执行的方法、局部变量等。
你写的每个方法都会在栈里开辟一个“栈帧”,执行完就自动收拾行李,撤走了。
其他线程不能乱翻你的行李!
如果栈用超了,会报 StackOverflowError(比如无限递归)。
2. 本地方法栈(Native Method Stack)——“外挂存储”
这个栈专门存放**调用本地方法(Native Method,比如 C 语言写的)**的栈帧。
如果你的 Java 代码调用了System.gc()
或JNI
里的 C 代码,就会在这开个新栈。
3. 程序计数器(PC Register)——“线程的GPS”
这是每个线程的小本本,记录着它当前执行到哪一行代码了。
CPU 需要不断切换线程,所以每个线程都有自己的程序计数器,方便恢复执行。
这个区域占的内存特别小,因为它只需要存个地址而已!
🌍 总结:谁共享,谁独享?
区域 | 共享 / 独享 | 作用 |
---|---|---|
堆(Heap) | 共享 🏢 | 所有对象和数组都在这里,垃圾回收也在这儿动手 |
方法区(Method Area) | 共享 📖 | 存放类的字段、方法、静态变量、运行时常量池等 |
栈(Stack) | 独享 🎒 | 线程的行李箱,存放方法调用的栈帧和局部变量 |
本地方法栈(Native Stack) | 独享 🎮 | 处理调用本地(Native)方法的栈 |
程序计数器(PC Register) | 独享 📍 | 线程的“GPS”,记录当前执行的位置 |
🎬 场景:对象的一生(Java 版《西游记》)
想象一下,一个 Java 对象就像一个西游小分队的新人,他从出生到“取经成功”(或者被 GC 处理掉 😢),会经历一系列的冒险。
🌱 1. 新生儿诞生——对象创建
「徒儿啊,你是谁?」
当你写下:
Person p = new Person();
JVM 会立刻开始工作:
- 先去方法区(Method Area)查找“类的信息”(就像唐僧在招收徒弟前,得先看看他们的履历)。
- 找到了类后,JVM 在 堆(Heap)里分配内存 给这个对象,分配完后,JVM 先给对象初始化成半成品(这时候变量还没有被赋值)。
- 执行构造方法(Constructor),填充对象的属性,让它变成一个完整的“徒弟”。
- 对象实例化成功,返回对象引用。
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
🍼 2. 新生儿呱呱落地——进入 Eden 区
「徒儿,你先住在庙里吧!」
刚创建的对象会先被分配到 堆(Heap)中新生代的 Eden 区,这个地方是对象的新手村,因为:
- 这里是 GC 重点照顾的区域,短命对象能快点被清理,减少内存占用。
- JVM 采用的是“逃不过三次 GC 就送走”原则(和猪八戒被唐僧三打白骨精类似)。
🎒 3. 试炼开始——Survivor S0 和 S1
「徒儿,考验开始了!」
Eden 区的对象,如果活过了一次 Minor GC,就会被送到 Survivor 区(S0 或 S1):
- S0 和 S1 是两个来回倒腾的地方,对象在这两个区域之间拷贝、存活、增加年龄。
- 每次 GC,活着的对象都会被转移到另一个 Survivor,每次移动年龄 +1。
- 对象的年龄最大是 15,但一般到了 一定年龄(如 8 或 10)就会晋升到老年代。
👴 4. 晋升老年——进入老年代
「徒儿,你终于成了大师兄!」
- 对象在 Survivor 区活得够久(比如 10 次 GC),就会被晋升到 老年代(Old Generation)。
- 老年代里的对象通常都是长寿对象,比如全局变量、缓存数据。
- GC 对老年代的清理频率较低,但一旦清理就是 Full GC,可能会造成应用短暂停顿(STW,Stop The World)。
💀 5. 终极考验——Full GC 清理
「徒儿,终究难逃一劫!」
当 JVM 发现 堆内存不够用了,会触发 Full GC,此时:
- JVM 先用可达性分析(GC Roots),看看这个对象还有没有活路。
- 如果对象和 GC Roots 之间没有引用链,说明它没人管了,就会被标记为垃圾。
- 如果对象还实现了
finalize()
方法,会给它一次“复活”机会(不推荐使用)。 - 彻底无望的对象,最终被 GC 清理出 JVM,彻底从内存消失
public class FinalizeDemo {
@Override
protected void finalize() throws Throwable {
System.out.println("我还想挣扎一下……");
}
public static void main(String[] args) {
FinalizeDemo obj = new FinalizeDemo();
obj = null; // 让它变成垃圾
System.gc(); // 尝试回收
}
}
🏁 6. 终局:对象彻底消失
「徒儿,你的任务完成了!」
- 被 GC 彻底回收后,对象就从 JVM 世界里永远消失,腾出宝贵的内存。
- 老年代的 Full GC 清理时间较长,会影响应用性能,所以需要调优垃圾回收策略(如 G1、ZGC)。
📌 总结:对象的一生
阶段 | 对象位置 | 发生了什么 |
---|---|---|
创建对象 | Eden 区 | 方法区找类信息,在堆里分配内存并初始化 |
刚出生 | Eden 区 | 所有新对象都先待在 Eden |
GC 试炼 | Survivor S0 / S1 | 存活的对象来回拷贝,每次加 1 岁 |
成长 | 老年代(Old Gen) | 年龄达到阈值(一般 10 岁),晋升老年代 |
清除 | Full GC 清理 | 无用对象被垃圾回收,彻底消失 |
🎤 最后的话
📢 求三连:点赞 👍 收藏 ⭐ 关注 ❤️
如果这篇文章帮到了你,别忘了给个“三连”!👇👇👇
💖 你的支持,就是我更新的动力! 💖
你可以:
✅ 关注我,第一时间学习更多 Java 进阶知识!
✅ 点赞支持,让我知道这篇内容对你有帮助!
✅ 收藏起来,以后面试前复习一遍,涨薪不慌!