1.对象的创建流程是啥?
如上图所示,对象的创建流程大致是这个样子。首先当虚拟机遇到一个new 指令(new关键字,对象克隆,对象序列化)时,首先回去判断这个类有没有被加载过,如果没有加载那么需要先加载;其次就要对对象分配相应的内存,划分内存的方法通常有,指针碰撞(Java堆中的内存绝对规整)、空闲列表(Java堆中的内存并不规整)这两种方法,我们默认的是使用指针碰撞的方式进行分配,接下来我用画图的方式来解释下指针碰撞
上图是指针碰撞的基本模型,当需要新给对象分配内存时,我们的指针会往右挪动对象大小的空间如下图所示,这就是指针碰撞。
接下来我们来说说空闲列表分配方法(FreeList),这个方法其实很好理解,当我们的已使用内存和空闲内存相互交错,那么指针碰撞这种方式就不太可行,这时候我们的虚拟机就会维护一个列表来存储哪些内存是可用的,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
以上这两种分配方式如果在并发的条件下都会出现问题,那么如何解决并发问题呢?虚拟机采用了两种方式来解决,首先就是我们熟悉的,CAS,虚拟机采用CAS结合失败重试的方法保证更新操作的原子性来对分配内存空间的动作进行同步处理。第二种方式就是我们可以配置的TLAB方式,中文称之为本地线程分配缓冲,把内存分配的动作按照线程划分在不同的空间之中进行,保证每个线程在Java堆中会预先分配一小块内存。相关JVM参数设置,-XX:+UseTLAB 默认开启,-XX:TLABSize 指定TLAB的大小。
当我们完成对象内存的分配后,我们需要执行初始化,这个初始化比较好理解,当我们创建一个静态变量时,如
public static int initData = 666;
初始化的时候,则会给initData 赋0;
我们知道对象在内存中存储的布局可以分为三块区域,对象头,实例数据,对齐填充;
而初始化结束时,虚拟机就会对对象进行必要的设置,而这些设置的相关的信息会存储在对象头中,接下来我们来说说对象头 对象头中含有标记字段(Mark Word)、类型指针(Klass Pointer)、如果是数组它还会有数组长度,至于标记字段中有哪些数据,如上图所写。上面说的三块区域比较抽象,我们可以利用Java代码让其变的直观一点,首先我们要引用一个jar包,pom依赖如下图
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-benchmarks</artifactId>
<version>0.17</version>
</dependency>
接下来我们创建一些对象,并且打印一些关键信息
package com.tuling.jvm;
import org.openjdk.jol.info.ClassLayout;
public class JolTest {
public static void main(String[] args) {
ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
System.out.println("--------------------------------------------");
ClassLayout layout2 = ClassLayout.parseInstance(new int[]{});
System.out.println(layout2.toPrintable());
System.out.println("--------------------------------------------");
ClassLayout layout3 = ClassLayout.parseInstance(new A());
System.out.println(layout3.toPrintable());
}
public static class A {
int id;
String name;
byte b;
Object object;
}
}
我们可以得到以下打印:
java.lang.Object object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf80001e5
12 4 (object alignment gap)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
--------------------------------------------
[I object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800016d
12 4 (array length) 0
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
--------------------------------------------
com.tuling.jvm.JolTest$A object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf800cd1a
12 4 int A.id 0
16 1 byte A.b 0
17 3 (alignment/padding gap)
20 4 java.lang.String A.name null
24 4 java.lang.Object A.object null
28 4 (object alignment gap)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
Process finished with exit code 0
通过打印的相关信息我们可以清晰的看到,对象头中存储的数据以及一些其它数据,这里的klass pointer上图中没有相应的解释,这里说明一下,这个类型指针其实是对应方法区的类信息,至于对齐这一块,我们知道 8个字节 是对象寻址的最优解,所以虚拟机会增加对应的填充。
当虚拟机设置完对象头之后,接下来我们会执行<init>方法,即 给 initData 赋 666 并执行对象的构造方法;以上便是对象创建的全过程。下面我们再说下另外一个知识点,对象的指针压缩:
什么是java对象的指针压缩?
1.jdk1.6 update14开始,在64bit操作系统中,JVM支持指针压缩
2.jvm配置参数:UseCompressedOops,compressed压缩、oop(ordinary object pointer)对象指针
3.启用指针压缩:XX:+UseCompressedOops(默认开启),禁止指针压缩:XX:UseCompressedOops
为什么要进行指针压缩?
1.在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,
占用较大宽带,同时GC也会承受较大压力
2.为了减少64位平台下内存的消耗,启用指针压缩功能
3.在jvm中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得jvm
只用32位地址就可以支持更大的内存配置(小于等于32G)
4.堆内存小于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,即使用低虚拟地址空间
5.堆内存大于32G时,压缩指针会失效,会强制使用64位(即8字节)来对java对象寻址,这就会出现1的问题,所以堆内
存不要大于32G为好