使用 JOL 分析 Java 对象内存

前面介绍了Java对象在JVM中的内存布局。那么有什么方法可以方便的计算Java对象的内存占用?
本篇文章我们将介绍如何使用JOL分析Java对象内存。JOL是一个用来分析JVM中Object布局的小工具。包括Object在内存中的占用情况,实例对象的引用情况等等。JOL可以在代码中使用,也可以独立的以命令行中运行,文中使用java为jdk8。
quickstart
首先在项目依赖中添加JOL。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
</dependency>然后可以使用ClassLayout分析对象布局。
public class JolSimple
{
public static void main(String[] args) {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(Object.class).toPrintable());
}
}默认jol会使用jvm 的attach api动态加载agent。但是常有失败的可能。比较推荐的方法是:
- 显式指定Manifest
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<id>jol-samples</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>org.openjdk.jol.vm.InstrumentationSupport</Premain-Class>
<Launcher-Agent-Class>org.openjdk.jol.vm.InstrumentationSupport$Installer</Launcher-Agent-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>-javaagent方式指定 agent。
Case
下面将通过一些案例介绍如何使用JOL分析对象内存。下面的case默认开启了内存压缩。
基础类型&引用
使用下面的一行代码可以知道基础类型和引用占用的内存大小。
System.out.println(VM.current().details());打印日志如下:
## VM mode: 64 bits
## Compressed references (oops): 3-bit shift
## Compressed class pointers: 0-bit shift and 0x800000000 base
## Object alignment: 8 bytes
## ref, bool, byte, char, shrt, int, flt, lng, dbl
## Field sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
## Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8
## Array base offsets: 16, 16, 16, 16, 16, 16, 16, 16, 16上述日志说明
- Java引用占用4个字节(启用压缩引用)
- boolean/byte占用1个字节,char/short占用2个字节,int/float占用4个字节,double/long占用8个字节。
- 基础类型和引用作为数组元素呈现时占用相同的空间
- 数组基础offset(对象头+长度)是16,没有padding。
对象
使用例子查看对象的内存布局。
public class JolCase01 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(A.class).toPrintable());
}
public static class A {
long f;
}
}正如我们在前文介绍的,对象使用了8byte对齐,在不足的部分使用padding补齐。
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 (alignment/padding gap)
16 8 long A.f N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total数组
数组对象的长度没有放在数组类型中,JVM 在对象头中专门分配了一个区域用来存储数组长度。从本例中可以看到数组对象的长度信息。
public class JolArray01 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseInstance(new int[8]).toPrintable());
}
}从输出结果可以看到对象头中有 4 个字节的 array length 区域,存放数组长度。
[I object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf8000173
12 4 (array length) 8
16 32 int [I.<elements> N/A
Instance size: 48 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total字段重排序
使用例子查看多字段对象的内存布局。
public class JolCase02 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(A.class).toPrintable());
}
public static class A {
boolean bo1;
byte b1;
char c1, c2;
double d1, d2;
float f1, f2;
int i1, i2;
long l1, l2;
short s1, s2;
}
}正如我们在前文介绍的,对象使用了字段重排序,尽可能的填满内存。
info.victorchu.demos.jol.quickstart.JolCase02$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 float A.f1 N/A
16 8 double A.d1 N/A
24 8 double A.d2 N/A
32 8 long A.l1 N/A
40 8 long A.l2 N/A
48 4 float A.f2 N/A
52 4 int A.i1 N/A
56 4 int A.i2 N/A
60 2 char A.c1 N/A
62 2 char A.c2 N/A
64 2 short A.s1 N/A
66 2 short A.s2 N/A
68 1 boolean A.bo1 N/A
69 1 byte A.b1 N/A
70 2 (object alignment gap)
Instance size: 72 bytes
Space losses: 0 bytes internal + 2 bytes external = 2 bytes total继承字段顺序
使用例子查看继承对象的内存布局。
public class JolCase03 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(C.class).toPrintable());
}
public static class A {
int a;
}
public static class B extends A {
int b;
}
public static class C extends B {
int c;
}
}正如我们在前文介绍的,继承关系中,JVM会首先存放超类的字段。
info.victorchu.demos.jol.quickstart.JolCase03$C object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 int A.a N/A
16 4 int B.b N/A
20 4 int C.c N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total继承字段重排
public class JolCase04 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(C.class).toPrintable());
}
public static class A {
long a;
}
public static class B extends A {
int b;
}
public static class C extends B {
long c;
int d;
}
}如果子类首个成员变量是 long 或者 double 等 8 字节数据类型,而父类结束时没有 8 位对齐。会把子类的小于 8 字节的实例成员先排列,直到能 8 字节对齐。
info.victorchu.demos.jol.quickstart.JolCase04$C object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 (alignment/padding gap)
16 8 long A.a N/A
24 4 int B.b N/A
28 4 int C.d N/A
32 8 long C.c N/A
Instance size: 40 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total注意到上面对象头和字段中间存在 gap。在Java15后,对整个字段布局进行了大修,在这种情况下,会使用子类字段填充对象头。
// java 17
info.victorchu.demos.jol.quickstart.JolCase04$C object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 int B.b N/A
16 8 long A.a N/A
24 8 long C.c N/A
32 4 int C.d N/A
36 4 (object alignment gap)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total类继承gap
考虑下面这段代码:
public class JolCase05 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(C.class).toPrintable());
}
public static class A {
boolean a;
}
public static class B extends A {
boolean b;
}
public static class C extends B {
boolean c;
}
}由于A中的字段不足8byte,所以需要在类字段后面增加padding gap。
info.victorchu.demos.jol.quickstart.JolCase05$C object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 1 boolean A.a N/A
13 3 (alignment/padding gap)
16 1 boolean B.b N/A
17 3 (alignment/padding gap)
20 1 boolean C.c N/A
21 3 (object alignment gap)
Instance size: 24 bytes
Space losses: 6 bytes internal + 3 bytes external = 9 bytes total这个问题在Java15后,也被优化,在这种情况下,不会出现类字段之间的gap。
info.victorchu.demos.jol.quickstart.JolCase05$C object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 1 boolean A.a N/A
13 1 boolean B.b N/A
14 1 boolean C.c N/A
15 1 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total特殊类
public class JolCase06 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(Throwable.class).toPrintable());
System.out.println(ClassLayout.parseClass(Class.class).toPrintable());
}
}在JDK 8及以下版本中,可以看到一些关于Throwable 类不合理的内存空隙。如果查看Throwable类的源码,可以看到 Throwable.backtrace 字段,这个字段在内存dump中找不到。 这是因为该字段保存的是虚拟机内部的数据,是不允许用户访问的。
java.lang.Throwable object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 (alignment/padding gap)
16 4 java.lang.String Throwable.detailMessage N/A
20 4 java.lang.Throwable Throwable.cause N/A
24 4 java.lang.StackTraceElement[] Throwable.stackTrace N/A
28 4 java.util.List Throwable.suppressedExceptions N/A
Instance size: 32 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total但是在JDK9及其后续版本,这个字段是可见的。
java.lang.Throwable object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 java.lang.Object Throwable.backtrace N/A
16 4 java.lang.String Throwable.detailMessage N/A
20 4 java.lang.Throwable Throwable.cause N/A
24 4 java.lang.StackTraceElement[] Throwable.stackTrace N/A
28 4 java.util.List Throwable.suppressedExceptions N/A
32 4 int Throwable.depth N/A
36 4 (object alignment gap)
Instance size: 40 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes totalClass类的实例字段中有很大的gap,同时检查Class的java代码也找不到对应的java字段。这些并不是不可见字段,而是虚拟机"注入"的元信息。
java.lang.Class object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 java.lang.reflect.Constructor Class.cachedConstructor N/A
16 4 java.lang.Class Class.newInstanceCallerCache N/A
20 4 java.lang.String Class.name N/A
24 4 java.lang.Module Class.module N/A
28 4 (alignment/padding gap)
32 4 java.lang.String Class.packageName N/A
36 4 java.lang.Class Class.componentType N/A
40 4 java.lang.ref.SoftReference Class.reflectionData N/A
44 4 sun.reflect.generics.repository.ClassRepository Class.genericInfo N/A
48 4 java.lang.Object[] Class.enumConstants N/A
52 4 java.util.Map Class.enumConstantDirectory N/A
56 4 java.lang.Class.AnnotationData Class.annotationData N/A
60 4 sun.reflect.annotation.AnnotationType Class.annotationType N/A
64 4 java.lang.ClassValue.ClassValueMap Class.classValueMap N/A
68 28 (alignment/padding gap)
96 4 int Class.classRedefinedCount N/A
100 4 (object alignment gap)
Instance size: 104 bytes
Space losses: 32 bytes internal + 4 bytes external = 36 bytes total@Contended
java使用@sun.misc.Contended(JDK9后注解改成@jdk.internal.vm.annotation.Contended)注解解决CPU伪共享问题。使用@Contended注解需要关闭jvm选项-XX:-RestrictContended。
public class JolCase07 {
public static void main(String[] args) {
System.out.println(ClassLayout.parseClass(B.class).toPrintable());
}
public static class A {
int a;
int b;
@Contended
int c;
int d;
}
public static class B extends A {
int e;
@sun.misc.Contended("first")
int f;
@sun.misc.Contended("first")
int g;
@sun.misc.Contended("last")
int i;
@sun.misc.Contended("last")
int k;
}
}可以看到@Contended会在对象布局时产生128长度的padding gap(可以通过-XX:ContendedPaddingWidth设置gap长度)。且 f和g,i和k被放在了不同组中。
info.victorchu.demos.jol.quickstart.JolCase07$B object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) N/A
8 4 (object header: class) N/A
12 4 int A.a N/A
16 4 int A.b N/A
20 4 int A.d N/A
24 128 (alignment/padding gap)
152 4 int A.c N/A
156 128 (alignment/padding gap)
284 4 int B.e N/A
288 128 (alignment/padding gap)
416 4 int B.f N/A
420 4 int B.g N/A
424 128 (alignment/padding gap)
552 4 int B.i N/A
556 4 int B.k N/A
Instance size: 560 bytes
Space losses: 512 bytes internal + 0 bytes external = 512 bytes totalMarkWord
对象头中的mark words 中会存放锁信息,它是实现轻量级锁和偏向锁的关键。在申请锁然后再释放锁的过程中,我们可以清晰的看到 mark words 值的变化。
偏向锁
下面的例子用来说明偏向锁的变化情况:
- JDK 9之前偏向锁在JVM启动5秒后才可以使用。因此,在JDK 8及以下版本中运行本例的时候需要增加JVM运行参数
-XX:BiasedLockingStartupDelay=0。 - 从 JDK 15 之后偏向锁不再是默认的锁,所以在JDK 15 中运行本例需要增加JVM运行参数:
-XX:+UseBiasedLocking=。
public class JolMarkWord01 {
public static void main(String[] args) {
final A a = new A();
ClassLayout layout = ClassLayout.parseInstance(a);
System.out.println("**** Fresh object");
System.out.println(layout.toPrintable());
synchronized (a) {
System.out.println("**** With the lock");
System.out.println(layout.toPrintable());
}
System.out.println("**** After the lock");
System.out.println(layout.toPrintable());
}
public static class A {
// no fields
}
}日志如下:
**** Fresh object
info.victorchu.demos.jol.quickstart.JolMarkWord01$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
info.victorchu.demos.jol.quickstart.JolMarkWord01$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007ff91800c005 (biased: 0x0000001ffe460030; epoch: 0; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
info.victorchu.demos.jol.quickstart.JolMarkWord01$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007ff91800c005 (biased: 0x0000001ffe460030; epoch: 0; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total从输出结果可以看出:
- mark word 的值发生了变化(也就是VALUE字段的值),状态从biasable变到biased。
- 偏向锁释放时,不会修改markword。
轻量级锁
轻量级锁的目的是,在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。如果是 JDK 8 及以下版本直接运行本例即可,不需要增加任何JVM参数。如果是 JDK 9运行时需要添加参数:-XX:-UseBiasedLocking。
public class JolMarkWord02 {
public static void main(String[] args) {
final A a = new A();
ClassLayout layout = ClassLayout.parseInstance(a);
System.out.println("**** Fresh object");
System.out.println(layout.toPrintable());
synchronized (a) {
System.out.println("**** With the lock");
System.out.println(layout.toPrintable());
}
System.out.println("**** After the lock");
System.out.println(layout.toPrintable());
}
public static class A {
// no fields
}
}日志如下:
**** Fresh object
info.victorchu.demos.jol.quickstart.JolMarkWord02$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
info.victorchu.demos.jol.quickstart.JolMarkWord02$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fee771f9958 (thin lock: 0x00007fee771f9958)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
info.victorchu.demos.jol.quickstart.JolMarkWord02$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total从输出结果的 mark word 字段值可以看到对象头的初始状态为 “non-biasable”。当持有对象的锁时状态变为 “thin lock”。释放锁之后对象头的状态又恢复到初始状态。
重量级锁
当虚拟机检测到多线程竞争时,虚拟机就委托操作系统来实现互斥,也就是使用操作系统互斥量来实现锁的操作。为了模拟多线程竞争锁的场景,增加了一个竞争线程。
public class JolMarkWord03 {
public static void main(String[] args) throws Exception {
final A a = new A();
ClassLayout layout = ClassLayout.parseInstance(a);
System.out.println("**** Fresh object");
System.out.println(layout.toPrintable());
Thread t = new Thread(() -> {
synchronized (a) {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
// Do nothing
}
}
});
t.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("**** Before the lock");
System.out.println(layout.toPrintable());
synchronized (a) {
System.out.println("**** With the lock");
System.out.println(layout.toPrintable());
}
System.out.println("**** After the lock");
System.out.println(layout.toPrintable());
System.gc();
System.out.println("**** After System.gc()");
System.out.println(layout.toPrintable());
}
public static class A {
// no fields
}
}运行日志如下:
**** Fresh object
info.victorchu.demos.jol.quickstart.JolMarkWord03$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** Before the lock
info.victorchu.demos.jol.quickstart.JolMarkWord03$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fcf5c70a8e0 (thin lock: 0x00007fcf5c70a8e0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
info.victorchu.demos.jol.quickstart.JolMarkWord03$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fcf540049ea (fat lock: 0x00007fcf540049ea)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
info.victorchu.demos.jol.quickstart.JolMarkWord03$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fcf540049ea (fat lock: 0x00007fcf540049ea)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After System.gc()
info.victorchu.demos.jol.quickstart.JolMarkWord03$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000009 (non-biasable; age: 1)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total从输出结果可以看到,开始时对象头的状态是默认状态,当线程 t 持有对象锁后对象头中的锁信息变为轻量级锁。然后当主线程持有锁后,对象头的轻量级锁膨胀为重量级锁。膨胀后的锁状态在锁释放后会一直保持,只有当经过GC后才会恢复到初始状态。
在jdk15以后,markword中的重量级锁并不一定在GC后清除。当有足够多的monitor未被清理时,才会触发异步清理。
HashCode
hash code 存放在 mark word中。
public class JolMarkWord04 {
public static void main(String[] args) {
final A a = new A();
ClassLayout layout = ClassLayout.parseInstance(a);
System.out.println("**** Fresh object");
System.out.println(layout.toPrintable());
synchronized (a) {
System.out.println("**** With the lock");
System.out.println(layout.toPrintable());
}
System.out.println("**** After the lock");
System.out.println(layout.toPrintable());
System.out.println("hashCode: " + Integer.toHexString(a.hashCode()));
System.out.println(layout.toPrintable());
synchronized (a) {
System.out.println("**** With the second lock");
System.out.println(layout.toPrintable());
}
System.out.println("**** After the second lock");
System.out.println(layout.toPrintable());
}
public static class A {
// no fields
}
}从输出结果可以看到新创建的对象头中并没有生成 hash code 的值,当调用hashCode() 方法后才会生成 hash code 的值。且 生成的 hash code 值在对象的生命周期内不会再改变。即便被锁信息覆写,一旦锁被释放,已经计算好的hash code又会被会写。
**** Fresh object
info.victorchu.demos.jol.quickstart.JolMarkWord04$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
info.victorchu.demos.jol.quickstart.JolMarkWord04$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007f9b6c00c005 (biased: 0x0000001fe6db0030; epoch: 0; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
info.victorchu.demos.jol.quickstart.JolMarkWord04$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007f9b6c00c005 (biased: 0x0000001fe6db0030; epoch: 0; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
hashCode: 3fa77460
info.victorchu.demos.jol.quickstart.JolMarkWord04$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000003fa7746001 (hash: 0x3fa77460; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the second lock
info.victorchu.demos.jol.quickstart.JolMarkWord04$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007f9b71377950 (thin lock: 0x00007f9b71377950)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the second lock
info.victorchu.demos.jol.quickstart.JolMarkWord04$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000003fa7746001 (hash: 0x3fa77460; age: 0)
8 4 (object header: class) 0xf800c105
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total高级功能
对象引用
JOL 支持查看对象的所有引用信息。
public class JolAdvance01 {
public static void main(String[] args) {
ArrayList<Integer> al = new ArrayList<>();
LinkedList<Integer> ll = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
Integer io = i; // box once
al.add(io);
ll.add(io);
}
al.trimToSize();
PrintWriter pw = new PrintWriter(System.out);
pw.println(GraphLayout.parseInstance(al).toFootprint());
pw.println(GraphLayout.parseInstance(ll).toFootprint());
pw.println(GraphLayout.parseInstance(al, ll).toFootprint());
pw.close();
}
}上面的例子通过 ArrayList 和 LinkedList 来展示对象的引用信息。也可以同时查看多个对象的引用信息,但是 JOL 会避免重复计数,即两个对象中相同的部分只统计一次。
java.util.ArrayList@330bedb4d footprint:
COUNT AVG SUM DESCRIPTION
1 4016 4016 [Ljava.lang.Object;
1000 16 16000 java.lang.Integer
1 24 24 java.util.ArrayList
1002 20040 (total)
java.util.LinkedList@7d70d1b1d footprint:
COUNT AVG SUM DESCRIPTION
1000 16 16000 java.lang.Integer
1 32 32 java.util.LinkedList
1000 24 24000 java.util.LinkedList$Node
2001 40032 (total)
java.util.ArrayList@330bedb4d, java.util.LinkedList@7d70d1b1d footprint:
COUNT AVG SUM DESCRIPTION
1 4016 4016 [Ljava.lang.Object;
1000 16 16000 java.lang.Integer
1 24 24 java.util.ArrayList
1 32 32 java.util.LinkedList
1000 24 24000 java.util.LinkedList$Node
2003 44072 (total)对象的内存地址
为了触发GC,运行时堆大小要小于1G(-Xmx256M -Xms256M),可以打开GC日志(-XX:+PrintGCDetails)方便对比。
public class JolAdvance02 {
public static void main(String[] args) {
PrintWriter pw = new PrintWriter(System.out, true);
long last = VM.current().addressOf(new Object());
for (int l = 0; l < 1000 * 1000 * 1000; l++) {
long current = VM.current().addressOf(new Object());
long distance = Math.abs(current - last);
if (distance > 4096) {
pw.printf("Jumping from %x to %x (distance = %d bytes, %dK, %dM)%n",
last,
current,
distance,
distance / 1024,
distance / 1024 / 1024);
}
last = current;
}
pw.close();
}
}
// Jumping from ffefffd8 to f5580000从输出结果可以看到地址发生了较大距离的切换。结合GC日志,可以得知这是GC导致的。
对象晋升
每次 GC 之后存活对象的年龄都会增长,当增长到一定阈值后就会从年轻代晋升到老年代。我们可以通过JOL 观察对象的年龄和观察对象晋升的过程(主要是通过对象地址的变化来确定,因为每次 GC 都会改变对象的存储位置)。
为了触发GC,运行时堆大小要小于1G(-Xmx256M -Xms256M),可以打开GC日志(-XX:+PrintGCDetails)方便对比。
public class JolAdvance03 {
static volatile Object sink;
public static void main(String[] args) {
PrintWriter pw = new PrintWriter(System.out, true);
Object o = new Object();
ClassLayout layout = ClassLayout.parseInstance(o);
long lastAddr = VM.current().addressOf(o);
pw.printf("*** Fresh object is at %x%n", lastAddr);
System.out.println(layout.toPrintable());
int moves = 0;
for (int i = 0; i < 100000; i++) {
long cur = VM.current().addressOf(o);
if (cur != lastAddr) {
moves++;
pw.printf("*** Move %2d, object is at %x%n", moves, cur);
System.out.println(layout.toPrintable());
lastAddr = cur;
}
// make garbage
for (int c = 0; c < 10000; c++) {
sink = new Object();
}
}
long finalAddr = VM.current().addressOf(o);
pw.printf("*** Final object is at %x%n", finalAddr);
System.out.println(layout.toPrintable());
pw.close();
}
}*** Fresh object is at f567fe30
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0x200001ed
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*** Move 1, object is at fd620178
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000009 (non-biasable; age: 1)
8 4 (object header: class) 0x200001ed
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
*** Move 2, object is at feb18a80
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000011 (non-biasable; age: 2)
8 4 (object header: class) 0x200001ed
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total从输出结果可以看到对象的地址更改多次,每次对象的地址都不同,同时可以看到 JOL 输出的对象头中的 age 会随着增加。
GC对数组的影响
无论选择哪种 GC 数组中初始的元素都是按照索引顺序存储的。但是如果我们选择并行GC,那么当发生 GC 后数组的元素就是按照倒序存储(建议参数-XX:ParallelGCThreads=1,如果是多线程,结果是混乱的)。
/**
* @see https://bugs.openjdk.java.net/browse/JDK-8024394
*/
public class JolAdvance04 {
public static void main(String[] args) {
PrintWriter pw = new PrintWriter(System.out, true);
Integer[] arr = new Integer[10];
for (int i = 0; i < 10; i++) {
arr[i] = i + 256; // boxing outside of Integer cache
}
String last = null;
for (int c = 0; c < 100; c++) {
String current = GraphLayout.parseInstance((Object) arr).toPrintable();
if (last == null || !last.equalsIgnoreCase(current)) {
pw.println(current);
last = current;
}
// 显式触发GC
System.gc();
}
pw.close();
}
}从输出结果可以看到第一次数组的元素是按照索引顺序存储的,后续都是倒序存储。从本例也可以看出,垃圾收集器也可以影响数组对象在内存中的存储结构。
[Ljava.lang.Integer;@330bedb4d object externals:
ADDRESS SIZE TYPE PATH VALUE
71567fc40 56 [Ljava.lang.Integer; [256, 257, 258, 259, 260, 261, 262, 263, 264, 265]
71567fc78 16 java.lang.Integer [0] 256
71567fc88 16 java.lang.Integer [1] 257
71567fc98 16 java.lang.Integer [2] 258
71567fca8 16 java.lang.Integer [3] 259
71567fcb8 16 java.lang.Integer [4] 260
71567fcc8 16 java.lang.Integer [5] 261
71567fcd8 16 java.lang.Integer [6] 262
71567fce8 16 java.lang.Integer [7] 263
71567fcf8 16 java.lang.Integer [8] 264
71567fd08 16 java.lang.Integer [9] 265
Addresses are stable after 1 tries.
[Ljava.lang.Integer;@330bedb4d object externals:
ADDRESS SIZE TYPE PATH VALUE
5c001a9a0 56 [Ljava.lang.Integer; [256, 257, 258, 259, 260, 261, 262, 263, 264, 265]
5c001a9d8 12080 (something else) (somewhere else) (something else)
5c001d908 16 java.lang.Integer [9] 265
5c001d918 16 java.lang.Integer [8] 264
5c001d928 16 java.lang.Integer [7] 263
5c001d938 16 java.lang.Integer [6] 262
5c001d948 16 java.lang.Integer [5] 261
5c001d958 16 java.lang.Integer [4] 260
5c001d968 16 java.lang.Integer [3] 259
5c001d978 16 java.lang.Integer [2] 258
5c001d988 16 java.lang.Integer [1] 257
5c001d998 16 java.lang.Integer [0] 256
Addresses are stable after 1 tries.
[Ljava.lang.Integer;@330bedb4d object externals:
ADDRESS SIZE TYPE PATH VALUE
5c001a950 56 [Ljava.lang.Integer; [256, 257, 258, 259, 260, 261, 262, 263, 264, 265]
5c001a988 9728 (something else) (somewhere else) (something else)
5c001cf88 16 java.lang.Integer [9] 265
5c001cf98 16 java.lang.Integer [8] 264
5c001cfa8 16 java.lang.Integer [7] 263
5c001cfb8 16 java.lang.Integer [6] 262
5c001cfc8 16 java.lang.Integer [5] 261
5c001cfd8 16 java.lang.Integer [4] 260
5c001cfe8 16 java.lang.Integer [3] 259
5c001cff8 16 java.lang.Integer [2] 258
5c001d008 16 java.lang.Integer [1] 257
5c001d018 16 java.lang.Integer [0] 256
Addresses are stable after 1 tries.原理
Instrumentation
使用java.lang.instrument.Instrumentation.getObjectSize()方法,可以很方便的计算任何一个运行时对象的大小(Shadow heap size),返回该对象本身在内存中的大小。不过,我们在代码中无法直接实例化它,需要在JVM启动时,通过指定代理的方式,让JVM来实例化它。其原理是通过反射,累加计算 Retained heap size。
unsafe
java中的sun.misc.Unsafe类,有一个objectFieldOffset(Field f)方法,表示获取指定字段在所在实例中的起始地址偏移量,如此可以计算出指定的对象中每个字段的偏移量,值为最大的那个就是最后一个字段的首地址,加上该字段的实际大小,就能知道该对象整体的大小。
参考资料
相关内容
如果你觉得这篇文章对你有所帮助,请我一杯咖啡吧~
微信支付
支付宝
