原来New关键字创建对象的背后还隐藏了这么多秘密,看完这篇文章我顿悟了
前言
对于前面几篇文章, 主要就是说明了一个.java文件是如何一步步编译, 解析最后加载到JVM中运行的, 那么本篇文章将说明对象是如何创建的, 包括创建过程、对象头与指针压缩、jvm对象内存分配详解、逃逸分析,线上分配,标量替换等等内容。
内容有点多,所以准备分为三篇文章来写:
- JVM对象创建及对象大小与指针压缩
- 对象内存分配
- 对象内存回收
如果感觉文章中有的图片字太小不清楚的可以通过公众号加我,然后说明是哪篇文章的图片,然后我发给你。
对象的创建
对象创建的主要流程:
图片
1.类加载检查
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。new指令对应到语言层面上讲是,new关键词、对象克隆、对象序列化等。
对于我们来说,我们写的java代码是new 一个对象,实际上对于底层jvm实际上是执行了一个new 指令。
这里用的插件是:jclasslib Bytecode Viewer
图片
首先会判断这个类有没有被加载过,如果没有加载过,那么它首先会执行加载类的过程(前几篇文章有讲),如果加载过了,那么就要开始new对象了,这个对象一般来说可能放在堆中也有可能放在栈里边,但是不管放在哪,前提都是需要分配一块内存空间的。
2.分配内存
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类 加载完成后便可完全确定,为对象分配空间的任务等同于把 一块确定大小的内存从Java堆中划分出来。
这个步骤有两个问题:
- 如何划分内存。
- 在并发情况下, 可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
划分内存的方法:
“指针碰撞”(Bump the Pointer)(默认用指针碰撞)
如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,比如下图中蓝色实线表示当前指针位置,虚线表示挪动后的位置,那所谓分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。
图片
“空闲列表”(Free List)
如果Java堆中的内存并不是规整的,已使用的内存和空 闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录。
图片
但是具体使用的是指针碰撞的方式还是使用的是空闲列表的分配方式,取决于使用的什么垃圾回收算法,如果使用的是标记整理的话,那么最终剩余的内存肯定是第一种,那么使用的也就是指针碰撞的方式,如果使用的是标记清除的话,那么最终剩余的内存肯定是第二种,所以就使用空闲列表的方式来分配内存。
解决并发问题的方法:
不管使用哪种方式分配,都会出现并发问题,也就是两个线程同时创建了一个对象,然后争抢同一块内存
图片
多个线程创建了多个对象,但是内存空间只有一块,那么jvm为了解决这种并发问题,采取了以下两种措施
CAS(compare and swap)
虚拟机采用CAS配上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。
CAS配上失败重试也就是线程A和线程B同时争抢这一块内存,如果线程A先争抢到了这块内存,那么线程B重新进行分配,发现这块内存分配给了线程A,然后就会在这块内存后面进行内存分配操作。这样线程A、B对象的内存空间就在并发的情况下被分配了。
本地线程分配缓冲(TLAB: Thread Local Allocation Buffer)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中(比如Eden区)预先分配一小块内存。通过-XX:+/-UseTLAB参数来设定虚拟机是否使用TLAB(JVM会默认开启-XX:+UseTLAB)
那么这个内存也不可能特别大,好像默认是Eden区的1% , 通过-XX:TLABSize 可以指定TLAB大小。如果这个时候放不下了,那么就会恢复CAS配上失败重试的方式进行分配。当然,一般不推荐你去改JVM默认的参数设置。
图片
线程A和线程B在Eden区预先分配一块属于自己的内存空间,然后把各自的对象放到各自的空间种。JDK8默认使用的就是这种方式。
对象的分配过程会在下一篇文章详细说明。
3.初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
也就是对于对象的成员变量,比如int initData = 666;那么在这个过程,会先给initData 赋一个0值,就和前面有一篇文章中提到过静态变量的初始化赋值是一样的。最终可能有一步会把真正的值666赋给initData。
4.设置对象头
初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance Data)和对齐填充(Padding)。HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时 间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
32位对象头
图片
对象头中有一个Mark Word标记字段,第一列是对象的一个状态,可能有一些对象被加锁了或者是被GC标记了,不同的对象,它对象头的结构是不一样的,比如说一个对象是正常的对象,也就是没有任何的锁,对象头中前面25bit存储是对象的hashCode,中间4bit存储的是对象的分代年龄,分代年龄在上一篇文章中有讲过,它是4bit,也就以为着它的分代年龄是| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) // // 64 bits: // -------- // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) // size:64 ----------------------------------------------------->| (CMS free block) // // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)