探索JVM底层之调优实战、对象内存布局、指针压缩

本文深入探讨了Java对象在JVM中的内存布局,包括对象头、实例数据、对齐填充等,并通过实例展示了对象大小的计算。同时,详细解释了指针压缩的原理和作用,以及其对最大堆空间的影响。最后,文章提出了JVM调优的策略,并结合亿级流量系统场景分析了如何进行JVM堆区设置和调优。
摘要由CSDN通过智能技术生成

一、Oop

在这里插入图片描述
oop模型:Java中的对象在JVM中的存在形式;

typeArrayKlass:存放基本数据类型数组的元信息
objArrayKlass:存放引用类型

在这里插入图片描述

二、对象的内存布局

在这里插入图片描述

对象头中包括:markword、类型指针、数组长度。

1、markword: 主要用来表示对象的线程锁状态,另外还可以用来配合GC、存放该对象的hashCode;
64bit 占8B,32bit占4B
在这里插入图片描述

2、类型指针:对象所属的类的元信息的实例指针,instanceKlass在方法区的地址。它所占的大小和是否开启指针压缩有关,如果开启了指针压缩,占用4个字节,如果关闭了占用8个字节;

3、数组长度:如果这个对象不是数组,占0字节,如果是数组,占4字节。其实可以根据这个4字节算出来一个数组最多可以存多少个元素;2的32次方- 1 个。为什么数组长度占4个字节呢?因为是用int存储这个数组大小的

4、实例数据:类的非静态属性,生成对象时就是实例数据,即对象属性;

boolean 1B
byte 1B
char 2B
short 2B
int 4B
float 4B
double 8B
long 8B

引用类型:
开启指针压缩:4B
关闭指针压缩:8B

5、对其填充:Java中所有的对象大小都是8字节对齐。16B,24B,32B… 如果一个对象大小是30字节,JVM底层会补2字节(对齐填充),凑层32字节,达到8字节对齐。

为什么需要对齐填充(可能原因):为了更好的写程序,性能更高

对象的内存布局,其实还有另外一种情况:
在这里插入图片描述

上面的西乡出现了两次对齐填充;

三、计算对象大小

1、没实例数据对象

import org.openjdk.jol.info.ClassLayout;

public class CountEmptyObjectSize {
    public static void main(String[] args) {
        CountEmptyObjectSize objectSize = new CountEmptyObjectSize();

        // 查看对象大小 需要jol-core包
        System.out.println(ClassLayout.parseInstance(objectSize).toPrintable());

    }
}

com.jihu.test.oop.CountEmptyObjectSize 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


Process finished with exit code 0

开启指针压缩:16B = 8B(markword) + 4B(类型指针) + 0B(数组长度) + 0B(实例数据) + 4B(对齐填充)

关闭指针压缩:16B = 8 + 8 + 0 + 0 + 0;

2、普通对象

import org.openjdk.jol.info.ClassLayout;

public class CountObjectSize {

    int a = 10;
    int b = 20;

    public static void main(String[] args) {
        CountObjectSize objectSize = new CountObjectSize();

        // 查看对象大小 需要jol-core包
        System.out.println(ClassLayout.parseInstance(objectSize).toPrintable());

    }
}

com.jihu.test.oop.CountObjectSize 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 CountObjectSize.a                         10
     16     4    int CountObjectSize.b                         20
     20     4        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

开启指针压缩:24 = 8(md) + 4(类型指针) + 0(数组长度) + 4*2(实例数据) + 4(对齐填充)

关闭指针压缩:24 = 8 + 8 + 0 + 4*2 +0;

为什么要有指针压缩
开启指针压缩后,不仅可以节省空间,寻址也会更搞笑。因为占用的内存少了,需要检索的总内容自然也少了。

3、数组对象

import org.openjdk.jol.info.ClassLayout;

public class CountArraySize {

    static int[] arr = {0, 1, 2};

