对象创建时内存分配
从归属区分:
归属线程的:虚拟机栈、本地方法栈、pc计数器
归属jvm的:堆、方法区
从功能区分:
保存对象实例数据:堆
保存类的数据:方法区
保存方法变量:虚拟机栈
保存本地方法变量:本地方法栈
保存线程执行位置:pc计数器
ps:jdk8以前,HotSpot通常用永久代来作为方法区的实现,其内存大小在启动时确定,虽然gc会处理这里的垃圾,但是当加载过多的类时,还是会出现oom。
在jdk8以后的版本中,元空间取代了永久代,元空间开辟在本地内存(不在虚拟机中,直接使用服务器内存),理论上不会出现内存不足,并且将原先永久带中的字符串常量移到堆中,其他元数据(类元、字段、静态属性、方法、常量等)移动到元空间。
1 内存管理
new:我们很多时候,使用new来创建对象,new是强类型校验,他能调动所有的构造函数,当机器遇到new指令的时候,先检查类是否已经被加载,如果没有,那么先进行类加载。加载完毕,则开始在堆中分配内存。
对象的内存我理解这里分为三个部分(也可将后两者放一起,毕竟都在堆中,也都属于对象):
在方法区中分配对象元数据,类元、字段、静态属性、方法、常量等
在堆中为对象分配对象头信息内存
初始化对象在堆中分配对象的实例数据内存。
2 类加载时分配对象元数据区
在java8之后,元空间取代perm(永久代)作为元数据所在地,并将常量池移动到堆中。
在类加载过程“加载”阶段,虚拟机将class文件用二进制流方式读取到方法区。
在方法区中生成Class对象以保存类信息
分配类变量(与之相比较的叫实例变量)内存,static修饰的变量、final域中的变量赋值、字段表、常量池等等。
注:由于数组时没有对象信息的,所以无法通过类加载器来加载数组(类加载器通过加载class文件获得类信息),所以数组是由虚拟机而不是类加载器创建的。
数组是一串连续的内存地址....大小一旦分配就无法更改。
3 对象头信息数据区
对象保存在堆中,对象头信息包括2部分:对象标记(Mark Word) 和 类型指针
该数据一定是在堆中,属于对象的。
对象的运行时信息,记做Mark Word。
此对象头信息记录如下类型
存储内容
偏向锁标志位(biased_lock)
标志位(lock)
状态
对象哈希码、对象分代年龄
0
01
未锁定
指向锁记录的指针
0
00
轻量级锁定
指向重量级锁的指针
0
10
膨胀(重量级锁定)
空,不需要记录信息
0
11
GC标记
偏向线程ID、偏向时间戳、对象分代年龄
1
01
偏向锁
其一是对象的各种记录:
所谓运行时信息,值的是这个对象在运行过程中需要保存的信息,既然是运行时才记录,说明随着对象的运行而在不断变化。
例如:对象分代年龄,对象每在一次Minor GC中存活,将年龄增加1,并在适当时候(默认年龄15,可设置)移动到“老年代”。
其二是对象的锁状态:
偏向锁标志位(biased_lock)+ 标志位(lock)决定了对象的锁状态。
该部分内存在32位和64位系统中,分别占用32bit和64bit内存
32位系统中:
25bit储存对象哈希码(identity_hashcode)
4bit储存对象年龄分代(age)
1bit储存偏向锁标志位(biased_lock)
2bit储存锁标志位(lock)
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
64位系统中
关于类型指针(class pointer)
这部分占用4个字节(如果不开启指针压缩,则占8个字节,此处取4个字节)
明显就是在对象头中指向方法区中class元数据的指针,通过此指针,我们可以通过对象来获取到对象的class信息。毕竟class是在方法区中,而对象是在堆中,如果没有指针关联是不科学的,而且由于一个class可以有多个对象实例,所以肯定是由对象指向class
这里是不是很明显,如何计算一个对象的大小??
在64位机器上,对象头信息包含64bit(8个字节)的对象运行时信息(Mark Word ),还有类型指针(class pointer)4个字节。那么就算没有任何实例需要赋值,一个对象的创建最少需要12个字节!但由于HotSpot VM要求对象初始化大小必须是8的倍数,所以实际需要16个字节
4 对象的实例数据区
在类加载“初始化”后,如果对象需要初始化赋值,那么需要执行init方法,该方法将为对象赋值实例数据
该内存一定是在堆内存区,也是属于对象的。
如果对象是引用,那么只需要保存实例的引用,一个引用占用4个字节。
如果是基本类型,则根据对象类型保存对应长度
5 填充区域
由于HotSpot VM要求对象初始化大小必须是8的倍数,所以有这个要求而已。
6 图解对象内存分配
7 实例:计算对象内存大小
/**
* @Author: dhcao
* @Version: 1.0
*/
public class OneObj {
// 1. 对象头占用12个字节
// 2. 这个在方法区,不包含在对象中,占0个字节
private static int a = 10;
// 3. 3个int共占12个字节
int a1;
int a2;
int a3;
// 4. 2个refObj共占8个字节
Object b1;
Object b2;
// 5. 此处也只算引用,只占8个字节。ps:一个Integer对象大小是16个字节...
Integer o1 = new Integer(91);
Integer o2 = new Integer(98);
// 所以共占:12 + 12 + 8 + 8 = 40 。正好是8的倍数,所以就占40个字节。
}
// 测试
/**
* @Author: dhcao
* @Version: 1.0
*/
public class SizeTest {
public static void main(String[] args) {
OneObj obj = new OneObj();
while(true){
}
}
}
按照我们的预想,这个是40个字节没问题了,我启动jprofile监控看看就知道了
如果我们增加一个对象呢
// 5. 此处也只算引用,占了12个字节。ps:一个Integer对象大小是16个字节...
Integer o1 = new Integer(91);
Integer o2 = new Integer(98);
Integer o3 = new Integer(98);
// 此时比上面测试多了一个引用o3,那么应该是44个字节,进行补齐,应为48字节