引用变量和对象实例
示例
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();threadLocal 是一个引用变量(Reference Variable),它存的是一个内存地址。
new ThreadLocal<>() 是一个对象实例(Object Instance),它是在内存中真实存在的实体。
threadLocal 这个引用变量,指向了 new ThreadLocal<>() 这个对象实例在内存中的地址。
new ThreadLocal<>():
JVM 在堆内存中开辟了一块空间,创建了一个 ThreadLocal 类型的对象实例。
这个对象有它自己的状态(比如它内部的数据结构)。
threadLocal:
JVM 在元空间里为这个类分配了一个位置,创建了一个名为 threadLocal 的引用变量。
这个引用变量本身只占很小的一点空间,专门用来存放一个地址。
=:
JVM 将第一步创建的对象实例的内存地址,赋值给了第二步创建的引用变量 threadLocal。
从此以后,我们通过 threadLocal 这个名字,就能找到并操作那个在堆里的 ThreadLocal 对象。
当我们讨论内存泄漏时,我们担心的不是引用变量本身,而是对象实例是否被回收。
threadLocal = null时,对象实例和引用变量时怎么被回收的?
threadLocal 这个引用变量(在元空间中)的值被修改为 null。它不再指向任何对象了。
触发 GC:当下一次垃圾回收(特别是 Young GC 或 Full GC)发生时,GC 会从 GC Roots 开始进行可达性分析。
分析结果:ThreadLocal 对象实例现在不可达了,没有任何路径可以从 GC Roots 访问到它。
回收对象:GC 会判定这个 ThreadLocal 对象为垃圾,并将其回收,释放它占用的堆内存。
在这种情况下,对象实例被回收了,而引用变量 threadLocal 本身依然存在(因为它在元空间里,生命周期和类一样长,没有回收的概念,一直存在),只是它的值变成了 null。
引用变量本身占用的内存非常小,几乎可以忽略不计。即使有“很多”引用变量,它们也不会导致内存不足。
在 64位 JVM 中,如果开启了压缩普通对象指针(默认开启,-XX:+UseCompressedOops),一个引用变量占用 4 字节。
如果没有开启压缩,则占用 8 字节。
我们以最常见的 4 字节来计算:
1,000,000(一百万)个引用变量,占用的内存是:
1,000,000 * 4 bytes = 4,000,000 bytes ≈ 4 MB
是的,一百万个引用变量才占用大约 4MB 内存。对于一个动辄分配几 GB(几千 MB)堆内存的 Java 应用来说,这几乎是九牛一毛。
集合类
List<Object> list = new ArrayList<>();
Object obj = new Object();
list.add(obj);
obj 的引用变量是?
第一步:创建对象和引用
Object obj = new Object();
new Object():在堆内存中创建了一个 Object 的对象实例。假设它的地址是 0x1234。
Object obj:在虚拟机栈中创建了一个名为 obj 的引用变量。
=:将对象实例的地址 0x1234 赋值给引用变量 obj。
此时内存状态:
[栈] obj (引用变量) ---> [堆] Object 实例 @ 0x1234
第二步:将引用传递给 List
list.add(obj);
这行代码做了什么?它把 obj 中存储的引用值(地址 0x1234)复制了一份,然后交给了 ArrayList。
ArrayList 内部有一个数组,它会把 0x1234 这个地址存到自己的数组元素里。
此时内存状态:
[栈] obj (引用变量) ---+
|
+---> [堆] Object 实例 @ 0x1234
|
[堆] ArrayList 内部数组 ----+
内存角度举例分析
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
假设我们的 JVM 运行在一个 64 位系统上,并且开启了压缩指针(UseCompressedOops),所以一个地址占用 4 字节。
第 1 步:加载类与分配静态引用
当 JVM 第一次准备使用这个类时:
加载类信息:类加载器读取 .class 文件,将 ThreadLocal 类的元数据信息(方法、字段定义等)加载到元空间。
分配静态引用:JVM 在元空间中为这个类分配空间,其中就包括我们的静态引用变量 threadLocal。
此时内存状态:
[元空间]
+---------------------------------------------------+
| ThreadLocal类的元数据... |
| |
| threadLocal (引用变量) |
| - 当前值: null (因为还没赋值,里面是空的) |
| |
+---------------------------------------------------+
第 2 步:创建对象实例
接下来,JVM 执行 new ThreadLocal<>():
分配内存:JVM 在堆内存的新生代 Eden 区,寻找一块连续的、未被使用的空间。
假设分配成功:JVM 找到了一块从地址 0x1000 开始的内存空间,大小足够容纳一个 ThreadLocal 对象(假设是 32 字节)。
初始化对象:JVM 在这块内存上初始化 ThreadLocal 对象的各个字段,执行构造函数。
此时内存状态:
[元空间]
+---------------------------------------------------+
| threadLocal (引用变量) |
| - 当前值: null |
+---------------------------------------------------+
[堆内存 - Eden区]
+---------------------------------------------------+
| ... 其他对象 ... |
| |
| 0x1000: [ ThreadLocal 对象实例 ] |
| - field1 = ... |
| - field2 = ... |
| - ... |
| |
+---------------------------------------------------+
现在,一个真实的 ThreadLocal 对象已经在堆里“安家落户”了,它的门牌号是 0x1000。
第 3 步:建立指向关系(赋值)
最后,JVM 执行 = 操作:
获取地址:JVM 知道刚刚创建的对象实例在地址 0x1000。
写入地址:JVM 将这个地址值 0x1000,写入到元空间里的 threadLocal 引用变量中。
最终内存状态:
[元空间]
+---------------------------------------------------+
| threadLocal (引用变量) |
| - 当前值: 0x1000 <--- 指向关系建立! |
+---------------------------------------------------+
|
| (引用/地址)
|
v
[堆内存 - Eden区]
+---------------------------------------------------+
| ... 其他对象 ... |
| |
| 0x1000: [ ThreadLocal 对象实例 ] |
| - field1 = ... |
| - field2 = ... |
| - ... |
| |
+---------------------------------------------------+
指向完成! 现在,threadLocal 这个引用变量就成功地指向了堆内存中的 ThreadLocal 对象实例。
另外一个例子:
public void process() {
Object obj = new Object(); // A
List<Object> list = new ArrayList<>(); // B
list.add(obj); // C
}
存变化过程:
执行 A 行:
栈:在 process 方法的栈帧中创建局部变量 obj。
堆:创建一个 Object 实例,假设地址为 0x2000。
指向:将地址 0x2000 写入栈中的 obj 变量。
[栈] obj (值: 0x2000) ---> [堆] Object实例 @ 0x2000
执行 B 行:
栈:创建局部变量 list。
堆:创建一个 ArrayList 实例,假设地址为 0x3000。
指向:将地址 0x3000 写入栈中的 list 变量。
[栈] list (值: 0x3000) ---> [堆] ArrayList实例 @ 0x3000
执行 C 行 (list.add(obj)):
读取:JVM 从栈中读取 obj 的值,即地址 0x2000。
复制:将这个地址值 0x2000 复制一份。
写入:将这个复制的地址值写入到堆中 ArrayList 实例的内部数组里。
最终内存状态:
+-------------------------+ +-----------------------+
| 虚拟机栈 | | 堆 |
|-------------------------| |-----------------------|
| process() 栈帧: | | ArrayList实例 @ 0x3000 |
| - obj (值: 0x2000) ---+----> | - 内部数组[0] (值: 0x2000) ---+
| - list (值: 0x3000) --+----> | |
+-------------------------+ | |
| |
+-----> Object实例 @ 0x2000 <----+
关键点:地址 0x2000 现在存在于两个地方:
栈中的 obj 变量。
堆中 ArrayList 的内部数组。
这就是为什么即使 obj 变量离开作用域(被销毁),只要 list 还在,那个 Object 实例就不会被回收的原因。
private static ThreadLocal<Object> threadLocal = new ThreadLocal<>();
为什么threadLocal 引用在元空间?
public void process() {
Object obj = new Object(); // A
List<Object> list = new ArrayList<>(); // B
list.add(obj); // C
}
为什么obj 引用在栈?Java 内存设计中一个最核心的原则:变量的生命周期决定了它的存放位置。
📍 问题一:为什么 threadLocal 引用在元空间?
核心答案:因为 threadLocal 是 static(静态)的。
static 关键字意味着这个变量不属于任何单个对象实例,而是属于类本身。
生命周期与类绑定:threadLocal 的生命周期从类被加载开始,到类被卸载结束。对于一个普通的 Java 应用,这个生命周期几乎等同于整个应用程序的生命周期,非常长。
存放位置:既然 threadLocal 是类的一部分,那么存放它的地方自然就是存放类信息的地方——元空间。元空间专门用来存放类的元数据(方法、字段定义等)和静态变量。
为什么不在栈里?
栈是线程私有的。每个线程都有自己的栈。static 变量是所有线程共享的,如果放在某个线程的栈里,其他线程就无法访问了,这违背了 static 的设计初衷。
为什么不在堆里?
虽然堆是所有线程共享的,但堆的主要职责是存放对象实例。把引用变量和对象实例混在一起管理,会让内存模型变得混乱。将类的“蓝图”(元空间)和“蓝图上的静态标记”(静态引用)放在一起,逻辑上更清晰。
一个比喻:
类:是一份建筑设计蓝图。
元空间:是存放所有蓝图的中央档案室。
static 变量:是蓝图上一个用红笔标出的永久性标记(比如“此楼总配电箱位置在此”)。
对象实例:是根据蓝图盖起来的一栋栋具体的楼,它们都在城市开发区(堆)里。
这个“永久性标记”自然要和蓝图一起存放在中央档案室(元空间),而不是放在某栋楼里(堆)或某个工人的口袋里(栈)。
🧪 问题二:为什么 obj 引用在栈?
核心答案:因为 obj 是一个局部变量,它的生命周期非常短暂。
生命周期与方法绑定:obj 是在 process() 方法内部定义的。它的生命周期从 process() 方法被调用开始,到 process() 方法执行完毕结束。当方法返回时,obj 这个引用变量就会被销毁。
存放位置:这种生命周期短、与线程执行紧密相关的变量,最适合存放在虚拟机栈里。每当一个方法被调用,JVM 就会为该线程创建一个栈帧,这个栈帧专门用来存放这个方法的局部变量(obj, list)、操作数等。
为什么不在元空间里?
元空间是为类的“长生命周期”成员准备的。把一个方法内部的临时变量放进去,就像在建筑蓝图上用铅笔写“张三今天来过工地”,用完还要擦掉,既没必要也浪费空间。
为什么不在堆里?
虽然 obj 指向的对象在堆里,但 obj 这个“遥控器”本身是临时的。如果把它也放在堆里,会增加 GC 的负担。GC 需要额外去追踪这个临时引用,而把它放在栈里,方法一结束,栈帧弹出,引用就自动消失了,非常高效,GC 根本不用管它。
内存
堆
Java 堆内存(Heap Memory)是 Java 虚拟机(JVM)管理的内存中最重要的部分之一,专门用来存放对象实例和数组。用 new 关键字创建的东西,都存放在这里。
堆内存是整个 JVM 实例中所有线程共享的区域,无论你创建多少个线程,它们访问的都是同一个堆。
Java 的自动内存管理,主要就是通过垃圾回收机制(GC)来管理堆内存。GC 会自动回收那些不再被引用的对象,释放空间。
堆的大小可以在 JVM 启动时通过参数设置:
-Xms # 初始堆大小 -Xmx # 最大堆大小 java -Xms512m -Xmx2g YourApp
堆的区域划分
典型结构是使用“分代模型”(Generational Model):
新生代
存放新创建的对象。老年代
存放长期存活的对象。元空间(JDK 8 之前叫永久代)
存放类的元数据信息、常量池、静态变量等。
GC和内存泄漏
概述
内存泄漏的本质,是某些不再需要的对象,却仍然被“无意地”持有引用,导致 GC 无法回收它们。
当一个本该被快速回收的对象(比如一个临时对象),被一个生命周期很长的对象(比如静态变量、长期存活的线程)引用时,这个临时对象就无法被回收了。随着时间推移,这类“僵尸对象”越积越多,最终耗尽内存。
泄漏场景
import java.util.ArrayList;
import java.util.List;
//引用变量list指向的对象实例new ArrayList<>()一直在变大,没有移除等操作,导致内存泄漏
public class StaticLeak {
// 这个 List 是静态的,生命周期与 JVM 相同
private static final List<Object> list = new ArrayList<>();
// 每次调用这个方法,都会向 list 中添加一个对象,但从不移除
public void addObject(Object obj) {
list.add(obj); // obj 被静态变量持有,无法被回收
}
public static void main(String[] args) {
StaticLeak leak = new StaticLeak();
for (int i = 0; i < 100000; i++) {
leak.addObject(new Object()); // 这些 Object 对象永远不会被 GC
}
}
}import java.io.FileInputStream;
import java.io.IOException;
//资源没释放,导致泄漏
public class ResourceLeak {
public void readFile() {
try {
FileInputStream fis = new FileInputStream("somefile.txt");
byte[] buffer = new byte[1024];
fis.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
// fis.close() 缺失,导致文件句柄泄漏
}
}GC回收机制能处理的情况
//引用变量显式置为 null
public void method() {
Object obj = new Object(); // 1. obj 是 GC Root,对象可达
System.out.println(obj.hashCode());
obj = null; // 2. 切断引用,对象不再被任何 GC Root 引用
// 3. 在下一次 GC 时,这个 Object 对象就会被回收
}//引用变量超出作用域(自动发生)
public void method() {
if (someCondition) {
Object localObject = new Object(); // localObject 是局部变量,是 GC Root
// ... 使用 localObject
} // <--- localObject 的作用域在这里结束
// 从这里开始,localObject 这个引用变量已经不存在了。
// 因此,它指向的 Object 实例就变得不可达,可以被 GC 回收。
// JVM 会自动处理这个过程,你不需要手动写 localObject = null;
}
//引用变量被重新赋值
public void method() {
Object obj1 = new Object(); // 对象 A 被创建
Object obj2 = new Object(); // 对象 B 被创建
obj1 = obj2; // obj1 现在指向了对象 B
// 此时,对象 A 不再被任何活跃的引用指向,变得不可达,可以被回收。
// 注意:这里并没有 obj1 = null;
}
//集合类中的对象被移除
List<Object> list = new ArrayList<>();
Object obj = new Object();
list.add(obj); // 对象被 list 引用,可达
// ... 一段时间后 ...
list.remove(obj); // 对象从集合中被移除,如果此时没有其他地方引用它,它就变得不可达
// 或者,整个集合被销毁
list = null; // 集合本身不可达,那么它里面的所有元素自然也变得不可达