java中我们随处可见的都是对象,而对象成为我们与计算机内核交换的主要载体,使用起来也非常简单,然而一个对象是如何被JVM创建的却是极其的复杂,它要经历类加载机制、分片内存以及设置对象头的内存布局。
下面讲介绍下Hotspot JVM下新建对象需要基本过程。
new语句最会被编译而成的字节码,而它将包含用来请求内存指令。
public class ObjectSizeMain{
public static void main(String []args){
ObjectSizeMain obj=new ObjectSizeMain();
}
}
C:\Users\Administrator\Desktop>javap -v ObjectSizeMain.class
Classfile /C:/Users/Administrator/Desktop/ObjectSizeMain.class
Last modified 2019-6-1; size 290 bytes
MD5 checksum 1635999ab99de05314d8568fbaaf6900
Compiled from "ObjectSizeMain.java"
public class ObjectSizeMain
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."":()V
#2 = Class #14 // ObjectSizeMain
#3 = Methodref #2.#13 // ObjectSizeMain."":()V
#4 = Class #15 // java/lang/Object
#5 = Utf8
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 ObjectSizeMain.java
#13 = NameAndType #5:#6 // "":()V
#14 = Utf8 ObjectSizeMain
#15 = Utf8 java/lang/Object
{
public ObjectSizeMain();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class ObjectSizeMain
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "ObjectSizeMain.java"
重点关注下红色部分内容,它透露了一些信息:
关于字节码可以参阅《实战JAVA虚拟机 JVM故障诊断与性能优化》中class文件结构的章节
JVM首先检查一个new指令的参数是否能在常量池中定位到一个符号引用,并且检查该符号引用代表的类是否已被加载、解析和初始化过(实际上就是在检查new的对象所属的类是否已经执行过类加载机制)。如果没有,先进行类加载机制加载类。可参阅你忽略的ClassLoader。
简单理解为:确定对象内存大小并从Java堆中划分出来
内存分配两种方式
Java堆内存是否规整,取决于GC收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的,然后选择对应的方式。
指针碰撞 | 适用场合:堆内存规整(即没有内存碎片)的情况下 原理:用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可 GC收集器:Serial、ParNew |
空闲列表 | 适用场合:堆内存不规整的情况下 原理:虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例(这一块儿可以类比memcached的slab模型),最后更新列表记录。 GC收集器:CMS |
内存分配并发问题
堆内存是各个线程的共享区域,所以在操作堆内存的时候,需要处理并发问题。处理的方式有两种:
CAS+失败重试 | 做法与AtomicInteger的getAndSet(int newValue)方法的实现方式类似 public final boolean compareAndSet(int expect, int update) { |
TLAB (Thread Local Allocation Buffer) |
原理:为每一个线程预先在Eden区分配一块儿内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配 -XX:+/-UseTLAB:是否使用TLAB -XX:TLABWasteTargetPercent:设置TLAB可占用的Eden区的比率,默认为1% -XX:PrintTLAB:查看TLAB的使用情况 JVM会根据以下三个条件来给每个线程分配合适大小的TLAB: 1.-XX:TLABWasteTargetPercent 2.线程数量 3.线程是否频繁分配对象 |
对象在内存中存储的布局分为三块
对象头 |
存储对象自身的运行时数据:Mark Word(在32bit和64bit虚拟机上长度分别为32bit和64bit),包含如下信息:
-XX:+UseCompressedOops:JDK 8下默认为启用,启用对象的指针压缩,节约内存占用的大小 -XX:+CompactFields:JDK 8下默认为启用,分配一个非static的字段在前面字段缝隙中,提高内存的利用率 -XX:FieldsAllocationStyle=1:JDK 8下默认值为‘1’,实例对象中有效信息的存储顺序
注:Mark Word具有非固定的数据结构,以便在极小的空间内存储尽量多的信息。如果对象是一个数组,对象头必须有一块儿用于记录数组长度的数据。JVM可以通过Java对象的元数据确定对象长度,但是对于数组不行。
在java中对象的每个成员属性都有一个offset,通过UnsafeUtils.unsafe().objectFieldOffset可以获得。但分配内存是有些差别,64位系统中CPU一次读操作可读取64bit(8 bytes)的数据,读取属性long不能读取2次(如果从对象头开始就会发生),因此会打破存储顺序(FieldsAllocationStyle)。字段重排列技术指的是重新分配字段的先后顺序,以达到内存对齐的目的。 |
实例数据 | 对象真正存储的有效信息 |
对齐填充 | HotSpot VM的自动内存管理系统要求对象大小必须是8字节的整数倍,对齐填充没有特别的含义,它仅仅起着占位符的作用 |
为对象的字段赋值(这里会根据所写程序给实例赋值)
public class ObjectSize_test {
/**
* 启动参数中添加: -javaagent:D:\Program\repository\classmexer\classmexer\0.03\classmexer-0.03.jar
*/
@Test
public void showObjSize() {
LongInstace v = new LongInstace();
//24=12+8+2+2=对象头+long+byte+padding(2),不压缩的话就是16+8+2...
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(v));
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(v));
LongInstace2 v2 = new LongInstace2();
//16=12+4=对象头+oops
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(v2));
//40=16+24=LongInstace2+Long
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(v2));
IntegerInstace1 v3 = new IntegerInstace1();
//16=12+4=对象头+oops
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(v3));
//32=16+16=LongInstace3+Integer,有压缩
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(v3));
LongInstace[]vs=new LongInstace[2];
vs[0]=v;
System.out.printf("shallow size: %s.byte\n", MemoryUtil.memoryUsageOf(vs));
System.out.printf("retained size: %s.byte\n", MemoryUtil.deepMemoryUsageOf(vs));
System.out.printf("first item offset : %s,per item size: %s \n",UnsafeUtils.unsafe().arrayBaseOffset(vs.getClass()),UnsafeUtils.unsafe().arrayIndexScale(LongInstace[].class));
ComplexInstance obj = new ComplexInstance();
//48=12+1+2+2+4+4+8+8+padding(7)
System.out.println("ComplexInstance:" + MemoryUtil.memoryUsageOf(obj));
}
@Test
public void showLayout() throws NoSuchFieldException {
/**
* 无填充
* 内存布局:对象头(12) + oops(4)
*/
long offset = UnsafeUtils.unsafe().objectFieldOffset(IntegerInstace1.class.getDeclaredField("value"));
//12=对象头(12) 开始
System.out.printf("IntegerInstace1.value(Integer): %s \n", offset);
/**
* 有填充
* 规则:64位系统中CPU一次读操作可读取64bit(8 bytes)的数据,读取属性long不能读取2次(如果从对象头开始就会发生)
*/
offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace1.class.getDeclaredField("value"));
//16=对象头(12)+padding(4)
System.out.printf("LongInstace1.value(long): %s \n", offset);
offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value2"));
//12=对象头(12) -XX:+CompactFields生效
System.out.printf("LongInstace.value2(byte): %s \n", offset);
offset = UnsafeUtils.unsafe().objectFieldOffset(LongInstace.class.getDeclaredField("value3"));
//13=对象头(12)+value2 -XX:+CompactFields生效
System.out.printf("LongInstace.value3(char): %s \n", offset);
/**
* 内部有多个成员变量时,布局按-XX:FieldsAllocationStyle=1(longs/doubles、ints/floats、shorts/chars、bytes/booleans)
* double=24=对象头(12)+int(4)+long(8),double和long是有变量定义的顺序决定的,不行你可以试试
* 48=对象头(12)+int(4)+long(8)+double(8)+float(4)+char(2)+short(2)+boolean(1)+padding(7)
*/
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("bool"));
System.out.printf("ComplexInstance.bool(boolean): %s \n", offset);//40
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("cha"));
System.out.printf("ComplexInstance.cha(char): %s \n", offset);//36
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("sho"));
System.out.printf("ComplexInstance.sho(short): %s \n", offset);//38
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("in"));
System.out.printf("ComplexInstance.in(int): %s \n", offset);//12
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("flo"));
System.out.printf("ComplexInstance.flo(float): %s \n", offset);//32
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("lon"));
System.out.printf("ComplexInstance.lon(long): %s \n", offset);//16
offset = UnsafeUtils.unsafe().objectFieldOffset(ComplexInstance.class.getDeclaredField("dou"));
System.out.printf("ComplexInstance.dou(double): %s \n", offset);//24
}
public final static class LongInstace {
protected long value = 0L;
byte value2;
boolean value3;
}
public final static class LongInstace1 {
protected long value = 0L;
}
public final static class LongInstace2 {
Long value = 0L;
}
public final static class IntegerInstace1 {
Integer value = 0;
}
public final static class ComplexInstance {
boolean bool;
char cha;
short sho;
int in;
float flo;
long lon;
double dou;
}
}