JVM篇2:Java中的对象你整明白了吗
文章目录
本文主要从源头介绍Java对象的创建过程、对象如何访问及堆中对象的分配
1.Java对象的创建过程
1.1类加载检查
- 虚拟机遇到一条new命令时,首先会去检查这个指令的参数是否能在常量池中定位带这个类的符号引用,并且检查这个符号引用代表引用的类是否已经被加载过、解析和初始化过。如果没有,必须先执行相应的类加载过程。
1.2分配内存
- 在类加载检查通过后,接下来JVM将给它分配内存,对象所需的内存大小在类加载完成以后便可以确定,为对象分配空间的任务等同于把一块确定大小的内存从堆中划分出来。
-
内存的分配方式有指针碰撞和空闲列表两种,选择哪一种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定的(也就是取决去GC收集器的算法是“标记-清除”,还是“标记-整理,有时也叫标记-压缩”),值得注意的是,复制算法内存也是规整的。
- 内存分配并发问题
在创建对象的时候,作为虚拟机,必须要保证线程是安全的,通常来说,虚拟机通过两种方式保证线程安全:
(1)CAS+失败重试:CAS是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去执行某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用CAS+失败重试的方式保证更新操作的原子性。
原子性指的是一个或者多个操作在 CPU 执行的过程中不被中断的特性。
(2)TLAB:虚拟机为每一个线程预先在Eden区分配一块内存,在给线程中的对象分配内存时首先在TLAB分配,当对象大于TLAB的剩余内存或者TLAB的内存已耗尽时,在采用第一种方法进行内存分配。
1.3初始化零值
- 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码在可以不赋初始值就可以直接使用,程序可以访问到这些字段的数据类型所对应的零值。
1.4设置对象头
- 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象头中。另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。
1.5执行init方法
- 在上述工作完成之后,**从虚拟机的角度来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚开始,因为方法还没有执行,所有的字段都还为零。**所以一般来说,执行new指令后悔接着执行方法,把对象按照程序员的意愿进行初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。
2.对象的访问定位方式有哪些?
- 建⽴对象就是为了使⽤对象,我们的Java程序通过栈上的 reference (引用)数据来操作堆上的具体对象。对象的访问⽅式由虚拟机实现⽽定,目前主流的访问方式有 使用句柄 和 直接指针两种。
(1)使用句柄:如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的地址信息。
(2)直接指针:如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象的地址。 - 各自的好处:
(1)使用句柄来访问最大的好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。
(2)使⽤直接指针访问⽅式最⼤的好处就是速度快,和使用句柄相比节省了一次指针定位的时间开销。
3.堆内存中对象的分配的基本策略
- 堆空间的基本结构为:
- 上图所示的 eden区、s0区、s1区都属于新⽣代,tentired 区属于⽼年代。⼤部分情况,对象都会⾸先在 Eden 区域分配,在⼀次新⽣代垃圾回收后,如果对象还存活,则会进⼊ s0 或者 s1,并且对象的年龄还会加 1(Eden区–> Survivor 区后对象的初始年龄变为1),当它的年龄增加到⼀定程度(默认为15岁),就会被晋升到⽼年代中。对象晋升到⽼年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
-
另外,大对象和长期存活的对象会直接进入老年代。