JVM-对象的组成以及内存分配

1,054 阅读5分钟

一、对象的组成

说到对象,我们第一反应想到的就是new一个对象,想当然的认为对象就仅仅是类的实例,其中包含类的成员变量。

我们可曾思考过,对象在JVM内部究竟是怎么组成的呢?内部又具体包含什么呢?

如果这里你有疑问,那么请耐心的往下看看

一个对象由三部分构成:

  1. 对象头: Mark Word、Klass Point、数组长度(如果是数组的话)
  2. 实例数据:类的实例信息
  3. 对齐填充:JVM要求Java对象的大小必须是8byte的倍数,这部分就是将对象大小补充为8字节的倍数

下面我们来看一张图:

image.png

  1. Mark Word:记录了该对象锁相关的信息、分代年龄、hashCode,在32位JVM中占32bit,在64位JVM中占64bit image.png

  2. Klass Point:指向方法区对应class信息的指针,在32位JVM中占32bit,在64位JVM中占64bit(开启指针压缩的情况下占32bit)

  3. 数组长度: 如果是数组的话,组成中会包含数组长度,32bit

二、 指针压缩?

image.png

在32位系统中,class pointer占4个字节(32bit),可以表示(2^32)个地址

在64位系统中,class pointer占8个字节(32bit),可以表示(2^64)个地址

因为CPU的最小寻址单位是byte,所以32位系统最大能表示(2^32)byte的对象(4/8=GB),64位系统最大能表示(2^64)byte的对象(2^34GB)

缺点:

在64位JVM下,指针长度会翻倍,导致对象变大,会占用更多的内存,增加了GC开销,且CPU缓存对象减少,降低了CPU缓存命中。

指针压缩原理

如果最小寻址单位是bit,即32位系统最大能表示(2^32)bit的对象(4/8=0.5GB),64位系统最大能表示(2^64)byte的对象((2^34/8)=2^31GB)

那么同理,java运用对齐填充的机制,保证对象大小通过8字节对齐,只能是8字节的整数倍,即java的寻址单位是8byte, 在64位系统中指针压缩之后,class pointer占4个字节(32bit),可以表示(2^32)个地址,即最大能表示((2^32)*8)byte的对象(32GB)

为什么堆内存不要超过32GB?

一旦堆内存超过32G内存,就超过了指针压缩之后的32G的寻址范围,此时指针压缩会失效。即便有足够的内存,也尽量不要超过32GB。因为它浪费了内存,降低了 CPU 的性能,还要让GC应对大内存。

三、逃逸分析、标量替换

我们上篇文章讲了JVM内存模型,我们先看一下对象内存分配的流程图 image.png

逃逸分析

顾名思义,分析某一个对象有没有逃离它所在的方法,判断该对象是否会被方法外的其他地方所引用

如图:

public class MyTest {

    public static void main(String[] args) {
        math();
    }

    public static void math(){
        A a = new A();
    }
}

main()方法没有返回值,可判断出A对象没有被外面所引用,即发生了逃逸分析

对发生了逃逸分析的对象,如果对应的栈帧有足够的内存会放在栈上,即该对象会随同栈帧(当前方法)的生命周期结束而销毁

优点:很大程度上节省了堆区的空间,降低了GC的次数。

标量替换

通过逃逸分析确定该对象不会被外界所引用,并且该对象可以进一步被分解时,则JVM不会在栈上创建该对象,而是将该对象的一些被该方法所使用到的成员变量脱离出来,分配在栈帧或者寄存器上

优点

  1. 一定程度上节省了栈帧中的内存
  2. 一定程度上避免了没有连续内存空间存储的问题

总结

标量替换的前提是开启了逃逸分析,jdk8之后默认开启了逃逸分析和标量替换

四、指针碰撞&&空闲列表

1. 指针碰撞

如果Java堆中内存是连续的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点,那么分配内存就仅仅是需要把指针向空闲空间挪动一段与对象大小相等的距离 image.png

2. 空闲列表

如果Java堆中的内存不是连续的,已使用的内存和空闲的内存相互交错,那就没有办法进行指针碰撞了,虚拟机就会维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录 image.png

3. 指针碰撞和空闲列表的并发问题

不管是指针碰撞,还是空闲列表,在操作指针或更新列表的时候,都可能会出现并发问题,那JVM是怎么解决的呢?

  1. CAS: 它是乐观锁的一种实现方式(不加锁,失败重试), 且本身具有原子性。
  2. TLAB: 全称是Thread Local Allocation Buffer,即线程本地分配缓存区,为每一个线程在Eden区预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用CAS进行内存分配