1. 虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建
- 1 当Java虚拟机遇到一条字节码new指令时
- 2.1 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过
- 2.2 如果没有,那必须先执行相应的类加载过程
- 3 虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定
- 指针碰撞
- 内存是绝对规整的,所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离
- Serial、ParNew等带压缩整理过程的收集器时,采用的分配算法是指针碰撞
- 空闲列表
- 如果Java堆中的内存并不是规整的,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录
- 使用CMS这种基于清除(Sweep)算法的收集器时,理论上 就只能采用较为复杂的空闲列表来分配内存
- 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定
- 分配内存在并发情况下不是线程安全的问题
- 对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性
- 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)
- 哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
- 指针碰撞
- 4 内存分配完成之后,虚拟机必须将分配到的内存空间(但不包括对象头)都初始化为零值
- 如果使用了TLAB的话,这一项工作也可以提前至TLAB分配时顺便进行
- 这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的零值。
- 5 对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码(实际上对象的哈希码会延后到真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式
- 6 从虚拟机的视角来看,一个新的对象已经产生了
- 但是从Java程序的视角看来,对象创建才刚刚开始——构造函数,即Class文件中的<init>()方法还没有执行,所有的字段都为默认的零值,对象需要的其他资源和状态信息也还没有按照预定的意图构造好。一般来说(由字节码流中new指令后面是否跟随invokespecial指令所决定,Java编译器会在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此),new指令之后会接着执行<init>()方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全被构造出来。
2 对象的内存布局
2.1 内存布局
-
HotSpot虚拟机中对象在堆内存中的存储布局可以分为三部分
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
-
对象头的组成 没有指针压缩
-
image.png
- MarK Word 存储对象自身的运行时数据
- 设计成一个有着动态定义的数据结构,以便在极小的空间内存储尽量多的数据,根据对象的状态复用自己的存储空间
-
32位
image.png
-
64位
image.png
- 类型指针 对象指向它的类型元数据的指针
- 并不是所有的虚拟机实现都必须在对象数据上保留类型指针
- MarK Word 存储对象自身的运行时数据
-
实例数据
- 对象真正存储的有效信息,代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段
- 字段内容存储顺序会受两个影响
- 虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)
- 字段在Java源码中定义顺序
- HotSpot默认的分配顺序
- longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,普通对象指针)
- 相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前
- +XX:CompactFields参数值为true(默认就为true),子类之中较窄的变量也允许插入父类变量的空隙之中
- 字段内容存储顺序会受两个影响
- 对象真正存储的有效信息,代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段
-
对齐填充
- 自动内存管理系统要求对象起始地址必须是8字节的整数倍,任何对象的大小都必须是8字节的整数倍
- 对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍)
- 如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全
- 原因之一
- 让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。
2.2 压缩指针
- 64位JVM在支持更大堆的同时,导致的问题
- 增加了GC开销
- 如 Integeter 实例 24(对象头 16字节+4字节int+4字节对齐填充)个字节 int类型 4个字节
- Integeter 开销是 int 的6倍, 这是为什么java要引入基本类型的原因之一
- 如 Integeter 实例 24(对象头 16字节+4字节int+4字节对齐填充)个字节 int类型 4个字节
- 降低CPU缓存命中率
- 64位对象引用增大了,CPU能缓存的oop将会更少,从而降低了CPU缓存的效率。
- 增加了GC开销
- 原理
- 因为有8字节填充,4字节值(32位)表示的是第几个8字节,4字节最大表示4G * 8 = 32G
- 超过32G则会关闭压缩指针
- 当压缩指针解引用,寻址32地址空间的伪64位指针
- 左移 3 位,再加上一个固定偏移量
- 因为有8字节填充,4字节值(32位)表示的是第几个8字节,4字节最大表示4G * 8 = 32G
- 会压缩的对象
- 对象头的类型指针
- 引用类型的字段
- 引用类型数组
- JVM相关参数
- -XX:ObjectAlignmentInBytes 默认值8 对其填充参数
- 可以进一步提升寻址范围。但是,这同时也可能增加对象间填充,导致压缩指针没有达到原本节省空间的效果
- -XX:-UseCompressedOops 压缩指针 选项默认启用
- -XX:ObjectAlignmentInBytes 默认值8 对其填充参数
System.out.println(org.openjdk.jol.info.ClassLayout.parseClass(Integer.class).toPrintable());
// 关闭指针压缩
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 16 (object header) N/A
16 4 int Integer.value N/A
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
// 启用指针压缩
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int Integer.value N/A
Instance size: 16 bytes
2.3 字段重排列
- Java 虚拟机重新分配字段的先后顺序,以达到内存对齐的目的
- java 虚拟机中有三种排列方法(对应 Java 虚拟机选项 -XX:FieldsAllocationStyle,默认值为 1)
3 对象的访问定位
-
创建对象就是为了后续使用,java程序通过栈上的reference数据来操作堆上的具体对象。
- reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置。
- 对象访问方式也是由虚拟机实现而定的,主流的访问方式主要有使用句柄和直接指针两种
- 句柄
- Java堆中将可能会划分出一块内存来作为句柄池,reference中存储对象的句柄地址
- reference存储对象的句柄地址,句柄中包含了对象实例数据与类型数据各自具体的地址信息
-
image.png
- 直接指针
- Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference中存储的是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
-
image.png
- 好处 速度更快,节省了一次指针定位的时间开销,对象访问在Java中非常频繁。
- reference类型在《Java虚拟机规范》里面只规定了它是一个指向对象的引用,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置。
-
HotSpot 采用的是直接指针
-
如何解决内存溢出
- 异常堆栈信息“java.lang.OutOfMemoryError”会跟随进一步提示“内存区域”
- Java堆内存溢出时,内存区域 “Java heap space”。
- 解决一般通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析
- 第一步 首先应确认内存中导致OOM的对象是否是必要的
- 内存泄漏
- 可进一步通过工具查看泄漏对象到GC Roots的引用链,找到泄漏对象是通过怎样的引用路径、与哪些GC Roots相关联,才导致垃圾收集器无法回收它们
- 内存溢出
- 检查Java虚拟机的堆参数(-Xmx与-Xms)设置,与机器的内存对比,看看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况
- 内存泄漏
- 第一步 首先应确认内存中导致OOM的对象是否是必要的
- 虚拟机栈和本地方法栈溢出
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
- 如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出OutOfMemoryError异常
- -Xss栈容量
- 方法区和运行时常量池溢出 内存区域 HotSpot “PermGen space”。
- 解决一般通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析
-
来源
- 深入理解Java虚拟机
- java并发编程的艺术
网友评论