JVisualVm显示
JVisualVm显示
变量引用
问题引出:person本身这个变量不会占用字节吗?
class Person {
private String name; // 引用 (4/8 字节)
private int age; // int (4 字节)
private Address address; // 引用 (4/8 字节)
}
Person person = new Person()
// 假设person实例:
// 对象头:12 字节
// name 引用:4 字节
// age:4 字节
// address 引用:4 字节
// 单个 Person 实例的大小 = 12 + 4 + 4 + 4 = 24 字节
但是这时候person本身这个变量不会占用字节吗?
解答:
1. 变量 person 和对象实例的区别
Person person = new Person(); // 这行代码涉及两部分内存
| 部分 | 存储位置 | 大小 | 说明 |
|---|---|---|---|
person 变量 |
栈(stack) | 4字节(32位JVM) 或 8字节(64位JVM) | 只是一个指向堆内存的指针(引用),不是对象本身。 |
new Person() 对象实例 |
堆(heap) | 24字节(你的计算) | 真正的对象数据,包含对象头、字段等。 |
2. JVisualVM 显示的 "大小(Size)" 是什么?
- 仅统计堆(heap)中的对象实例(即
new Person()的部分),不包括栈上的引用变量。 - 因此:
person变量(栈上的引用)不会被计入 JVisualVM 的 "Size"。new Person()实例(堆中的对象)会被计入,即你计算的 24 字节。
3. 完整内存布局示例
64位JVM(压缩指针开启)
栈(stack) 堆(heap)
┌─────────────┐ ┌─────────────────────┐
│ person变量 │ ──────→ │ Person对象实例 │
│ (8字节) │ │ - 对象头: 12字节 │
└─────────────┘ │ - name引用: 4字节 │
│ - age: 4字节 │
│ - address引用: 4字节 │
│ (总计: 24字节) │
└─────────────────────┘
- 你的原始计算(24字节)可能因 JVM 对齐填充 略有差异,但原则不变:
- 栈上的
person变量 只是一个指针,不占用堆内存。 - 堆中的
Person实例 是 JVisualVM 统计的目标。
- 栈上的
4. 为什么 JVisualVM 不统计栈上的变量?
- 栈内存 由线程私有,生命周期随方法调用结束而释放,不参与垃圾回收。
- 堆内存 是垃圾回收的主要区域,JVisualVM 的设计目的是分析 堆中的对象分布和内存泄漏。
5. 验证方法
(1) 使用 jol (Java Object Layout) 工具
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
class Address {
}
class Person {
private String name; // 引用 (4/8 字节)
private int age; // int (4 字节)
private Address address; // 引用 (4/8 字节)
}
public class Main {
public static void main(String[] args) {
Person person = new Person();
System.out.println(ClassLayout.parseInstance(person).toPrintable());
}
}
输出示例(64位JVM,压缩指针开启):
com.sh.ai.Person object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
12 4 int Person.age 0
16 4 java.lang.String Person.name null
20 4 com.sh.ai.Address Person.address null
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
- 你会看到 对象实例的真实大小(不包括栈上的
person变量)。
(2) JVisualVM 观察
- 运行程序后,在 "Classes" 标签页 查看
Person的 "Size":- 应与
jol输出的实例大小一致(如 24 字节),不包含栈引用。
- 应与
6. 关键结论
| 内存部分 | 是否计入 JVisualVM "Size" | 原因 |
|---|---|---|
栈上的 person 变量 |
❌ 否 | 栈内存由 JVM 自动管理,不是垃圾回收目标,也不在堆中。 |
堆中的 Person 实例 |
✅ 是 | 这是 JVisualVM 的统计目标,用于分析对象内存占用和垃圾回收。 |
因此:
- 你的原始计算(32字节)是堆中
Person实例的大小,正确。 person变量本身(栈上的引用)不占用堆内存,所以不影响 JVisualVM 的统计。
类界面
基本显示

