一、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、热点代码缓冲区