布局、定位、分配"/>
对象的创建过程、内存布局、定位、分配
对象的创建过程
-
当Java虚拟机遇到一条字节码new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否加载、解析和初始化过。
-
如果没有,先将class loading到内存
-
linking过程:verification校验,preparation把类的静态变量设默认值,resolution将符号引用解析成直接引用
-
intializing初始化,把类的静态变量设为初始值并执行静态代码块
-
分配内存
指针碰撞(Serial、ParNew等带Compact过程的收集器)
假设Java内存是完全规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点指示器,那所分配内存就是把指针向空闲空间那边移动一段与对象大小相等的距离。
空闲列表(使用CMS基于Mark-Sweep算法的收集器)
如果Java堆中的内存并不规整,已使用的内存和空闲的内存相互交错,虚拟机就必须维护一个列表,记录那些内存块可用,在分配的时候在列表上找到一块足够大的空间划分给对象实例,并更新列表记录。
-
成员变量赋默认值
-
设置对象头中的信息
-
调用构造函数
<init>
-
执行super()语句
-
成员变量顺序赋初始值
-
执行构造函数方法语句
-
对象的内存布局
对象在堆内存中的存储布局可以划分为三个部分
- 对象头(Header)由Mark Word和ClassPointer组成
- 实例数据(Instance Data)
- 对齐填充(Padding)
普通对象的内存布局
- 对象头中的mark word 存储自身的运行时数据 长度是8个字节
- 对象头中的类型指针(ClassPointer),即对象指向它的类型元数据的指针 ,-XX+UseCompressedClassPointer为4个字节,不开启为8个字节
- 实例数据,引用类型 -XX:+UseCompressedOops为4个字节 不开启为8个字节 Oops(Ordinary Object Pointers)
- Padding对齐,是8的倍数
数组对象的内存布局
- 对象头中的mark word如上
- 对象头中的类型指针(ClassPointer)如上
- 数组长度 4个字节
- 数组数据
- Padding对齐,是8的倍数
下面写一个例子观察下Object的大小,利用java的agent机制
- 新建项目ObjectSize(jdk1.8)
- 创建文件ObjectSizeAgent,利用Agent,jvm虚拟机在把class load到内存,在load内存中可以有一个Agent代理,可以截获class文件,并可以进行修改,在这里我们主要是将Object的大小读出来
package com.changyy.agent;import java.lang.instrument.Instrumentation;/*** @author changyy* @Description:* @date 2020/11/22 22:25*/
public class ObjectSizeAgent {/** java内部字节码处理调试Instrumentation*/private static Instrumentation inst;/*** 必须要有premain函数参数也是固定的,第二个是Instrumentation,这个是虚拟机调用的,它会帮助我们初始化Instrumentation,所以在这里我们给成员变量赋值,虚拟机调用的时候Instrumentation这个时候就拿到了** @param agentArgs* @param _inst*/public static void premain(String agentArgs, Instrumentation _inst) {inst = _inst;}// 调用Instrumentation的getObjectSize方法public static long sizeof(Object o) {return inst.getObjectSize(o);}
}
- src目录下创建META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: changyy
Premain-Class: com.changyy.agent.ObjectSizeAgent
如果是maven需要在pom配置中追加自定义行到MANIFEST.MF
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.0.2</version><configuration><archive><manifestEntries><!--Premain-Class为key,这个<Premain-Class>中的值为value --><Premain-Class>com.changyy.agent.ObjectSizeAgent</Premain-Class><Can-Redefine-Classes>true</Can-Redefine-Classes></manifestEntries></archive></configuration></plugin></plugins></build>
- 打成jar包 此时jar名
ObjectSize-1.0-SNAPSHOT.jar
- 在需要使用该Agent jar的项目引入该jar包
project structure -> project setting -> library 添加该jar包
- 运行时需要该Agent jar的类,加入参数
-javaagent:F:\project\learning-notes\ObjectSize\target\ObjectSize-1.0-SNAPSHOT.jar
- 测试类
public class TestObjectSize {public static void main(String[] args) {System.out.println(ObjectSizeAgent.sizeof(new Object()));System.out.println(ObjectSizeAgent.sizeof(new int[]{}));System.out.println(ObjectSizeAgent.sizeof(new P()));}private static class P {int id; //4个字节String name; //4个字节int age; //4个字节byte b1; //1个字节byte b2; //1个字节Object o; //4个字节byte b3; //1个字节}
}
输出结果
16
16
32
cmd 命令提示符查看UseCompressedClassPointer和UseCompressedOops 是开启的
C:\Users\admin>java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=266121280 -XX:MaxHeapSize=4257940480 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_171"
Java(TM) SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.171-b11, mixed mode)
Object是16个字节,Mark Word8个字节,ClassPointer由于压缩指针是开启的,把8个字节压缩成4个字节,对象头占8+4=12个字节
再对齐Padding是8的倍数,所以分配的内存是16个字节
int数组,对象头与普通对象的一样是8+4=12个字节,数组长度是4个字节,共16个字节
对象P,对象头8+4=12个字节,实例数据,由于UseCompressedOops 参数开启,引用类型参数是4个字节,int(4*2)+String(4)+byte(1+4)+Object(4)=19个字节,19+12+对齐=32个字节
自己可以测试下类P再加个引用类型成员变量,P就会变成19+4+12+对齐=40个字节。
Hotspot开启内存压缩的规则(64位机)
- 4G以下,直接砍掉高32位
- 4G-32G,默认开启内存压缩ClassPointer Oops
- 32G,压缩无效,使用64位
对象头
32位虚拟机Mark Word的存储结构
64位虚拟机Mark Word的存储结构
-
锁标志位带包对象的锁状态
-
GC的分代年龄:在使用PS垃圾回收器,GC年龄默认是15,然后进入老年带,为什么是默认15,就是因为分代年龄是4个字节
-
synchronized锁升级的过程中有一个偏向锁,一个对象被一个线程锁定,它只偏向这个线程,这个线程下次再来不需要加锁,所以它有一个偏向锁,严格上讲它有3位来代表锁,
-
hashcode:hashcode是指identity hash code如果不是identity hash code就不会存在mark word中,”以内存计算的HashCode的方式“为“identity hash code”。所以其实未覆盖
Object
类的hashCode()
方法也被称为“identity hash code”,按原始内容计算的hashcode存在这里,重写过的hashcode方法计算的记过不会存在这里,如果对象没有重写过hashcode方法,那么默认是调用os::random
产生hashcode们可以通过System.identityHashCode
获取,os::random产生hashcode的规则为next_rand=(16807seed)mod(2*31-1)
,因此可以使用31位存储,一旦生成hashcode,jvm会将其记录在markword中。 -
什么时候产生hashcode?当然是调用未重写的hashcode()方法及System.identityHashCode的时候
-
有锁的情况
- 轻量级锁和重量级锁都会在线程的栈里面创建一块专门的空间Displaced Mark Word,用于在获得锁的时候,复制“锁”的对象头里面的Mark Word内容,把当前的线程ID写进Mark Word;而在释放锁的时候,再从Displaced Mark Word复制回锁的Mark Word里面。
- 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态;当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量级锁;
对象定位
对象定位有两种:句柄和直接指针
- 如果使用句柄的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息
- 如果使用直接指针的话,Java堆中对象的内存分布就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次简介访问的开销
使用句柄的好处是reference中存储的是稳定句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改,使用直接指针访问最大的好处是速度更快,节省一次指针定位的时间开销,
对象分配
对象分配过程:new一个对象先往栈上分配,如果未发生栈上逃逸且栈能分配下就在栈上分配,如果发生栈上逃逸,并且对象特别的话,直接分配到堆内存老年代,如果不大,首先会在本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)上分配,如果本地缓冲区分配不下,就分配在Eden上,然后经过多重GC,GC过程年龄到了就直接到老年代。
更多推荐
对象的创建过程、内存布局、定位、分配
发布评论