简述
今天继续写深入理解 java 虚拟机的对象创建的理解. 这次和上次隔的时间有些长, 是因为有些东西确实不好理解, 就查阅各种资料, 然后弄明白了才来做记录.
(此文中所阐述的内容都是以 HotSpot 虚拟机为例的.)
对象的创建
java 程序在运行过程中无时无刻都有对象被创建出来, 那么创建对象是个怎么样的过程呢? 还是看看我自己的理解吧.
判断是否已经执行类加载
当虚拟机遇到一条 new 指令时 , 首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用, 并且检查这个符号引用代表的类是否已经被加载, 解析和初始化过, 如果没有, 那必须先执行相应的类加载过程.(类加载过程, 以后我也会单独的介绍)
内存分配
当已经执行过类加载过程后, 会为新对象在 Java 堆中分配一个大小已经确定的内存, 具体的内存分配规则有两种:
指针碰撞 (Bump the Pointer)
如果 Java 堆中的内存是绝对规整的, 所有用过的内存放一边, 空闲的内存放到一边, 中间放着指针为分界点, 分配内存就是把指针向空闲的一边挪动一段与对象大小相等的距离.
空闲列表 (Free List )
如果 Java 堆中的内存并不是规整对的, 已使用的内存和空间相互交错, 虚拟机会将可以用的内存维护到一个列表上, 在分配内存时从这个列表中找到一块足够大的空间划给对象. 然后更新列表记录.
Java 堆中的内存是否是规整的是根据虚拟机所采用的垃圾收集器是否带有压缩整理功能决定的. Serial,ParNew 带压缩整理的分配内存用指针碰撞, CMS 这种通常用空闲列表方式分配内存 (垃圾收集器我也会单独介绍的, 看来对象创建涉及的地方很多呢.)
防止并发
在虚拟机上创建对象是非常频繁的行为, 所以要做到防止并发, 有以下两种方式可实现:
堆分配内存空间的动作进行同步处理, 实际上 JVM 采用 CAS(Cmpare And Set) 配上失败重试的方式保证更新操作的原子性;
把内存分配的动作按照线程划分在不同的空间之中进行, 即为每个线程在 java 堆中预先分配一块小内存, 称为本地线程分配缓冲区 (Thread Local Allocation Buffer,TLAB). 分配内存时在线程的 TLBA 上分配, 只有 TLAB 用完并分配新的 TLAB 时, 才需要同步锁定. JVM 是否使用 TLAB 可以通过 - XX:+UseTLAB 参数来设定.
初始化对象内存空间
内存分配完成后, JVM 将分配到的内存空间都初始化为零值 (不包括对象头).
对象头的设置
将对象的类, 哈希码, 对象的 GC 分代年龄等信息设置到对象头之中.
执行 Java 的 init 方法
设置完对象头后, 从 JVM 的角度来看一个对象已经完成了, 但是从 java 程序的角度来看还没有创建完成呢. 此时就需要执行 init 方法, 调用构造方法等过程, 这样一个真正可用的对象才算完全的产生出来.
对象的内存布局
创建完对象后, 对象对分配给自己的内存是如何布局的呢? 下面来介绍一下.
对象在堆内存中的布局可分为三部分: 对象头 (Header), 实例数据 (Instance Data), 对齐填充 (Padding).
对象头: 对象头包含两部分, 第一部分存储自身运行时数据, 如哈希码, GC 分代年龄, 锁状态标志, 线程持有锁, 偏向线程 ID, 偏向时间戳等, 官方称为 "Mark Word".
第二部分是类型指针, 即对象指向它的类元数据的指针, 通过此指针来确定是哪个类的对象.
实例数据: 存储对象中的各类型的字段内容. 无论是从父类继承来的还是在子类中定义的.
对齐填充: 并不是必然存在的, 当对象实例数据部分没有对齐时, 进行对齐补全.
对象的访问定位
Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象. reference 数据只是一个指向对象的引用, 具体的对象访问根据不同虚拟机有不同的实现, 主流的访问方式有两种: 使用句柄和直接指针.
使用句柄:
如果通过句柄来访问对象, Java 堆中会划出一块内存作为句柄池, reference 中存储句柄地址, 而句柄中包含对象的实例数据与类型数据各自的地址. 这样就能访问到对象了.
直接指针:
直接指针, 就是指 reference 中直接存储对象的地址. 但是 Java 堆对象的布局中就必须考虑如何防止访问类型数据相关信息.
这两种对象访问方式, 各有优势, 但是 HotSpot 使用的是指针对象访问, 但是句柄访问对象在整个软件开发范围中也是十分常见的.
参考
深入理解 Java 虚拟机
来源: https://www.cnblogs.com/jimoer/p/8849025.html