new出对象时, JVM内部发生了什么

看到这篇文章觉得比以前看过的所有文章说的比透彻。 从来不写博客的我, 想写一篇翻译来传播原作者的精神哈哈。

原文出处:https://www.javaspring.net/java/what-happened-when-new-an-object-in-jvm

前言

如你所知, Java时一门面向对象语言, 开发时常常会创建一摞子对象。

User user = new User();

这样一行代码, JVM会做些什么事情呢?

对【对象】的理解

  1. 内存布局

在Hotspot虚拟机内部, “对象”的内存布局被划分了三部分: 对象头实例数据和填充对齐
对象头包括两部分信息, 第一部分用来存储对象本身运行时相关数据(HashCode,GC分代年龄,lock锁状态标志,等等)。
第二部分是类型指针, 指向Class元数据区。虚拟机用这个指针来确定这个对象对应的Class对象(如果有句柄池,就不会有这个东西)。如果new出来的对象是数组,会被存储数组长度,如下所示
new出对象时, JVM内部发生了什么_第1张图片
Mard Word是一个【非固定】的把很多信息存储在一个尽可能小空间内的内存区域,同时也会根据对象状态来审时度势。
new出对象时, JVM内部发生了什么_第2张图片
实例数据部分是实际存储的有效信息,即代码中定义的各种类型的字段内容,是由父类继承还是在子类中继承(这句是借助翻译工具翻译的, 刚开始没读懂什么意思)。

“对象填充“并非是实际分配来存储信息的,它只是在虚拟机中扮演一个花瓶的角色,因为HotSpot虚拟机对象对象的起始地址必须是8个字节的整数倍,

2.【对象】访问

在Java程序中, 我们通过指针(指向对象的引用)来操作对象。众所周知,对象是存储在堆中,然而其指针是存储在“虚拟机栈“ 那么指针是什么定位到堆内存的对象呢?

  • 直接指针方法(HotSpot实现):直接存储在引用中的地址是堆中对象的地址。优点是定位速度快,缺点是对象移动(GC移动时的对象移动)本身需要修改
  • 句柄方法:Java堆的一部分用作句柄池。引用存储对象的句柄地址,句柄包含对象实例和类型的特定位置信息。优点是对象的移动只改变了句柄中的实例数据指针,缺点是两次定位。

创建对象的操作

  • 当虚拟机有遇到一个新的指令, 虚拟机会根据指令参数能否在常量池定位到Class的符号引号, 其后检查这个类是否已经被类加载器加载过。如果没有加载, 先加载类。
  • 类型检查通过后,虚拟机将会为新的对象分配内存,所需内存大小在类加载后决定。
  • 内存分配完成后,虚拟机将会把对象初始化为0, 是为了那些在程序代码里未设置初始值的字段能够直接被使用。对于静态变量, 会在类的准备阶段被初始化为0。
  • 设置对象头的基本信息, 类元数据, HashCode, gc年龄,等等
  • 以上操作完成后,一个新的对象诞生了。但是成员方法还未被访问,所有字段都是0。与此同时,要调用构造函数来根据程序员意愿实例化对象。静态变量的初始化操作会在类加载的初始化阶段完成。

有两种方法来分配内存

  • 规则的Java堆内存(垃圾回收算法用标记压缩), 用一个指针指向空闲内存,内存分配多少,指针移动多少。
  • 不规则的Java堆内存(垃圾回收算法用标记清除),虚拟机维护了一个内存空闲列表, 当内存被分配能从空闲列表找到哪块空闲时足够用的, 同时更新这个列表
  • 当内存不够用时, 将会发生一次GC(垃圾回收)

分配内存的并发解决方案
同步分配内存的骚操作—使用“CAS失败重试”来确保更新操作的原子性。每个线程在堆中预先分配一小部分内存,称为线程本地分配缓冲区(TLAB),只有在TLAB耗尽并分配一个新的TLAB时,线程才在其TLAB上分配内存。需要同步锁。由-XX:+/-UseTLAB参数设置

创建对象重排序问题

A a = new A();

它被简单的分解为三个步骤

  1. 为对象分配内存
  2. 初始化对象
  3. 设置指向内存

在2 , 3步骤, 者2个指令有可能会被重排, 从而引起 在多线程环境下 “在访问对象的时候, 对象还未初始化完成“。 单例模式的双检锁会存在这个问题。你可以用“volatile“来禁止重排序, 问题解决。

自己写了个代码, 就当是复习了

new出对象时, JVM内部发生了什么_第3张图片

如图所示, antlr=new AntlrDemo(); 在这一步的三个“jvm“指令当时, 如果对象的赋值先与初始化, 那么就空指针了。在volatile修饰下, 三个指令不会被重排。

本文由博客一文多发平台 OpenWrite 发布!

你可能感兴趣的:(后端开发)