    public static void main(String[] args) {
        CountArraySize objectSize = new CountArraySize();

        // 查看对象大小 需要jol-core包
        System.out.println(ClassLayout.parseInstance(arr).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)                           03 00 00 00 (00000011 00000000 00000000 00000000) (3)
     16    12    int [I.<elements>                             N/A
     28     4        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

开启指针压缩:32B = 8 (markword)+4(类型指针) + 4(数组长度) + 4*3 (数组中三个int)+ 4(填充)

关闭指针压缩:40B = 8 (markword)+8(类型指针) + 4(头部填充) + 4(数组长度) + 4*3 (数组中三个int)+ 4(尾部填充

**数组对象在关闭指针压缩的情况下出现两端填充,头部会多一次对齐填充**

在这里插入图片描述

四、指针压缩

1、为什么要指针压缩
为了节省内存,指针占用的内存小了,寻址更高效

2、实现原理
两个信息:
1、java中所有的对象都是8字节对齐的

8字节对齐有什么规律?

所有对象的指针后三位永远是0

2、测试数据如下

我们假设对象从0地址开始存储

test1 = 16B
test2 = 24B
test3 = 32B

-----------------
test1 = 00 000
test2 = 16(十进制转化成二进制)10 000
test3 = 40(十进制转成二进制为) 101 000

原理:
因为Java中的对象都是8字节对齐的,即对象的指针后三位永远是0,在存储的时候,会将后三位的0删除后存储,这样整个内存的大小明显的降低了。

1、存储的时候,后三位0抹除

test1 = 00
test2 = 16(十进制转化成二进制)100
test3 = 40(十进制转成二进制为) 101

2、使用的时候,后三位补3个0

test1 = 00 [000]
test2 = 16(十进制转化成二进制)10 [000]
test3 = 40(十进制转成二进制为) 101 [000]

public class CountArraySize {

    int[] arr = {0, 1, 2};

    public static void main(String[] args) {
        CountArraySize objectSize = new CountArraySize();
        while(true);
    }

开启指针压缩:
在这里插入图片描述
使用HSDB可以看到metadata_cimpressd_klass,说明开启了指针压缩;

问题:一个oop能表示的最大堆空间是多少?

35 = 32bit +3bit

一个oop存储的时候是3B(字节),32bit(位),
使用的时候,尾部补了三个0,总共35bit

即指针最大只有35位,
2的35次方是32G,即OOP的最大堆空间是32G。

问题:如果32G不够用了,如何扩容?
16字节对齐:

此时要求java中的所有对象都是16字节对齐的
8字节对齐:
1111…1111(32个) 000 -> 32G

16字节对齐后(末尾增加一个0,即增加一位):
1111…1111(32个) 0000 -> 32G * 2 = 64G

问题:那么这个修改扩容是对JVM进行修改呢还是对操作系统修改呢
需要修改JVM源码,即对JVM进行二次开发。淘宝的taoVM就是二次开发了JVM。

问题:jdk底层为什么没用16字节对齐?
1、没必要,32G已经很大了
2、现在的GC算法处理32G已经是极限了,如果过大,则可能导致GC时间为几个小时甚至无法继续提供服务。

所以说如果需要发展更大内存,本质应该是CPU的发展,GC算法是比较耗费CPU资源的。

五、JVM调优

1、项目未上线前预估调优
2、项目上线初期,基于日志做一些基础调优
3、发生OOM、频繁full gc做彻底的调优

1、调优类型
1、JVM内存模型调优
2、热点代码缓冲区的调优

2、案例实战:亿级流量系统调优

这里以亿级流量秒杀电商系统为例:
1、如果每个用户平均访问20个商品详情页,那访客数约等于500w(一亿 / 20)
2、如果按转化率10%来算,那日均订单约等于50w(500w * 10%)
3、如果30%的订单是在秒杀前两分钟完成的,那么每秒产生1200笔订单(50w * 30% / 120s)
4、订单支付又涉及到发起支付流程、物流、优惠券、推荐、积分等环节,导致产生大量对象,这里我们假设整个支付流程生成的对象约等于20K,那每秒在Eden区生成的对象约等于20M(1200笔 * 20K)
5、在生产环境中,订单模块还涉及到百万商家查询订单、改价、包邮、发货等其他操作,又会产生大量对象,我们放大10倍,即每秒在Eden区生成的对象约等于200M(其实这里就是在大并发时刻可以考虑服务降级的地方,架构其实就是取舍)
这里的假设数据都是大部分电商系统的通用概率,是有一定代表性的。
如果你作为这个系统的架构师,面对这样的场景,你会如何做JVM调优呢?即将运行该系统的JVM堆区设置成多大呢?

假设现在服务器是32G:
在这里插入图片描述
最大堆内存=32G*(1/4) = 8G
新生代:8*1/3 = 2.7G
Edan区:2.2G
From: 0.27G
To: 0.27G

没秒钟产生200M对象
用户下单到支付总共需要3s
那么每秒有600M进入到Edan区因为这600M对象还在用,是存活对象,gc时不会被释放

此时新生代默认分为了 2700Mb,
那么此时我们的系统多长时间后会发生young gc?
每14s(14s = 2700 / 200)进行一次young gc

gc的时候会进行垃圾回收,但是会有600M对象无法被回收。所以这600M对象会触发空间担保,会直接进入老年代;

老年代总共大约有5400M,多久会触发一次full gc呢?
9 * 14s = 126s
即每14s有600M对象进入到老年代

所以说有的系统频繁的进行full gc,就是因为有的对象没有在young gc时被清理干净,出发了空间担保、动态年龄判断或者15次gc,进入了老年代

如果说我们是这个系统的架构师,如何调优呢?


我们调优的原则是尽可能少的减少full gc,那么上述的600M对象就应该在young gc的时候就被清理掉。

六、那些地方需要调优

1、方法区

2、虚拟机栈

3、堆区

4、热点代码缓冲区

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值