背景简化:最近由于项目需要,需要计算一下对象的大小,防止放开灰度后导致服务期频繁GC
一个Java对象在内存中包括对象头、实例数据和补齐填充3个部分
由于本文主要是讲述对象的大小计算,所以不会详细讲解每个部分的作用,有兴趣可以上网搜索一些相关文章阅读。
Mark Word 用于存储对象自身的运行时数据
如哈希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程ID 、偏向时间戳等
这部分数据的长度在32 位和64 位的虚拟机中分别为32 bit 和64 bit
用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。
在32位系统占4字节,在64位系统中占8字节;
64位机中开启指针压缩占4字节
注意是klass 而不是class
许多文章都是class pointer可参考HotSpotGlossary
如果是数组对象,还有一个保存数组长度的空间,占4个字节
如果是数组对象且未开启指针压缩则还会存在一个padding用来对齐
这个部分很多文章都省略了
但确实存在
对象真正存储的有效的信息,程序代码中定义的各种的数据的类型
如果有继承的关系,还有继承父类的字段。
分配策略(参数FiedsAllocationStyle)影响java中定义的顺序,对相同宽度的字段总是被分配到一起,在这种情况下,父类定义的变量会出现在子类之前。CompactFields 为true (默认为true) 子类中较窄的变量也可能插入到父类变量中。
HostSpot 的默认分配策略为
Type | Bytes |
---|---|
boolean | 1 |
byte | 1 |
short | 2 |
char | 2 |
int | 4 |
float | 4 |
long | 8 |
double | 8 |
在32位系统占4字节,在64位系统中占8字节
由于 HostSpot VM 的自动内存管理系统要求对象的起始地址必须是8字节的整数倍
换句话说对象的大小必须为8字节的整数倍,要是实例数据没有对齐,则需要进行对齐填充来补全
以8字节对齐还是16字节对齐可以配置
这部分没有特殊意义填充0值
64位的JVM支持 -XX:+UseCompressedOops 来开启指针压缩功能 1.6 后默认开启
启用CompressOops后会压缩的对象:
1、每个Class的属性指针(静态成员变量)
2、每个对象的属性指针
3、普通对象数组的每个元素指针
当然,压缩也不是所有的指针都会压缩,对一些特殊类型的指针,JVM是不会优化的
例如指向PermGen的Class对象指针、本地变量、堆栈元素、入参、返回值和NULL指针不会被压缩。
java对象默认按8字节对齐
假设内存中只有三个对象 t1 = 16字节 t2 = 32字节 t3 = 24字节
再假设分配内存是从0开始分配 则三个对象的内存地址为
实现原理为存储
的时候后三位0
抹除 0X00 0X10 0X30``````使用
的时候后三位补0
实际就是一个编码和解码的过程,针对指针压缩也有一些优化,如零基压缩,由于本文是希望尽可能简单的将压缩的实现原理,所以不再这篇文章赘述,感兴趣的可以搜一些相关文章查看
一个oop所能表示的最大的内存空间是多少? 2的35次方 = 32G
为什么是 35呢? 开启指针压缩后 一个oop的大小是4字节 = 32 位 再加上取出后 后三位会补充0 所以是 32+3 =35
怎么样扩大oop的最大的内存空间呢?
改为16 /32 或者更大的 字节对齐 但是这样做的会导致空间的浪费没有必要
有了上边的基础,我们再来进行对象大小的计算
public class EmptyTuan {
public static void main(String[] args) {
//使用jol计算对象的大小(单位为字节):
System.out.println(ClassLayout.parseClass(EmptyTuan.class).toPrintable());
//使用jol查看对象的内存布局:
System.out.println(ClassLayout.parseInstance(new EmptyTuan()).toPrintable());
}
}
org.learn.code.jvm.classsize.EmptyTuan 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
16 = 8 (mark word) + 4 (klass Pointer) + 0 (Instance Data) + 4 (padding)
由于按8字节对齐所以浪费了4个字节
-XX:-UseCompressedOops
org.learn.code.jvm.classsize.EmptyTuan 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) 28 e0 15 12 (00101000 11100000 00010101 00010010) (303423528)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
通过对象的内存布局可以看到
16 = 8(mark word)+ 8(klass Pointer) + 0 (Instance Data) + 0 (padding)
因为关闭指针压缩后OOP的大小为8字节 并且正好是8的整数倍,所以不用填充,没有空间浪费
public class FullTuan {
int a = 10;
long b = 20L;
public static void main(String[] args) {
//使用jol计算对象的大小(单位为字节):
System.out.println(ClassLayout.parseClass(FullTuan.class).toPrintable());
//使用jol查看对象的内存布局:
System.out.println(ClassLayout.parseInstance(new FullTuan()).toPrintable());
}
}
先来计算一下 ?
8(Mark Word) + 4(Klass Pointer) + 4(Instance Data) + 8(Instance Data) =24
org.learn.code.jvm.classsize.FullTuan 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 int FullTuan.a 10
16 8 long FullTuan.b 20
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
先来计算一下
8(Mark Word) + 8(Klass Pointer) + 4(Instance Data) + 8(Instance Data)+ 4 (Padding) = 32
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) 28 e0 3c 10 (00101000 11100000 00111100 00010000) (272425000)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 8 long FullTuan.b 20
24 4 int FullTuan.a 10
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
前边说的是普通对象,记得在基础知识中提到过数组对象的对象头会多出来两部分数据
public class ArrayTuan {
/**
* 数组对象
*/
private static int[] array = {1, 2, 3,4};
public static void main(String[] args) {
//使用jol查看对象的内存布局:
System.out.println(ClassLayout.parseInstance(array).toPrintable());
}
}
[I 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) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 04 00 00 00 (00000100 00000000 00000000 00000000) (4)
16 16 int [I. N/A
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
对象头中又多出来4字节
[I 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) 68 0b dc 09 (01101000 00001011 11011100 00001001) (165415784)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 4 (object header) 04 00 00 00 (00000100 00000000 00000000 00000000) (4)
20 4 (alignment/padding gap)
24 16 int [I. N/A
Instance size: 40 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
4 bytes internal
证明是在对象头内部填充了4字节
public class ParentTuan {
private int a = 0;
private long b = 10;
}
public class SonTuan extends ParentTuan {
private String s = "123";
public static void main(String[] args) {
//使用jol计算对象的大小(单位为字节):
System.out.println(ClassLayout.parseClass(SonTuan.class).toPrintable());
//使用jol查看对象的内存布局:
System.out.println(ClassLayout.parseInstance(new SonTuan()).toPrintable());
}
}
验证一下私有属性是否可以被继承过去
org.learn.code.jvm.classsize.SonTuan 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int ParentTuan.a 0
16 8 long ParentTuan.b 10
24 4 java.lang.String SonTuan.s (object)
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
看完内存布局,有没有好奇他们的顺序是怎么排的呢?
-XX:FieldsAllocationStyle=1
(JDK 8下默认值为‘1’)
改变这个的参数可以改变实例对象中有效信息的存储顺序:
0:先放入oops(普通对象引用指针),然后在放入基本变量类型(顺序:longs/doubles、ints、shorts/chars、bytes/booleans)
1:先放入基本变量类型(顺序:longs/doubles、ints、shorts/chars、bytes/booleans),然后放入oops(普通对象引用指针)
2:oops和基本变量类型交叉存储
-XX:FieldsAllocationStyle=0
org.learn.code.jvm.classsize.SonTuan 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int ParentTuan.a 0
16 8 long ParentTuan.b 10
24 4 java.lang.String SonTuan.s (object)
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
没有变化 难道是因为父类的属性是 private 原因吗?修改一下 在进行验证
public class ParentTuan {
int a = 0;
private long b = 10;
}
再次打印内存布局
org.learn.code.jvm.classsize.SonTuan 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int ParentTuan.a 0
16 8 long ParentTuan.b 10
24 4 java.lang.String SonTuan.s (object)
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
还是没有变化 那是不是只能修改子类的顺序呢?先修改子类的结构如下
public class SonTuan extends ParentTuan {
private String s = "123";
int c = 10;
public static void main(String[] args) {
//使用jol计算对象的大小(单位为字节):
// System.out.println(ClassLayout.parseClass(SonTuan.class).toPrintable());
//使用jol查看对象的内存布局:
System.out.println(ClassLayout.parseInstance(new SonTuan()).toPrintable());
}
}
再次打印对象布局
org.learn.code.jvm.classsize.SonTuan 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 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
12 4 int ParentTuan.a 0
16 8 long ParentTuan.b 10
24 4 java.lang.String SonTuan.s (object)
28 4 int SonTuan.c 10
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
发现是我们期望的结果,所以这个参数是修改不了父类和子类属性的顺序的
org.learn.code.jvm.classsize.SonTuan 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) 20 e4 d1 11 (00100000 11100100 11010001 00010001) (298968096)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 8 long ParentTuan.b 10
24 4 int ParentTuan.a 0
28 4 (alignment/padding gap)
32 8 java.lang.String SonTuan.s (object)
Instance size: 40 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
经过前边的事例计算,相信大家已经对计算对象大小的方法有所掌握,但是现在为止,我们还没有在类中加入过静态变量,下面就让我们看一下有静态变量的对象的大小是怎么计算的。
public class StaticTuan {
private int a = 1;
private long b = 2;
private String s = null;
private static EmptyTuan emptyTuan = null;
public static void main(String[] args) {
//使用jol计算对象的大小(单位为字节):
System.out.println(ClassLayout.parseClass(StaticTuan.class).toPrintable());
//使用jol查看对象的内存布局:
// System.out.println(ClassLayout.parseInstance(new StaticTuan()).toPrintable());
}
}
静态属性的大小是不计算在对象里面的
org.learn.code.jvm.classsize.StaticTuan 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 int StaticTuan.a 1
16 8 long StaticTuan.b 2
24 4 java.lang.String StaticTuan.s null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
org.learn.code.jvm.classsize.StaticTuan 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) 80 f0 d8 0c (10000000 11110000 11011000 00001100) (215543936)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
16 8 long StaticTuan.b 2
24 4 int StaticTuan.a 1
28 4 (alignment/padding gap)
32 8 java.lang.String StaticTuan.s null
Instance size: 40 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
看完上边内容,我们是不是就可以愉快的去预估项目中内存的占用了呢?
我先写个例子:
public class MemoryAllocateTuan {
public static void main(String[] args) {
System.out.println("开始执行");
for (int i = 0; i < 1000; i++) {
new MemoryAllocateTuan();
}
System.out.println("执行结束");
}
}
如上对象是没有任何属性的,所以在开启指针压缩的情况下 大小=16字节
现在我们限制对内存大小为 5M 运行 1000 次 16*1000/1024=15M 应该报内存溢出 那我们执行一下看是不是我们期望的结果
开始执行
执行结束 time =0
Process finished with exit code 0
并没有出现内存溢出问题 再次修改代码 使我们可以dump内存
public class MemoryAllocateTuan {
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
try {
System.out.println("开始执行");
for (int i = 0; i < 1000; i++) {
new MemoryAllocateTuan();
}
System.in.read();
} finally {
System.out.println("执行结束 time =" + (start - System.currentTimeMillis()));
}
}
通过 jconsole 查看内存分配
从上图可以看到,堆基本没有分配内存。那对象是在哪产生的呢?
在《深入理解Java虚拟机中》关于Java堆内存有这样一段描述:
但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么绝对
了。
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
public StringBuffer craeteSB(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,称为线程逃逸或者方法返回值逃逸。
public StringBuffer craeteSB(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
1、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
2、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3、标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。用标量来替代聚合量
*注意hotSpot虚拟机没有真正实现栈上分配而是依赖标量替换*
再回过头来想一下刚才的例子为什么没有oom,应该是开启了逃逸分析,为了验证,我们可以来实验一下
关闭逃逸分析执行后的结果
[Full GC (Ergonomics) Exception in thread "main" 5083K->5076K(5632K), 0.0260039 secs]
[Full GC (Ergonomics) 5082K->5077K(5632K), 0.0131476 secs]
[Full GC (Ergonomics) 5082K->5079K(5632K), 0.0127679 secs]
[Full GC (Ergonomics) 5082K->5069K(5632K), 0.0135017 secs]
[Full GC (Ergonomics) 5082K->5072K(5632K), 0.0212518 secs]
[Full GC (Ergonomics) 5082K->5073K(5632K), 0.0145557 secs]
java.lang.OutOfMemoryError: GC overhead limit exceeded
可以看到对象现在是在堆上分配,因为没有足够的内存直接OOM
好了到现在为止,关于计算对象内存大小的问题都讲完了。上边主要计算的是当前对象的大小,如果当前对象包含引用变量,这些引用变量对应的大小是没有计算的,但是在实际项目中也是需要计算的
接下来我们做个练习
Map emptyTuans = new HashMap();
如上,创建100W个EmptyTuan 加入到 emptyTuans 这时总堆占多大内存
首先先看一下一个Integer对象占多大内存:
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int Integer.value N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
一个Integer占16字节
通过上边计算得到一个 EmptyTuan 对象占16字节
再来计算一个hashMap占多少字节
java.util.HashMap object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 java.util.Set AbstractMap.keySet N/A
16 4 java.util.Collection AbstractMap.values N/A
20 4 int HashMap.size N/A
24 4 int HashMap.modCount N/A
28 4 int HashMap.threshold N/A
32 4 float HashMap.loadFactor N/A
36 4 java.util.HashMap.Node[] HashMap.table N/A
40 4 java.util.Set HashMap.entrySet N/A
44 4 (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
通过计算是48字节
计算一个 Node
的大小
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
}
大小=12(头)+16(data)+4(padding)=32字节
忽略其他因素:总内存大小 (100W*(16+16+32)+48)/1024/1024= 61M
所以大约需要61M左右
通过 jvisualvm
看一下内存
发现和我们预估的基本一致。
再回过头来想一下,我开篇说到的是否都理解了。
注:本文运行环境是 jdk1.8