Java基础与面试-每日必看(7)

前言

  Java不秃,面试不慌!
  欢迎来到这片 Java修炼场!这里没有枯燥的教科书,只有每日一更的 硬核知识+幽默吐槽,让你在欢笑中掌握 Java基础、算法、面试套路,摆脱“写代码如写诗、看代码如看天书”的困境。
记住: 代码会背叛你,但知识不会! 坚持积累,总有一天,HR会为你的八股文落泪,面试官会因你的算法沉默。


GC如何判断对象可以被回收

  想象一下,你的电脑里有个**“清道夫”**,它专门负责清理那些没人要的对象,腾出空间给新对象。那么,这个清道夫到底是怎么判断谁该走、谁能留呢?有两种主要的方法:

1. 引用计数法:

“你还有人记得你吗?”

每个对象都有一个粉丝数(引用计数),有人用它,粉丝数+1;
没人用了,粉丝数-1;
粉丝归零了,完喽,GC 清道夫就会把它扫地出门!

但这方法有个坑——“互捧”问题!

class A {
    B b;
}
class B {
    A a;
}

如果 AB 互相指着对方,即:

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 的回收流程

  1. 先检查对象是不是 GC Roots 不可达,如果是,就准备回收它。
  2. 如果对象重写了 finalize() 方法,它就会被放进一个叫 F-Queue 的队列里,让一个低优先级线程跑 finalize()
  3. finalize() 里,对象还能“自救”,只要它重新和 GC Roots 建立联系(比如 this 被某个静态变量引用),它就能复活!
  4. 但是,这辈子只有一次翻盘机会! 第二次再进 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,此时:

  1. JVM 先用可达性分析(GC Roots),看看这个对象还有没有活路
  2. 如果对象和 GC Roots 之间没有引用链,说明它没人管了,就会被标记为垃圾。
  3. 如果对象还实现了 finalize() 方法,会给它一次“复活”机会(不推荐使用)。
  4. 彻底无望的对象,最终被 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 进阶知识
点赞支持,让我知道这篇内容对你有帮助!
收藏起来,以后面试前复习一遍,涨薪不慌!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Starry-Walker

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值