后续我会陆陆续续更新虚拟机的源码,原理,和介绍。大家如果觉得对自己有用就点个关注吧。
声明
本文大部分内容摘自于《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》 — 周志明
并加上一些我自己的理解,和查阅的资料
对象的创建
检查常量池
当Java虚拟机遇到一条字节码new指令时:
- 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须执行相应的类加载过程。
这里面的常量池指的是Class常量池
Class常量池:主要存放两大类常量:字面量和符号引用。加载Class文件时,Class文件中String对象会进入字符串常量池(这里的进入是指 放入字符串的引用,字符串本身还是在堆中),别的大都会进入运行时常量池。
分配内存
- 当检查通过之后,就开始给对象分配内存。
知识点
- 对象所需的内存的大小在类加在完成后便可以完全确定。
分配内存的两种方式
指针碰撞
如果假设Java堆内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放到另一边,中间放着一个指针作为分界点的指示器。
有没有勾起你的回忆!对!这种管理方式就类似数据结构里面的队列,用指针作为分界线来分隔队列中存储数据的一部分和没有存储数据的一部分。在往队列里添加元素时,就将指针上移即可。
空闲列表
像上面说的往往Java堆内存不是规整的,这要取决于虚拟机对堆内存的处理算法,如果堆内存不是规整的,那就意味着可用的空间和不可用的空间不是连续的
这种情况虚拟机就需要维护一个列表,记录上面哪些内存块可用哪些不可用,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称作“空闲列表”。
至于用那种方式去为对象分配内存,这就取决于所采用的垃圾收集器是否带有空间压缩整理的能力。
分配内存时的线程安全问题
虚拟机创建对象的行为非常频繁,这就不得不考虑并发问题,无论用上面的那种方式去分配内存,都可能出现“脏读”的问题,线程1,2在同时为对象分配内存,他们都看上了同一块内存,如果不做处理的话,就会给程序带来不必要的问题。解决这种问题有两个方案可以参考
- 对分配内存空间的动作进行同步处理(加锁)——实际上虚拟机是采用CAS配上失败重试的方式保证更新的原子性
- 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。
分配完内存之后的事情
内存的初始化
内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值,如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
这就是为啥创建了一个对象,对象里面的一部分实例变量不需要初始化依然有默认值0。
设置单据头
Java虚拟机还要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等。
虚拟机的对象已经创建完成
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
因为虚拟机的这部分工作内容主要是管理内存的。