对象在JVM中的生活
在java中万事万物皆对象,而java是在JVM中进行运行的,那么对象在JVM中是如何运作的呢?我们既要知道对象是怎么来的,也要知道对象是怎么没的
先来看一下我们看到的是如何创建对象的
创建对象的几种方式
有以下几种方式
- 使用new进行实例化
1 | TestObject testObject = new TestObject(); |
- 使用Class的newInstance()
主要该方式调用的是无参的构造器,且必须是public的
1 | TestObject testObject1 = TestObject.class.newInstance(); |
- 使用Constructor的newInstance()
该方式使用的构造器根据所传类型来确定,没有权限修饰符的要求
1 | Constructor<?> constructor = TestObject.class.getConstructor(); |
- 使用clone()
需要实现Colneable接口,实现clone方法
1 | TestObject clone = (TestObject) testObject.clone(); |
- 使用反序列化
创建对象在JVM中的步骤
1 | TestObject testObject = new TestObject(); |
java编译为字节码之后为
1 | 0: new #2 // class com/zhanghe/study/jvm/TestObject |
首先会判断对应的类是否加载、链接、初始化
new指令会首先去检查这个指令的参数能否在元空间的常量池定位到一个类的符号引用,并检查该符号引用对应的类是否已经被加载、链接、初始化,如果没有,则在双亲委派的模式下,使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的class文件,如果没有找到文件,则会报ClassNotFoundException异常,如果找到,则进行类加载,生成对象的Class类对象
之后为对象分配内存,对象所需内存在类加载完成后便可以确定
在进行分配内存时存在一个问题,那就是如何进行分配?如果java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是将指针向空闲空间挪动一段与对象大小相等的距离,这种分配方式称为指针碰撞,在Serial、ParNew等带有Compact过程的收集器采用的是指针碰撞的算法;如果java堆中的内存并不规整,已使用的内存和空闲的内存交错,那么就无法使用指针碰撞了,虚拟机需要维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间来划分给对象实例,然后更新列表上的记录,这种分配方式称为空闲列表,在CMS这种基于mark-sweep的收集器采用的是空闲列表算法
是否规整取决于使用的垃圾算法是什么,如果使用的是压缩整理算法就是规整的
处理并发问题
由于对象实例化是非常频繁的,那么肯定会存在并发问题,这种并发问题如何解决呢?有两种解决方式:一、对分配内存空间的动作进行同步处理,采用CAS来保证更新操作的原子性;二、把内存分配的动作按照线程划分在不同的空间来进行,即每个线程在java堆(Eden区中的一小块)中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,简称TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定,可以使用-XX:+UseTLAB来设定使用TLAB
初始化分配空间,所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用(默认初始化)
设置对象的对象头(对象头中存储着很多信息,如这个对象是哪个类的实例、如何才能找到对象的元数据信息、对象的Hash码、对象的GC分代年龄等信息)
执行
方法进行初始化,属性的显示初始化、代码块中初始化、构造器中初始化
对象的内存布局
对象头
对象头包含两部分信息,第一部分用于存储对象自身的运行时数据(包括哈希值、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳),该部分被称为MarkWord;第二部分是类型指针(指向类元数据InstanceKlass),虚拟机通过这个指针来确定该对象的所属类型
可以使用该方式来查看对象头
1
2
3
4
5
6<!--查看对象头工具-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>1
ClassLayout.parseClass(TestDup.class).toPrintable()
实例数据
对象真正存储的有效信息,包括程序代码中定义的成员变量的值
对齐填充
用于确保对象的总长度为8字节的整数倍
对象访问定位
对象创建之后java程序如何找到对应的对象位置,目前有两种访问方式
句柄,指向指针的指针,句柄不直接指向对象,而是指向对象的指针(句柄不会发生变化,指向固定的内存地址),如果使用句柄的话,需要在java堆中划分出一块内存作为句柄池,栈中的reference存储的就是对象的句柄地址,在句柄中包含了对象实例数据和对象类型数据的具体地址信息
优点:在对象被移动时只需要改变句柄中的实例数据指针,而reference本身不需要修改
直接指针,直接指向对象,如果使用直接指针,那么在java堆中就必须考虑如何放置数据的相关信息,而在栈中的reference存储的就是对象的地址(HotSpot使用的就是该方式)
优点:速度快,节省了一次指针定位的时间开销。由于对象访问在java中非常频繁,所以每次节省一次开销可以节省很多的时间