跳到主要内容

java对象分配

对象创建主要过程

new一个对象时,执行一个new指令

01 类加载检查

先检查类是否已经被加载过,如果没有先加载类。

02内存分配

然后再分配内存空间,通常有两种

  • 指针碰撞(要求内存按序排列,分配时只是指针往后挪动)

  • 空闲列表(有记录维护空闲块,找到合适大小的分配,并更新记录)

在多个线程并发分配的时候解决争抢问题,通常也有两种

  • cas(多线程争抢,需保证分配的原子性)

  • 本地线程分配缓冲TLAB(Thread Local Allocation Buffer),提前在堆中为每个线程分配一块专属内存,线程分配时直接分配,避免争抢,jdk8默认使用TLAB方式。

03 初始化

内存分配完成,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。

04 设置对象头

hotspot虚拟机中,对象在内存里包括三部分

  • 对象头(Header)

    对象头包括三部分

    • Mark Word标记字段 32位系统4字节,64位系统8字节。用于存储对象自身的运行时数据,如hashcode、gc分代年龄、锁状态标志、线程持有的锁、偏向线程id,偏向时间戳等。

      对象不同状态,对应的对象头结构也不一样,根据锁状态,对象有5种状态:无状态、轻量级锁、重量级锁,GC标记、偏向锁
      img

    • Klass Pointer类型指针 对象指向他的类元数据的指针,开启压缩4字节,关闭压缩8字节,指向方法区中的类元数据。 img

    • 数组长度 通常是4字节,只有数组对象才会有

  • 实例数据(Instance Data)

  • 对象填充(Padding),jvm会将对象的信息按照8字节对齐,按8字节对齐,系统底层按8字节倍数对齐,读取效率高。所以对象长度是8、16、24、32这种8的倍数的长度,不足的补齐。

通过org.openjdk.jol:jol-core包,可以查看对象的内存布局。

ClassLayout layout = ClassLayout.parseInstance(new Object());
System.out.println(layout.toPrintable());
05 执行<init>方法

c++调用的init方法,按照程序员意愿进行初始化,赋值,并调用构造方法。

关于指针压缩

jdk1.6开始,64位系统,默认开始指针压缩,将Klass指针、Object对象指针进行压缩。从8字节压缩到4字节。

  • 使用较大指针在主存和缓存之间移动数据,会占用较大带宽,同时也增加了内存占用对GC造成压力

  • jvm中,4字节32位地址最大支持4G内存(2^32),不过通过压缩编码,jvm用32位地址可以支持最大32G内存。

  • 堆小于4G,不需要启用指针压缩,jvm会直接去除高32位地址,只使用低虚拟地址空间,而大于32G时,压缩指针会失效,强制以8字节64位来对java对象寻址,就会浪费很多内存。

java是怎么实现32位地址支持最大32G内存的

计算机寻址的最小单位是字节,即Byte(8bit)

4个字节,32位,可以表示2^32个地址,如果这些地址是真实的内存地址,最多能标识2^32Byte/1024/1024/1024=4G,相当于32位地址,最多能按照字节寻址找到4G的内存。

上面有讲过jvm对象,在内存中是要对齐到8字节及其倍数的,所以可以认为JVM将堆内存按照最小8字节来划分,每个8字节就只需要一个地址,即每个地址代表8字节。

那么可以算出,如果按单个字节寻址,32位可以寻址4G,那么按8个字节为一块来寻址,则可寻址的范围就变成了4G*8=32G

综上,jvm通过底层分配对象内存时以8字节为最小单位,32位地址按单字节寻址能支持最大4G,但是如果按8字节为单位寻址(一个地址有8个字节),能支持的内存就变成了4*8=32G

所以经常会有人说,jvm的内存不要超过32G。不是说不能用,是分配超过32G,指针压缩失效,只能使用64位来标识对象地址,内存浪费严重(1.5倍),并且对CPU带宽、GC回收都造成压力。

对象内存分配

整体的过程如下 img

01 栈上分配

通过逃逸分析(-XX:+DoEscapeAnalysis,JDK7后默认开启),方法内不会逃逸出去的对象,就可以在栈上分配,随着方法执行完毕栈帧出栈,对象直接回收了,可以节省大量的对空间降低GC次数。

逃逸分析之后,使用标量替换(-XX:+EliminateAllocations,JDK7后默认开启)优先分配在栈上。一个线程栈只有1M,栈帧更小,对象需要的空间比较大,不是创建一个对象直接放在栈上,而是把这个对象的成员变量分解成若干个被这个方法所使用的成员变量,把这些代替方法的变量分配到栈帧或者寄存器上。标量:不可被进一步分解量,如基本类型,reference等。

-XX:+PrintFlagsFinal # 查看java各种设置最终效果
-XX:+DoEscapeAnalysis # 逃逸分析
-XX:+EliminateAllocations # 标量替换
-XX:+PrintGC #打印GC
-XX:+PrintGCDetails #打印GC详细信息
02 大对象分配

Eden放不下的,或者超过指定阈值的,长期存活的,都会放到老年代。

-XX:+UseTLAB #启用TLAB jdk8默认开启
-XX:TLABSIZE #指定TLAB大小

参考:

8.JVM内存分配机制超详细解析 - 盛开的太阳 - 博客园

聊一聊JAVA指针压缩的实现原理(图文并茂,让你秒懂)_指针压缩原理-CSDN博客