JVM创建对象过程
# JVM创建对象过程
# 1 Java创建对象的过程
步骤1:类加载检查
当虚拟机遇到new指令时,首先检查常量池中有没有这个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化过。如果没有先执行相应的类加载过程。
步骤2:分配内存
在堆中为新对象分配内存。
步骤3:初始化零值
为成员变量赋初始零值,这一操作可以保证对象的字段在Java代码中不赋值就可以使用。
步骤4:设置对象头
虚拟机对对象进行设置,比如它是哪个类的实例、如何找到元数据信息、哈希码、GC 分代年龄等,这些都放在对象头中。
步骤5:执行 init()
方法
从虚拟机角度对象已经产生,但是从java视角看对象创建才刚刚开始。通过执行 init 方法对对象进行初始化,就是调用构造器等方法。
# 2 Java程序从创建到运行的过程
- 编辑源代码
.java
- 通过 javac 编译
.java
文件生成字节码文件.class
- JVM 的类加载器加载字节码文件到内存
- JVM 解释执行,最后通过操作系统操作 CPU 执行获取结果
# 3 对象创建的方法
当虚拟机遇到一条 new 指令时 ,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,那必须先执行相应的类加载过程。
- 使用 new 创建对象
- 使用反射机制创建对象,Class.forName()、使用 Class 类的 newInstance、使用 Constructor 类的
newInstance()
方法。 - 使用拷贝(深拷贝和浅拷贝)
- 采用序列化机制,调用
java.io.ObjectInputStram
对象的readObject()
放。
# 4 对象内存分配方式
当已经执行过类加载过程后,会为新对象在 Java 堆中分配一个大小已经确定的内存,具体的内存分配规则有两种:
指针碰撞(Bump the Pointer):如果 Java 堆中的内存是绝对规整的,所有用过的内存放一边,空闲的内存放到一边,中间放着指针为分界点,分配内存就是把指针向空闲的一边挪动一段与对象大小相等的距离。
空闲列表 (Free List ):如果 Java 堆中的内存并不是规整的,已使用的内存和空间相互交错,虚拟机会将可以用的内存维护到一个列表上,在分配内存时从这个列表中找到一块足够大的空间划给对象。然后更新列表记录。
Java 堆中内存是否规整是根据虚拟机锁采用的垃圾收集器的压缩整理功能决定的,Serial, ParNew带压缩整理的分配内存用指针碰撞,CMS通常用空闲列表方式分配内存。
防止并发,在虚拟机上创建对象是非常频繁的行为,所有要做到防止并发,两种方式:
- 堆分配内存空间的动作进行同步处理,实际上 JVM 采用 CAS + 失败重试的方式保证更新操作的原子性。
- 把内存分配的动作按照线程划分在不同的空间中进行,即为每个线程在 Java 堆中预先分配一块小内存,称为本地线程分配缓冲区(Thread Local Allocation Buffer,TLAB)。
# 5 初始化对象内存空间
内存分配完毕,JVM 将分配到的内存空间都初始化零值(不包含对象头);
设置对象头(将对象的类、哈希码、对象的GC分代年龄等信息设置到对象头中);
执行 Java 的 init 方法,就是调用构造方法等过程。
# 6 对象在堆中的组成
- 对象头:包含两部分,第一部分存储自身运行时数据,如哈希码,GC 分代年龄、锁状态标志、线程持有锁、偏向线程 ID、偏向时间戳等,称 Mark Word;第二部分是类型指针,指向它的类元数据,通过此指针来确定是哪个类的对象,成 Klass Word。
- 实例数据:存储对象中的各类型的字段内容。无论是从父类继承来的还是在子类中定义的。
- 对齐填充:并不是必须存在的,当对象实例数据部分没有对齐时,进行对齐补全。
# 7 对象访问定位的两种方式
Java 程序通过栈上的 reference 数据来操作具体的对象,访问方式主要分为以下两种方式:
使用句柄:Java 堆中会划分出来一块内存作为句柄池,栈中的 reference 存储的就是对象的句柄地址,句柄中包含了对象实例数据和对象类型的具体地址。优点:reference 中存储的句柄地址是稳定的,对象的移动只会改变句柄中实例的数据指针,而 reference 不需要修改。
直接指针(Hotspot的方式):referernce 直接指向堆中对象数据,堆中的对象指向对象类型数据。优点:访问快。
# 8 Java中的对象是否都是在堆上分配
不一定,因为JVM通过逃逸分析,能够分析出一个新对象的使用范围,并以此确定是否要将这个对象分配到堆上。
逃逸分析
一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。
不在堆上分配内存的优点:
- 对象分配在栈上,可以快速地在栈上创建和销毁对象,不用再将对象分配到对空间,减少JVM垃圾回收压力。
- 分离对象或标量替换:当JVM通过逃逸分析,确定要将对象分配到栈上时,即时编译可以将对象打散,将对象替换为一个个很小的局部变量,我们将这个打散的过程叫做标量替换。将对象替换为一个个局部变量后,就可以非常方便的在栈上进行分配了。
- 同步锁消除:如果 JVM 通过逃逸分析,发现一个对象只能从一个线程被访问到,则访问这个对象时,可以不加同步锁。如果程序中使用了 synchronized 锁,则 JVM 会将 synchronized 锁消除。