2020-11-02 一个java对象到底能吃多少内存?欢迎关注微信公众号《程序猿崛起》有问题找我

对象及其生命周期

一)对象的内存布局

在HotSpot虚拟机里,对象在堆内存中存储布局可分为三部分,即对象头(Header) 、 实例数据(Instance Data)、对齐填充(Padding)。

1)对象头

对象头包括:

(1)Mark World :用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。synchronized锁升级就依赖锁标志、偏向线程等锁信息,垃圾回收新生代对象转移到老年代则依赖于GC分代年龄。在32位系统上占4个字节,在64位系统上占8个字节。如下图是32位系统Mark World

64位系统如下图所示

虽然他们在不同系统的长度不一样,但是基本组成内容是一致的。

  • 锁标志位(lock) : 区分锁状态,11时表示对象待GC回收状态,只有最后2位标识有效

  • biased_lock : 是否偏向锁,由于正常锁和偏向锁的锁标识都是01,没办法区分,这里引入一位的偏向锁标识位。

  • 分代年龄 (age) : 表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。

  • 对象的hashcode(hash) : 运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里,当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。

  • 偏向锁的线程ID (java Thread) : 偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID,在后面的操作中,就无需再进行尝试获取锁的动作。

  • epoch : 偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁ptr_to_lock_record : 轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题中设置指向锁记录的指针。

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。

(2)Class Pointer (类型指针):用来指向对象对应的Class对象(其对应的元数据对象)的内存地址,JAVA虚拟机通过这个指针来确定该对象是那个类的实例,在32位系统上占4个字节,在64位系统上占8个字节。

(3)Length: 如果对象是数组对象,length的值就表示数组的长度,在32位系统占4字节,在64位系统占8字节。

注意,在64位虚拟机中,为了节约内存可以使用选项-XX:+UseCompressedOops开启指针压缩,默认是开启指针压缩的,也可使用-XX:-UseCompressedOops关闭指针。开启指针压缩后,实际上会压缩的对象包括:每个Class的属性指针(静态成员变量)及每个引用类型的字段(包括数组)指针,而本地变量,堆栈元素,入参,返回值,NULL这些指针不会被压缩。

另外,值得注意的是对象头是理解JVM中对象存储方式最核心的部分,甚至是理解多线程、分代GC、锁等理论的基础,也是窥探JVM底层诸多实现细节的出发点,后面会详细整理出来。

2)实例数据(Instance Data):

是对象真正存储的有效信息,即对象的各个字段数据,无论是从父类继承下来的,还是在子类中定义的字段都必须记录下来。具体如下:

Primitive Type (主要类型)Memory Required(bytes) (所需字节)
byte1
char2
short2
int4
long8
float4
double8
boolean1
reference32位系统占4个字节,64位占8个字节,数组也是引用对象

3)对齐填充(Padding):

对齐填充仅仅起着占位符的作用,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被设计成正好是8字节的倍数,因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。并不是都在实例数据之后补,有可能会在实例数据项之间补全,当有父类成员变量被子类继承,初始子类对象时,有可能会在父类最后一个成员变量之后补全,下面会有案例。

4)图示

二)验证无父类对象占用内存大小

1)编写测试代码

package com.yangbaopeng.concurrency.test.jvm;
​
import org.apache.lucene.util.RamUsageEstimator;
​
/**
 * 使用第三方工具类RamUsageEstimator,所在maven坐标如下
 * <dependency>
 *          <groupId>org.apache.lucene</groupId>
 *          <artifactId>lucene-core</artifactId>
 *          <version>4.0.0</version>
 * </dependency>
 * RamUsageEstimator.shallowSizeOf(Object obj) 是计算指定对象本身在堆内存中所占字节大小
 * RamUsageEstimator.sizeOf(Object obj) 计算指定对象及其引用树上的所有对象的综合大小,单位字节
 * @author yangbp
 * @date 20201101
 */
public class TestObjectSize {
    private String name = new String("hello");
​
    private String place = "world";
​
    private static String degree = "undergraduate";
​
    private int count = 100;
​
    private char value[] ;
​
    private long distance = 1000L;
​
    private float money = 1000.01F;
​
    private Object object = new Object();
​
    public static void main(String[] args) {
        TestObjectSize testObjectSize = new TestObjectSize();
        System.out.println("对象本身在堆内存中所占字节大小====" + RamUsageEstimator.shallowSizeOf(testObjectSize));
        System.out.println("打印对象及其引用树上的所有对象的综合大小===" + RamUsageEstimator.sizeOf(testObjectSize));
    }
}

2)修改TestObjectSize配置

 

这行参数是为了关闭HotSpot指针压缩,我本机用的HotSpot是64位的。

 

3)运行结果并分析

