1 对象的实例化
1.1 创建对象的方式
- new
- 最常见的方式
- 变形1 : Xxx的静态方法
- 变形2 : XxBuilder/XxoxFactory的静态方法
- Class的newInstance():反射的方式,只能调用空参的构造器,权限必须是public
- Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求
- 使用clone() :不调用任何构造器,当前类需要实现Cloneable接口,实现clone()
- 使用反序列化:从文件中、从网络中获取一个对象的二进制流
- 第三方库Objenesis
1.2 创建对象的步骤
- 1.判断对象对应的类是否加载、链接、初始化
- 2.为对象分配内存
- 2.1 如果内存规整一指针碰撞
- 2.2 如果内存不规整:虚拟机需要维护一个列表,空闲列表分配
- 3.处理并发安全问题
- 3.1 采用CAS配上失败重试保证更新的原子性
- 3.2 每个线程预先分配一块TLAB
- 4.初始化分配到的空间一所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
- 5.设置对象的对象头
- 6.执行init方法进行初始化
1.2.1 判断对象对应的类是否加载、链接、初始化
虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化。( 即判断类元信息是否存在)。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为Key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象
1.2.2 为对象分配内存
首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
1.2.2.1 如果内存规整一指针碰撞
如果内存是规整的,那么虚拟机将采用的是指针碰撞法(BumpThePointer)来为对象分配内存。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离罢了。如果垃圾收集器选择的是Serial、ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact (整理)过程的收集器时,使用指针碰撞
1.2.2.2 内存不规整
如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虛拟机将采用的是空闲列表法来为对象分配内存。意思是虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。这种分配方式成为空闲列表(Free List)
- 说明:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
- 给对象的属性赋值的操作: ① 属性的默认初始化 ② 显式初始化 ③ 代码块中初始化 ④ 构造器中初始化
1.2.3 处理并发安全问题
在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用 了两种方式解决并发问题:
- CAS ( Compare And Swap ),实际上虚拟机采取CAS配上失败重试的方式保证更新操作的原子性
- TLAB把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB ,Thread Local Allocation Buffer) 虚拟机是否使用TLAB,可以通过一XX:+/一UseTLAB参数来 设定
1.2.4 初始化分配到的空间
- 内存分配完成后,虚拟机将分配到的内存空间(不包括对象头)都初始化为零值。如果使用了TLAB,这个工作可以提前到TLAB分配时进行
- 这步操作保证对象的实例字段在Java代码中,可以不赋初始值就直接使用,程序可以访问到字段对应数据类型所对应的零值
1.2.5 设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现
1.2.6 执行init方法进行初始化
上面工作完成后,从虚拟机角度来说,一个新的对象已经产生了,但是从Java程序的视角来说,对象创建才刚刚开始,对象的构造方法(Class文件中init()方法)还未执行,所有字段都是默认的零值。new指令之后接着执行init方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全构造出来
2 对象的内存布局
2.1 对象头(Header)
- 这部分数据的长度在32位和64位的虚拟机(未开启指针压缩中)分别是32bit和64bit,官方称为【Mark Word】运行时元数据
- 哈希值( HashCode )
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
- 类型指针:即对象指向它的类型元数据的指针,通过这个指针来确认该对象属于哪个类的实例
- 说明:如果是数组,还需记录数组的长度
2.2 实例数据(Instance Data)
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段) 规则:
- 相同宽度的字段总被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认也是true),那么子类中较窄的变量也允许插入父类变量的空隙之间,以节省一点点空间。
2.3 对齐填充(Padding)
没有特别含义,仅仅起到占位符作用
解释:因为HotSpot虚拟机自动内存管理系统,要对对象的起始地址必须是8字节的整数倍,换句话就是任何对象的大小都必须是8字节的整数倍。
对象头已经精心设计为8字节的整数倍,1倍或者2倍。
对象实例数据部分如果没有对齐的话,就需要通过对其填充来补全。
2.4 代码分析
/**
* 测试对象实例化的过程
* ① 加载类元信息 - ② 为对象分配内存 - ③ 处理并发问题 - ④ 属性的默认初始化(零值初始化)
* - ⑤ 设置对象头的信息 - ⑥ 属性的显式初始化、代码块中初始化、构造器中初始化
*
* 给对象的属性赋值的操作:
* ① 属性的默认初始化 - ② 显式初始化 / ③ 代码块中初始化 - ④ 构造器中初始化
*
*/
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer(){
acct = new Account();
}
}
class Account{
}
public class CustomerTest {
public static void main(String[] args) {
Customer cust = new Customer();
}
}
3 对象的访问定位
创建对象的目的就是为了使用它
JVM是如何通过栈帧中的对象引|用访问到其内部的对象实例的呢?-> 定位,通过栈上reference访问
对象访问的主要方式有两种(JVM规范没有明确规定)
- 句柄访问
缺点:占用空间,效率低(先访问句柄,在访问堆) 优点:对象发生移动时,不需要修改栈帧
- 直接指针(HotSpot采用)
优点:不占空间,速度快 缺点:对象发生移动时,需要修改栈帧
4 直接内存
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域
- 直接内存是Java堆外的、直接向系统申请的内存区间
/**
* IO NIO (New IO / Non-Blocking IO)
* byte[] / char[] Buffer
* Stream Channel
*
* 查看直接内存的占用与释放
*/
public class BufferTest {
private static final int BUFFER = 1024 * 1024 * 1024;//1GB
public static void main(String[] args){
//直接分配本地内存空间
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
System.out.println("直接内存分配完毕,请求指示!");
Scanner scanner = new Scanner(System.in);
scanner.next();
System.out.println("直接内存开始释放!");
byteBuffer = null;
System.gc();
scanner.next();
}
}
-
通常,访问直接内存的速度会优于Java堆。即读写性能高
- 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存
- Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
-
也可能导致OutOfMemoryError异常:OutOfMemoryError: Direct buffer memory
/**
* 本地内存的OOM: OutOfMemoryError: Direct buffer memory
*/
public class BufferTest2 {
private static final int BUFFER = 1024 * 1024 * 20;//20MB
public static void main(String[] args) {
ArrayList<ByteBuffer> list = new ArrayList<>();
int count = 0;
try {
while(true){
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
list.add(byteBuffer);
count++;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
System.out.println(count);
}
}
}
- 由于直接内存在Java堆外,因此它的大小不会直接受限于一Xmx指定的最大 堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存
- 缺点
- 分配回收成本较高
- 不受JVM内存回收管理
- 直接内存大小可以通过MaxDirectMemorySize设置
- 如果不指定,默认与堆的最大值-Xmx参数值一致
JVM完整目录
1. jvm概述
2.类加载机制
3.运行时数据区[PC寄存器、虚拟机栈、本地方法栈]
4.运行时数据区[堆]
5.运行时数据区[方法区]
6.暂缺
7. 运行时数据区[对象的实例化内存布局与访问定位、直接内存]
8.执行引擎(Execution Engine)
9.字符串常量池
10.垃圾回收[概述、相关算法]
11.垃圾回收[垃圾回收相关概念]
12.垃圾回收[垃圾回收器]
13.常见的OOM
14. JDK命令行工具