讲一下 Java 创建一个对象的过程。【⭐⭐⭐⭐】

下图便是 Java 对象的创建过程,我建议最好是能默写出来,并且要掌握每一步在做什么。

Java创建对象的过程

Step1:类加载检查

虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池(存在元空间)中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的<kbd>类加载过程</kbd>。

Step2:分配内存

类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。 分配方式有 “指针碰撞”“空闲列表” 两种,选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定

内存分配的两种方式:(补充内容,需要掌握)

选择以上两种方式中的哪一种,取决于 Java 堆内存是否规整。而 Java 堆内存是否规整,取决于 GC 收集器的算法是"标记-清除",还是"标记-整理"(也称作"标记-压缩"),值得注意的是,复制算法内存也是规整的

  • 指针碰撞

    适用场合: 堆内存规整〔即没有内存碎片)的情况下

    原理: 用过的内存全部整合到一边,没有用过的内存放在另一边,中间有一个分界值指针,只需要向着没用过的内存方向将该指针移动对象内存大小位置即可 GC收集器 : SerialParNew

  • 空闲列表 适用场合: 堆内存不规整的情况下 原理﹔ 虚拟机会维护一个列表,该列表中会记录哪些内存块是可用的,在分配的时候,找一块儿足够大的内存块儿来划分给对象实例,最后更新列表记录。 GC收集器: CMS3

内存分配并发问题(补充内容,需要掌握)

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS+失败重试: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。
  • TLAB: 为每一个线程预先在Eden区分配一块儿内存,JVM 在给线程中的对象分配内存时,首先在 TLAB 分配,当对象大于 TLAB 中的剩余内存或 TLAB 的内存已用尽时,再采用上述的 CAS 进行内存分配

Step3:初始化零值

内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

Step4:设置对象头

初始化零值完成之后,虚拟机要对对象进行必要的设置,【例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。】 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。

Step5:执行 init 方法

在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始,<init> 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 <init> 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

标记清除、标记复制、标记整理分别是怎样清理垃圾的?各有什么优缺点?【⭐⭐⭐⭐⭐】

标记清除

可以看到,在对象可以被回收的区域上,JVM会直接把这些垃圾对象占用的内存直接清除掉。

优点:简单

缺点:造成内存空间碎片

执行效率不稳定,如果Java堆中包含大量对象,某一次回收时无用的对象非常多,这时候会花费很多时间进行内存的清除。 有可能造成内存空间碎片,上图只是一个理想的删除过程,正好没有内存碎片产生,而实际上在内存中待清除的内存有可能不是连续的,导致会产生许多内存碎片,如果某个大对象无法找到一块连续的内存进行存放时,会误以为堆内存不足,提前触发Full GC

标记复制

标记-复制算法将原本的堆内存划分了两个区域,采用了“半区复制”算法,将一半的内存省出来,当发生垃圾收集行为时,将存活的对象复制到另外一半保留区域中连续存放。

优点:标记-复制算法的优点是解决了大对象分配内存的内存碎片问题,也解决了标记-清除算法中大量垃圾对象导致的清除效率问题。

缺点:也非常的明显,那就是可分配的内存空间少了整整一半,而且如果某次存活的对象较多,甚至全部存活,那么复制的效率将会非常低。

标记整理

根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存

分代收集

当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。 一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

  • 比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
  • 老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集

HotSpot为什么要分为新生代和老年代? 因为有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。提高效率。

关注公众号@孙中明 回复:3005 获取书籍 《深入理解Java虚拟机》

image.png