testObjectSize对象本身的内存大小:对象头( mark world(8字节) + class pointer(8字节) + length(0字节) )+实例数据( String name(引用类型,故8字节) + String place (引用类型,故8字节) + static String degree(类变量,存储在方法区,故0字节)+ int(4字节)+ value (数组类型,故8字节) + long(8字节) + float(4字节)+Object(8字节)= 64字节(因为64是8的倍数,所以对齐填充0字节),即创建一个testObjectSize对象大约需要64字节。

testObjectSize对象及其引用树上的所有的综合大小: testObjectSize对象本身的内存大小 + String nam对象本身及其引用链的综合大小 + String place对象本身及其引用链的综合大小 + value数组对象本身的大小 + object对象本身的大小 = X;

String nam对象本身的大小: 对象头( mark world (8字节) + class pointer (8字节) + length(0字节) )+ 实例数据( int (4字节) + value数组1(8字节))= 32 字节 (因32为8的倍数,故对齐填充为0字节),即String name对象的内存大小为32字节。

value数组1对象本身的大小:对象头( mark world (8字节) + class pointer (8字节) + length(8字节) )+实例数据( 5 * 2)= 34 字节(因34不是8的倍数,40为8的倍数)故 value数组对象本身的大小为40字节。

String place对象本身的大小: 对象头( mark world (8字节) + class pointer (8字节) + length(0字节) )+ 实例数据( int (4字节) + value数组2(8字节))= 32 字节 (因32为8的倍数,故对齐填充为0字节),即String place对象的内存大小为32字节。

value数组2对象本身的大小:对象头( mark world (8字节) + class pointer (8字节) + length(8字节) )+实例数据( 5 * 2)= 34 字节(因34不是8的倍数,40为8的倍数)故 value数组对象本身的大小为40字节。

char value[] 数组并未初始化,不牵涉其他引用对象。

object对象本身的大小: 对象头( mark world (8字节) + class pointer (8字节) + length(0字节) )+ 实例数据( 0字节)= 16 字节 (因16为8的倍数,故对齐填充为0字节);即object对象的内存大小为16字节。

即 X = 64 + 32 + 40 + 32 + 40 + 16 = 224字节,故testObjectSize对象及其引用树上的所有的综合大小为224个字节。

配置-XX:+UseCompressedOops,开启指针压缩后

testObjectSize对象的内存大小:对象头( mark world(8字节) + class pointer(4字节) + length(0字节) )+实例数据( String name(引用类型,故4字节) + String place (引用类型,故4字节) + static String degree(类变量,存储在方法区,故0字节)+ int(4字节)+ value (数组类型,故4字节) + long(8字节) + float(4字节)+Object(4字节)== 44字节(因为44不是8的倍数,48是8的倍数,所以对齐填充4字节),即创建一个testObjectSize对象大约需要48字节。

testObjectSize对象及其引用树上的所有的综合大小: testObjectSize对象本身的内存大小 + String nam对象本身及其引用链的综合大小 + String place对象本身及其引用链的综合大小 + value数组对象本身的大小 + object对象本身的大小 = X;

String nam对象本身的大小: 对象头( mark world (8字节) + class pointer (4字节) + length(0字节) )+ 实例数据( int (4字节) + value数组1(4字节))= 20 字节 (因20不是8的倍数,故对齐填充为4字节),即 String nam对象本身的大小为24字节。

value数组1对象本身的大小:对象头( mark world (8字节) + class pointer (4字节) + length(4字节) )+实例数据( 5 * 2)= 26 字节(因26不是8的倍数,32为8的倍数)故 value数组对象本身的大小为32字节。 

String place对象本身的大小: 对象头( mark world (8字节) + class pointer (4字节) + length(0字节) )+ 实例数据( int (4字节) + value数组2(4字节))= 20 字节 (因20不是8的倍数,故对齐填充为4字节),即String nam对象本身的大小为24字节。

value数组2对象本身的大小:对象头( mark world (8字节) + class pointer (4字节) + length(4字节) )+实例数据( 5 * 2)= 26 字节(因26不是8的倍数,32为8的倍数)故 value数组对象本身的大小为32字节。

char value[] 数组并未初始化,不牵涉其他引用对象。

object对象本身的大小: 对象头( mark world (8字节) + class pointer (4节) + length(0字节) )+ 实例数据( 0字节)= 12字节 (因12为8的倍数,故对齐填充为4字节),即object对象的大小为16字节。

即 X = 48 + 24 + 32 + 24 + 32 + 16 = 176字节,故testObjectSize对象及其引用树上的所有的综合大小为176个字节。

运行结果如下

三)验证有父类成员变量时对象占用内存大小

1)编写测试代码

package com.yangbaopeng.concurrency.test.jvm;

import org.apache.lucene.util.RamUsageEstimator;

/**
 * 验证有父类成员变量时对象的内存大小
 * @author yangbp
 * @date 20201101
 */
public class XiaoMing extends people{

