1. 对象实例化
1.1 创建对象的方式
new:最常见的方式
Student student = new Student();
Class的newInstance方法:反射的方式,智能调用public的空参构造器
Class clazz = Class.forName("com.hong.Student");
Student student = (Student) clazz.newInstance();
Constructor的newInstance(XXX):反射的方式,可以使用空参和有参构造器,无权限要求
HelloWorld.class.getConstructor().newInstance();
使用clone()方法:不调用任何构造器,要求当前类实现Cloneable接口,实现clone()
Student clone = student.clone();
使用反序列化从文件或者网络中获取对象的二进制流
public class Student implements Serializable {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public void say() {
System.out.println("hello i'm hong!"+this.age);
}
public static void main(String[] args) throws Exception {
Student student = new Student();
student.setAge(10);
String filePath = "com";
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
oos.writeObject(student);
oos.close();
System.out.println("序列化完成!");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));
Student student1 = (Student) ois.readObject();
ois.close();
student1.say();
System.out.println("反序列化完成!");
}
}
第三方库Objenesis
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>3.0.1</version>
</dependency>
Objenesis objenesis = new ObjenesisStd();
ObjectInstantiator<Student> instantiator = objenesis.getInstantiatorOf(Student.class);
Student st1 = instantiator.newInstance();
st1.say();
System.out.println(st1.toString());
Student st2 = instantiator.newInstance();
st2.say();
System.out.println(st2.toString());
//结果
hello i'm hong!null
com.Student@b1bc7ed
hello i'm hong!null
com.Student@7cd84586
1.2 创建对象的步骤
1.2.1 判断对象对应的类是否加载、连接、初始化
当虚拟机遇到一个创建对象的指令的时候,首先检查这个指令的参数能否在元空间的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经进行加载连接初始化等。
如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader + 包名 + 类名为key进行查找对应的 .class文件。如果能找到则按照规则加载该类。如果没有找到则抛出ClassNotFoundException异常。
1.2.2 为对象分配内存
首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小
在分配内存空间的时候存在着两种情况,首先是如果内存规整的情况下则采用指针碰撞的方式进行内存分配。内存不规整的情况下则需要维护一个列表记录空闲的内存区域,进行空闲列表分配。
指针碰撞:内存中所有用过的内存放在一侧,没有用过的内存放在另一侧,中间使用一个指针指向分界点处。当需要进行内存分配的时候,指针需要向空闲内存一侧移动分配的内存大小的距离。
空闲列表:如果使用的内存和空闲内存相互交错,则就需要维护一个列表记录空闲内存出现的位置。内存分配的时候就从列表中找到足够大的内存空间划分给对象,并更新列表上的内容。
选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
1.2.3 处理并发问题
采用CAS失败重试、区域加锁保证更新的原子性。CAS是乐观锁的一种实现方式。乐观锁,就是每次假设没有冲突,不加锁地去执行操作。JVM采用CAS+失败重试的方式保证更新操作的原子性。
每个线程预先分配一块TLAB:通过设置 -XX:+UseTLAB
参数来设定:JVM为每个线程预先在Eden区分配一小块区域-线程本地分配缓存(TLAB),JVM在给线程中的对象分配内存时,首先在 TLAB中划分内存。当对象大于TLAB剩余内存或者TLAB用尽时,JVM会再采用CAS+失败重试当方式分配内存。
1.2.4 初始化分配到的内存
内存分配完成后,虚拟机需要将分配到的内存空间都初始化零值(不包括头对象)。这一过程保证了Java的实例对象在JVM中可以不赋初始值就直接使用,程序能访问这些字段的数据类型所对应的零值。
1.2.5 设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
1.2.6 执行init方法进行初始化
在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
给对象属性赋值的操作
- 属性的默认初始化
- 显式初始化
- 代码块中初始化
- 构造器中初始化
2. 对象的内存布局
2.1 对象头
对于普通对象来说由运行时元数据(Mark Word)和类型指针(Klass Word )。对于数组对象来说还需要一个记录数组长度。
2.1.1 运行时元数据
- 哈希值(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 翩向时间戳
详细解释:
标志位:区分锁的状态,最后两位为11时表示为GC回收状态。
是否偏向锁:由于未锁定和偏向锁的标志位都是01,所以引入一位是否偏向锁(biased_lock)来判断,当等于1时,则是偏向锁。
分代年龄:表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
对象hashCode:运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
偏向锁的线程ID1:偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。在后面的操作中,就无需再进行尝试获取锁的动作。
epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
2.1.2 类型指针
Klass Word对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
2.2 实例数据(Instance Data)
它是对象真正存储的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承下来的和本身拥有的字段)
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true(默认为true):子类的窄变量可能插入到父类变量的空隙
2.3 对齐填充(Padding)
不是必须的,也没有特别的含义,仅仅起到占位符的作用
2.4 示例
public class Customer{
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer() {
acct = new Account();
}
}
public class CustomerTest{
public static void main(string[] args){
Customer cust=new Customer();
}
}
3. 对象访问定位
JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢?
3.1 句柄访问
需要额外维护一个句柄池,reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。
3.2 直接指针(HotSpot采用)
直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据
4. 直接内存(Direct Memory)
4.1 概述
直接内存是在Java堆外的、直接向系统申请的内存区间。下面是 《深入理解 Java 虚拟机 第三版》2.2.7 小节 关于 Java 直接内存的描述。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致 OutOfMemoryError 异常出现,所以我们放到这里一起讲解。 在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 显然,本机直接内存的分配不会受到 Java 堆大小的限制,但是,既然是内存,则肯定还是会受到本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置
-Xmx
等参数信息,但经常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError 异常。
- 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
- Java的NIO库允许Java程序使用直接内存,用于数据缓冲区
4.2 非直接缓存区
使用IO读写文件,需要与磁盘交互,需要由用户态切换到内核态。在内核态时,需要两份内存存储重复数据,效率低。
4.3 直接缓存区
使用NIO时,操作系统划出的直接缓存区可以被java代码直接访问,只有一份。NIO适合对大文件的读写操作。也可能导致OutOfMemoryError异常
- 分配回收成本较高
- 不受JVM内存回收管理
直接内存大小可以通过
MaxDirectMemorySize
设置。如果不指定,默认与堆的最大值-Xmx参数值一致