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 的统计。

类界面

基本显示

image-20250805104535256

实例数 (Instances):当前内存中该类的活动对象实例数量。

大小 (Size):这些实例占用的实际内存大小(以字节为单位)。

  • 计算方式是 实例数 × 单个实例的大小,不包括这些对象引用的其他对象所占空间。

  • 注意:这个大小一般小于保留大小,因为不包含对象引用的其他对象。

保留大小 (Retained Size):如果这些实例被垃圾回收后,可以释放的总内存量。包括这些实例本身的大小加上它们独占引用的所有对象的大小。

  • 该对象本身占用的内存(包括其字段)+ 该对象独占引用的所有对象的内存(即没有被其他对象引用的对象)。

  • 重要性:这是分析内存泄漏时最重要的指标之一,因为它显示了真正可以被回收的内存总量

关键概念:保留大小(Retained Size)的计算

保留大小是指仅由该对象(或该类的所有实例)独占引用的内存,即如果这些对象被垃圾回收,能释放多少内存

假设Person 对象(36字节)引用一个 Address 对象(16字节)。

情况1:Address 不被共享(独占引用)

  • 假设:有 100Person 实例,每个 Person 实例独占一个 Address 实例(Address 不被其他对象引用)。
  • Person 的保留大小 = Person 自身大小 + Address 大小
    = 100 × 36 + 100 × 16
    = 3,600 + 1,600
    = 5,200 字节

情况2:Address 被共享(非独占引用)

  • 假设:有 100Person 实例,引用同一个 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,得出一下结论:

image-20250805144109479

Address 不被共享(每个 Person 独占一个 Address):Person 的保留大小 = Person 自身大小 + Address 大小。即5200 = 3600+1600

Address 被共享(多个 Student 共享同一个 Address): Student 的保留大小 = Student 自身大小

总结

指标 包含内容 是否在堆中?
大小(Size) 实例本身(对象头 + 字段 + 引用变量),不包括引用变量指向的对象
保留大小 实例本身(对象头 + 字段 + 引用变量) + 独占引用变量的所有对象(递归计算,排除共享部分)
  • 引用变量本身(如 address始终计入 Size
  • 引用变量指向的对象 只有在 独占引用变量时 才会计入 Retained Size

这样设计的原因是:

  • Size 用于分析对象自身的内存占用。
  • Retained Size 用于分析内存泄漏(识别哪些对象如果被删除会释放最多内存)。

显示实例界面

image-20250718093919679

jvisualvm中的基本类型显示的值对应其本身,对象类型显示的值是啥?

  • 基本类型(Primitive Types)
    直接显示其实际值

  • 对象类型(Reference Types)

    • jVisualVM 默认显示对象 ID(而非 toString()

    • 显示的是 对象的引用地址(内存地址)

    • 如果对象重写了 toString() 方法,jvisualvm 可能会显示 toString() 的结果

JVisualVm显示线程

JVisualVm显示线程

posted @ 2025-08-05 20:07  deyang  阅读(32)  评论(0)    收藏  举报