    private boolean married = false;
    private long birthday = 128902093242L;
    private char tag = 'c';
    private double sallary = 1200.00d;
    private Object object;

    public static void main(String[] args) {
        XiaoMing xiaoMing = new XiaoMing();
        System.out.println("打印对象所占字节大小============"+RamUsageEstimator.shallowSizeOf(xiaoMing));
        System.out.println("打印对象及其引用树上的所有对象的综合大小==="+RamUsageEstimator.sizeOf(xiaoMing));
    }
}

class people{

    private int age;
    private String name;

}

2)运行结果

1)关闭压缩

    首先我们看到XiaoMing这个类继承自Person类,故xiaoMing这个对象也拥有了int age、String name及其自身的成员变量。在关闭指针压缩的情况下,

xiaoMing对象所占字节大小:对象头( mark world(8字节) + class pointer(8字节) + length(0字节) )+实例数据( int age(4字节) + String name(引用类型,故8字节) + 第一次Padding(4字节)+ boolean(1字节)+ long(8字节) + char(2字节) + double(8字节)+Object(8字节)== 59字节(因为59不是8的倍数,64是8的倍数,所以第二次对齐填充4字节),即创建一个xiaoMing对象大约需要64字节。

注意JVM有条规则就是:父类的最后一个成员变量与子类第一个成员变量间隔必须大于4个字节,因为name和married之间间隔为0字节,所以才有了第一次对齐填充4个字节,开启指针压缩也是一样。

因为xiaoMing对象不管是继承的引用变量还是本身拥有的引用变量没有被赋值或new出来,故xiaoMing对象引用链上只有它自己。所以xiaoMing对象及其引用树上的所有对象的综合大小也是64字节。结果如下

2)开启压缩

    xiaoMing对象所占字节大小:对象头( mark world(8字节) + class pointer(4字节) + length(0字节) )+实例数据( int age(4字节) + String name(引用类型,故4字节) + 第一次Padding(4字节)+ boolean(1字节)+ long(8字节) + char(2字节) + double(8字节)+Object(4字节)== 47字节(因为47不是8的倍数,48是8的倍数,所以第二次对齐填充1字节),即创建一个xiaoMing对象大约需要48节。

因为xiaoMing对象不管是继承的引用变量还是本身拥有的引用变量没有被赋值或new出来,故xiaoMing对象引用链上只有它自己。所以xiaoMing对象及其引用树上的所有对象的综合大小也是48字节,结果如下

四 ) 对象的创建方式

实例化一个类有四种途径:

1)明确使用new操作符

2)调用Class或者java.lang.reflect.Construct对象的newInstance()方法

3) 调用任何现有对象的clone()方法

4) 通过java.io.ObjectInPutStream类的getObject()方法序列化。

五 对象实例化的过程

(1) 当虚拟机要实例化一个对象时,首先会从方法区中的常量池找这个类的符号引用,并检查这个符号引用所代表的类有没有被虚拟机已经加载、解析和初始化过。如果没有,则会先进行相应类的类加载过程。

(2)如果在类的加载检查通过后,虚拟机将会为新生对象分配内存,为对象在堆区分配一块确定大小的空间。

(3)内存分配完成后,虚拟机会先将对象的内存空间(不包括对象头)都初始化为零值,这步操作是为了保证对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问这些字段的数据类型所对应的零值。

(4)接下来,虚拟机就会对对象头进行必要的设置,例如对象的类型信息、元数据地址、对象的哈希码、对象的GC分代年龄等信息,也会根据虚拟机当前运行状态的不同,决定是否启用偏向锁等。

(5)最后开始执行对象的构造函数,即Class文件中的<init>方法,按照开发人员的意图对对象进行初始化。

六 对象的访问定位

我们都知道,一个对象在堆区创建完成后,这个引用变量会压入栈中,即一个reference,它指向堆区对象的地址,这个引用定位的方式有两种:使用句柄访问对象和直接访问对象。

1)通过句柄访问对象

 

如上图所示,使用句柄访问的话,Java堆中可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。这样访问好处是:reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象)时只会改变句柄中的实例数据指针,而reference本身不需要改变。

1)通过直接指针访问对象

如上图所示,如果使用直接指针访问的话,明显速度更快,因为reference中存储的直接是对象地址,比使用句柄访问少一次间接访问的开销。HotSpot虚拟机主要就是使用这种方式进行对象访问。

七 思考

现在我们已经会估算一个对象所占字节的大小,但在实际场景中,这个数值还应该区扩大10~20倍,这样以来,一个对象所需的空间会更大,假如我有一个对象60字节,扩大20倍,也就大概1M,假如我堆区设定了256M,那这个对象只能创建256个,如果程序某些时刻可以达到每分钟200次的请求,每次请求耗时300ms,那你想你这个堆发生垃圾回收的频类是有多快?JVM应该如何设置才能尽可能不影响客户体验。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值