JVM性能调优
- 1. 对象的创建
- 1.1 类加载检查
- 1.2 分配内存
- 1.3 初始化
- 1.4 设置对象头
- 1.HotSpot虚拟机的对象头包括三部分信息:Mark Word、Klass Pointer类型指针、数组长度
- 1.5 执行<init>方法
- 2. 对象大小与指针压缩
- 2.1 什么是java对象的指针压缩?
- 2.2 为什么要进行指针压缩
- 3. 对象内存分配
- 3.1 对象内存分配流程图
本文是按照自己的理解进行笔记总结,如有不正确的地方,还望大佬多多指点纠正,勿喷。
课程内容:
1、JVM对象创建过程详解
2、对象头与指针压缩详解
3、JVM对象内存分配详解
4、逃逸分析&栈上分配&标量替换详解
5、对象内存回收机制详解
6、日均百万级订单交易系统JVM参数设置实例
1. 对象的创建
对象创建的主要流程:
1.1 类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
1.2 分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
1.如何划分内存。
⒉.在并发情况下,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
-
指针碰撞(默认使用指针碰撞):如果java堆内存是绝对规整的,那么会把所有用过的内存放在一边,空闲的内存放在另外一边,中间用一个指针来作为分界点的指示器,那所分配的内存仅仅把那个指针空闲空间的挪动一段与对象大小相同的距离。
-
空闲列表(Free List):如果java堆内存不是绝对规整的,已使用的空间和未使用的空间互相交错,那么虚拟机维护一份列表,记录哪些内存块是可用的,在划分内存空间的时候从列表中找到一块足够大的内存空间分配给对象实例,并更新列表上的记录。
解决并发问题的方法:
-
CAS (compare and swap):虚拟机采用
失败重试的机制方式
保证操作的原子性对分配内存空间的动作进行同步处理,第一个线程抢占到了分配空间,第二个线程没有抢占到就重试抢占后面一块内存空间。大概意思就是谁抢上谁用,抢不上的继续抢。
-
本地线程分配缓冲(Thread Local Allocation Buffer,TLAB):把内存分配的动作按照线程分配在不同的空间之中完成,
也就是每个线程在java堆中预先分配出一块小的内存
。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用(JVM默认开启-XX:+UseTLAB) ,-XX:TLABSize指定TLAB大小,默认是Eden区的百分之1,放不下就走CAS
1.3 初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
1.4 设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域对象头(Header) 、实例数据(Instance Data)和对齐填充(Padding)
。
1.HotSpot虚拟机的对象头包括三部分信息:Mark Word、Klass Pointer类型指针、数组长度
-
Mark Word标记字段(32位 4字节 ,64位占8字节)用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
-
对象头的另外一部分是类型指针(Klass Point 开启压缩占4字节,关闭压缩占8字节),并不是Class ,我们使用的对象的getClass方法的那个Class对象是在堆内存而这个是类的元数据信息 。即对象指向它类的元数据的指针,元数据信息是放在方法区之中,虚拟机通过这个指针来确定这个对象是那个类的实例,类的元数据信息是放在C++的对象来承载的
类型指针:一个对象new出来时放在堆里面的,但是在这个对象的头部区域有一个指针,指向方法区的这个对象所属的类的内元素信息。类的元素信息是加载到这个方法区的。比如下面这个对象。要找到这个compute方法在元素区的代码就是通过这个类型指针去找。
下面圈主的就是类指针
其实还有一个东西,这个东西叫类对象,这个对象有一个类对象,这个math类所属的对象,这个类对象是放在堆里面的。
那这个类对象与类元信息有什么区别?
这整个类里面乱七八杂的代码是放在方法区的,mathclass可以理解为类装载之后,JVM内部给java开发人员访问这个类的信息。通过这个mathclass类区访问,通过反射可以拿到各种信息。JVM内部使用的头部类型指针去拿的,他的底层是使用c++来实现的。而开发人员是通过那个mathclass去拿的。
mathclass里面是不放内容的,内容都在方法区,他就是一个定向。
下面这个图是一个32的图
1.5 执行方法
执行方法,即对象按照程序员的意愿进行初始化。对应到语言层面上讲,就是为属性赋值(注意,这与上面的赋零值不同这是由程序员赋的值)和执行构造方法。
2. 对象大小与指针压缩
对象大小可以用jol-core包查看,引入依赖。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
package ding;
import org.openjdk.jol.info.ClassLayout;
public class JOLSample {
public static void main(String[] args) {
ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
System.out.println();
ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
System.out.println(layout1.toPrintable());
System.out.println();
ClassLayout layout2 = ClassLayout.parseInstance(new A());
System.out.println(layout2.toPrintable());
}
/*
-XX:+UseCompressedOops 默认开启的压缩所有的指针
-XX:+UseCompressedClassPointers 默认开启的只压缩对象头里的类型指针Klass Pointer
Oops:Ordinary Object Pointers
*/
public static class A{
//8B mark word
//4B Klass Pointer 如果关闭压缩-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,
int id; //4B
String name; //4B如果关闭压缩-XX:-UseCompressedOops,则占用8B
byte b; //1B
Object o; //4B如果关闭压缩-XX:-UseCompressedOops,则占用8B
}
}
运行结果分析:
2.1 什么是java对象的指针压缩?
-
jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
-
jvm配置参数:UseCompressedOops, compressed–压缩、 oop(ordinary object pointer)–对象指针
-
启用指针压缩;-XX:+UseCompressedOops(
默认开启
),禁止指针压缩:-XX:-UseCompressedOops
-XX:+UseCompressedOops 默认开启的压缩所有的指针
-XX:+UseCompressedClassPointers 默认开启的只压缩对象头里的类型指针Klass Pointer
这个时候我们加一下禁止指针压缩这个命令对比一下:
通过开启某一个参数,可以让对象内存地址由8个字节压缩成4个字节。java里面是由很多很多成员变量的,每个对象都有一个对象头,如果不开启这个指针压缩,都是用8个字节来存储这些对象的内存地址,这些信息放到堆里面,无形的就会增大很多空间,导致堆的压力很大。很容易触发gc。
指针压缩是默认开启的
2.2 为什么要进行指针压缩
- 在64位平台的HotSpot中使用32位指针(实际存储用64位),内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,
占用较大宽带,同时GC也会承受较大压力
- 为了减少64位平台下内存的消耗,启用指针压缩功能
- 在jyvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的存入
堆内存
时压缩编码、取出到cpu寄存器
后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G) - 堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
- 堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内存不要大于32G为好
关于对齐填充:
对于大部分处理器,对象以8字节整数倍来对齐填充都是最高效的存取方式。