实例数 (Instances):当前内存中该类的活动对象实例数量。
大小 (Size):这些实例占用的实际内存大小(以字节为单位)。
-
计算方式是
实例数 × 单个实例的大小,不包括这些对象引用的其他对象所占空间。 -
注意:这个大小一般小于保留大小,因为不包含对象引用的其他对象。
保留大小 (Retained Size):如果这些实例被垃圾回收后,可以释放的总内存量。包括这些实例本身的大小加上它们独占引用的所有对象的大小。
-
该对象本身占用的内存(包括其字段)+ 该对象独占引用的所有对象的内存(即没有被其他对象引用的对象)。
-
重要性:这是分析内存泄漏时最重要的指标之一,因为它显示了真正可以被回收的内存总量
关键概念:保留大小(Retained Size)的计算
保留大小是指仅由该对象(或该类的所有实例)独占引用的内存,即如果这些对象被垃圾回收,能释放多少内存。
假设Person 对象(36字节)引用一个 Address 对象(16字节)。
情况1:Address 不被共享(独占引用)
- 假设:有
100个Person实例,每个Person实例独占一个Address实例(Address不被其他对象引用)。 Person的保留大小 =Person自身大小 +Address大小
=100 × 36+100 × 16
=3,600 + 1,600
= 5,200 字节
情况2:Address 被共享(非独占引用)
- 假设:有
100个Person实例,引用同一个Address。 - 此时,
Person的保留大小不包括Address的大小,因为即使Person被回收,Address仍然被其他对象引用,不会被释放。 - 所以
Person的保留大小 =Person自身大小 =100 × 36= 3,600 字节
总结
- 如果
Address被共享(被其他对象引用),Person的保留大小不会包括Address的内存。因为Address不被Person独占持有,Address还被其他对象引用,删除Person不会释放Address。 - 如果
Address不被共享(仅由Person引用),Person的保留大小会包括Address的内存。Address仅由Person引用,删除Person会释放Address。
使用 JVisualVM验证
class Address {
// 每个Address对象占用16字节 = 12对象头 + 4对齐填充
public Address() {
}
}
class Person {
// 每个Person对象实际占用24字节 = 12对象头 + 4name + 4age + 4address
// 每个Person对象JVisualVM显示36字节 = 占用24字节 + 可能包含对齐填充、JVM内部开销或统计方式差异
private String name; // 引用 (4 字节,压缩指针开启)
private int age; // int (4 字节)
private Address address; // 引用 Address 对象 (4 字节,压缩指针开启)
public Person(Address address) {
this.address = address;
}
}
class Student {
// 每个Student对象实际占用24字节 = 12对象头 + 4name + 4age + 4address
// 每个Student对象JVisualVM显示36字节 = 占用24字节 + 可能包含对齐填充、JVM内部开销或统计方式差异
private String name; // 引用 (4 字节,压缩指针开启)
private int age; // int (4 字节)
private Address address; // 引用 Address 对象 (4 字节,压缩指针开启)
public Student(Address address) {
this.address = address;
}
}
public class MemoryTest {
public static void main(String[] args) throws InterruptedException {
System.out.println("程序启动,打开 JVisualVM 观察内存情况...");
// 场景1:Address 不被共享(每个 Person 独占一个 Address)
List<Person> personList1 = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Address address = new Address();
Person person = new Person(address);
personList1.add(person);
}
System.out.println("创建 100 个 Person,每个独占一个 Address...");
// 场景2:Address 被共享(多个 Student 共享同一个 Address)
Address sharedAddress = new Address();
List<Student> personList2 = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Student person = new Student(sharedAddress);
personList2.add(person);
}
System.out.println("创建 100 个 Student,共享同一个 Address...");
Thread.sleep(10000); // 10 秒时间观察 JVisualVM
System.out.println("测试结束,检查 JVisualVM 数据...");
}
}
运行上面的代码,打开JVisualVM,点击堆dump,得出一下结论:

Address 不被共享(每个 Person 独占一个 Address):Person 的保留大小 = Person 自身大小 + Address 大小。即5200 = 3600+1600
Address 被共享(多个 Student 共享同一个 Address): Student 的保留大小 = Student 自身大小
总结
| 指标 | 包含内容 | 是否在堆中? |
|---|---|---|
| 大小(Size) | 实例本身(对象头 + 字段 + 引用变量),不包括引用变量指向的对象 | 是 |
| 保留大小 | 实例本身(对象头 + 字段 + 引用变量) + 独占引用变量的所有对象(递归计算,排除共享部分) | 是 |
- 引用变量本身(如
address)始终计入Size。 - 引用变量指向的对象 只有在 独占引用变量时 才会计入
Retained Size。
这样设计的原因是:
Size用于分析对象自身的内存占用。Retained Size用于分析内存泄漏(识别哪些对象如果被删除会释放最多内存)。
显示实例界面

jvisualvm中的基本类型显示的值对应其本身,对象类型显示的值是啥?
基本类型(Primitive Types)
直接显示其实际值对象类型(Reference Types)
jVisualVM 默认显示对象 ID(而非
toString())显示的是 对象的引用地址(内存地址)
如果对象重写了
toString()方法,jvisualvm 可能会显示toString()的结果
浙公网安备 33010602011771号