JVM-03-JVM内存分配机制

243 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

JVM内存分配机制

这一部分讲述一个对象的产生,看一下我们日常用到的对象在JVM内存中到底经历了什么样的故事。中间经历了五步,类加载检查,内存分配,初始化零值,设置对象头和执行<init>方法。

类加载检查

当一个对象被new了,或者被加载了子类而自身还没被加载或者初始化时,将会根据字节码文件将静态数据结构转化为方法区内的动态数据结构。

具体的类加载过程可以看JVM-01这篇文章。

内存分配

对象一般会分配在堆(Eden区)中,分配的方式有两种,分别是指针碰撞和空闲列表。

指针碰撞要求内存是规整的,即指针左边为已经分配的空间,右边为还没有分配的空间,在分配空间时只需要将指针右移即可分配,这种只适合垃圾回收算法为整理算法的垃圾回收器使用。

另外一种是空闲列表,这种在内存中维护了一张表,里面记录了空闲空间的地址和大小,当某个对象需要分配空间时,就在这张表上寻找合适的空间。

但是在高并发的情况下,又可能会发生并发问题,如果两个对象同时选择了一块内存,那么将会发生某一个对象被覆盖的情况,要避免这种情况JVM中有两种方法,分别是TLAB和CAS。

TLAB,全称是Thread Local Allocation Buffer。顾名思义是分配给线程的Buffer,当某一个线程需要在堆上去分配内存时,将会先分配线程在堆上的Buffer空间。这个空间当然也不能特别大,具体的大小可以通过参数 -XX:TLABSize 去进行指定。

如果TLAB空间不足的话就会使用CAS的方法,在分配空间前再次确定当前内存是否被占用,保证分配内存空间的原子性。

开头说了,对象一般会分配在堆中,那当然还有不一般的情况,那就是分配在栈上

分配栈上

上一部分讲到,栈是属于线程私有的,如果分配在栈上的话,其他线程将无法访问到。因此我们需要去判断这个对象会不会被其他线程引用,如果仅在当前方法中使用那就可以在栈上分配,这个过程叫做逃逸分析。逃逸分析可通过参数 -XX:+DoEscapeAnalysis 打开。

如果逃逸分析通过,那么JVM将不会生成该对象,由于栈上的空间较小,有可能没有连续的内存给对象分配空间,那么我们就需要进行标量替换。标量替换通过参数 -XX:+EliminateAllocations 打开。成员变量将会进行进一步的分解,最终分解为如Java基本类型这样的标量。

初始化零值

这一步和类加载时类似,不是初始化具体的实例对象变量值,而是给分配到的内存空间赋零值,保证即使对象没有初始值也可以使用。当然,这些零值并不包括对象头。

设置对象头

对象头的作用是告诉我们当前对象是哪一个类的实例,如何找到类的源信息,对象的HashCode是多少,当前对象的GC分代年龄,锁情况等信息。

对象头分为两部分,第一部分是Klass Pointer,虚拟机通过这个指针确定这个对象是哪一个类的实例。第二部分叫做MarkWord,这一部分一共4个字节,锁标志位不同,内部分配空间也不同。

截屏2022-12-02 19.02.58.png

执行<init>方法

这一部分则是按照程序员的意愿把对象进行初始化,也就是执行我们开发时定义的构造函数。

至此,一个完整的对象产生了。

总结

本部分详细地介绍了一个对象是如何产生的,从字节码最后变成内存中的对象,中间经历了五步,类加载检查,内存分配,初始化零值,设置对象头和执行<init>方法。下一部分将去关注这一个对象在堆中到底会经历些什么故事,最后会去向哪里。解决了人生三大难题的第一个从哪来,接下来将会继续探寻我在哪,和向哪去的问题。