开始我们知道对象的创建过程包括以下几步:

  1. 类加载
  2. 实例化
  3. 初始化
  4. 引用赋值

这几步一般来讲是顺序进行的,不过由于硬件层面的一些优化,可能会发生指令重排的情况,导致3、4步调换位置,我们在程序中就会看到NPE空指针异常,通常可以用volatile修饰这个变量来解决,不过我们今天主要谈谈这个实例化过程,也就是给对象分配内存的过程。

在Java程序,我们绝大多数的对象都在堆内分配内存,只有少部分的对象经过JIT的逃逸分析之后会在栈上分配内存,那对于大多数对象来说,具体是在堆内的什么位置分配内存呢,我们需要首先来看看堆内更加详细的图

image-20240302222528855

堆内的对象会被分代存储,一个是新生代(包括Eden、From、To)和一个老年代,他们的内存大小比例默认是新生代:老年代 = 2:1,在新生代内部,Eden:From:To的大小比例默认是8:1:1。

新对象一般会在Eden进行分配,除了一些大对象(数组、字符串)直接进入到老年代,这样可以减少GC后对这些大对象的移动(因为新生代使用的是标记-整理算法),在分配对象内存的时候我们需要考虑Eden还有哪些空闲的区域,这里JVM针对不同的垃圾回收器用了两种办法:

  • 指针碰撞:适合没有内存碎片的垃圾回收器
  • 空闲链表:适合有内存碎片的垃圾回收器,如CMS

同时考虑到分配对象内存的时候会有并发问题,JVM有两种机制来避免并发问题:

  • 一个是TLAB(这个是JVM给线程分配的一块私有的空间,大概是Eden的1%),在TLAB内分配对象是没有并发问题的
  • 另一个是CAS + 失败重试,利用操作系统的原子指令给对象分配内存

那如果发现Eden已经没有足够大小的内存来分配给对象怎么解决?

大致来说就是先考虑Young GC,如果是JVM判断Young GC是有风险的,那么需要用对象担保机制,最后的办法就是Full GC,如果是Full GC后依旧不够内存分配给新对象,JVM会抛出OOM异常

image-20240415095811035