看到这篇文章觉得比以前看过的所有文章说的比透彻。 从来不写博客的我, 想写一篇翻译来传播原作者的精神哈哈。
前言
如你所知, Java时一门面向对象语言, 开发时常常会创建一摞子对象。
User user = new User();
这样一行代码, JVM会做些什么事情呢?
对【对象】的理解
- 内存布局
在Hotspot虚拟机内部, “对象”的内存布局被划分了三部分: 对象头, 实例数据, 和填充对齐。
对象头包括两部分信息, 第一部分用来存储对象本身运行时相关数据(HashCode,GC分代年龄,lock锁状态标志,等等)。
第二部分是类型指针, 指向Class元数据区。虚拟机用这个指针来确定这个对象对应的Class对象(如果有句柄池,就不会有这个东西)。如果new出来的对象是数组,会被存储数组长度,如下所示
Mard Word是一个【非固定】的把很多信息存储在一个尽可能小空间内的内存区域,同时也会根据对象状态来审时度势。
实例数据部分是实际存储的有效信息,即代码中定义的各种类型的字段内容,是由父类继承还是在子类中继承(这句是借助翻译工具翻译的, 刚开始没读懂什么意思)。
“对象填充“并非是实际分配来存储信息的,它只是在虚拟机中扮演一个花瓶的角色,因为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();
它被简单的分解为三个步骤
- 为对象分配内存
- 初始化对象
- 设置指向内存
在2 , 3步骤, 者2个指令有可能会被重排, 从而引起 在多线程环境下 “在访问对象的时候, 对象还未初始化完成“。 单例模式的双检锁会存在这个问题。你可以用“volatile“来禁止重排序, 问题解决。
自己写了个代码, 就当是复习了
如图所示, antlr=new AntlrDemo(); 在这一步的三个“jvm“指令当时, 如果对象的赋值先与初始化, 那么就空指针了。在volatile修饰下, 三个指令不会被重排。
本文由博客一文多发平台 OpenWrite 发布!