Java对象创建的过程

一、类加载过程

  1. 类加载检查
    • 当Java虚拟机(JVM)遇到new关键字时,它会先检查要创建的对象类是否已经被加载、链接和初始化。如果尚未加载,JVM会通过类加载器(ClassLoader)加载对应类的.class文件。
  2. 类加载
    • 类加载包括三个子步骤:加载、连接、初始化。
      • 加载:通过权限定类名,读取 class 文件内容为二进制流;二进制流转换成方法区(永久代或元数据区)的运行时 C++类字节码对象 Klass;最后再在堆上生成一个 Class 对象,用来间接获取元数据区的类定义信息,静态对象也保存在 Class 对象中。
      • 连接
        • **验证:**验证文件格式、字节码元数据、语法、符号引用;
        • 准备:为类的静态变量分配内存,并赋予默认初始值(例如0或null),但不会执行任何实际的初始化代码。
        • **解析:**将符号引用替换为直接引用。
      • 初始化:类的静态变量赋予正确的初始值,执行静态块;可能涉及父类初始化。
  3. 内存分配
    • JVM为新创建的对象分配内存空间。对象内存主要包括对象头、实例数据以及可能的对齐填充。
      • 对象头:存储对象自身的元数据如哈希码、锁状态标志、GC分代年龄等信息,以及指向其类元数据(Class对象)的指针。
      • 实例数据:存储类中定义的字段的实际数据。
      • 对齐填充:非必须,为了满足JVM对内存地址对其的要求而填充的额外空间。
  4. 初始化零值
    • 分配内存后,JVM会对对象的所有字段(包括实例变量)分配默认的初始值。
  5. 显式初始化:接下来,如果有在字段声明时直接赋予的初始值(例如int a = 10;),这些值会在构造函数执行前被赋予相应的变量。
  6. **对象头 必要信息设置 **主要是对象头中类的源数据信息,哈希码 ,对象 GC 分代年龄等。
  7. 初始化
    • 构造器初始化:调用类的构造方法(即构造器)进行初始化,执行构造器中的初始化代码,此时才会给实例变量赋予程序员指定的初始值。
    • 如果类中有父类并且还没有被初始化,则先初始化父类。
  8. 对象构造完成
    • 构造方法执行完毕后,对象就完全构造出来了,可以被程序正常使用。

二、对象内存分配方式

内存分配方式根据不同的收集器策略可分为两种,不同的收集器的堆内存规整程度不一致所以有两种分配策略。

指针碰撞 Bump The Pointer

在使用指针碰撞策略时,Java堆被假设为一个连续的内存空间,被分为已用和未用两部分,中间由一个指针作为分界线。当新对象需要内存时,JVM只需将指针向未用空间一侧移动与对象大小相等的距离即可。这种方式适用于使用标记-清除复制算法的垃圾收集器,因为这些算法能够整理出连续的内存空间。

空闲列表 Free List

如果Java堆中的内存不是连续的,或者已被使用的内存和未被使用的内存相互交错(这种情况通常发生在使用标记-整理或分代收集算法的垃圾收集器中),那么空闲列表策略更为适用。在这种情况下,JVM维护一个列表来记录堆中各个小块的可用内存空间。当新对象需要内存时,JVM会从列表中找到一个足够大的空闲块分配给对象,并更新列表。这种方法不需要连续的内存空间,但管理成本相对较高。

三、内存分配的安全问题

堆是线程间共享的一块儿区域,所以多个线程同时创建对象时都涉及对内存空间的申请和分配,那么内存分配就可能出现线程安全问题。依赖以下机制解决多线程安全问题,一种是** CAS 乐观锁机制**,一种是 TLAB 本地线程分配缓冲机制

**Thread Local Allocation Buffer (TLAB) 本地线程分配缓冲:**每个线程有一个属于自己的预分配内存空间,JVM 首先通过 CAS 为线程申请一块儿预分配内存。这样当某个线程需要申请新的内存空间时首先现在自己的 TLAB 上分配,能减少内存分配冲突。后续 TLAB 内存不足了才会 CAS 申请一块儿新的 LATB 或者直接在 Eden 区直接分配。

**Compare-and-Swap (CAS)**在某些JVM实现中,可能会使用CAS操作来实现无锁的线程安全内存分配。CAS是一种硬件级别的原子操作,允许线程在不加锁的情况下比较并交换内存中的值,从而减少锁带来的性能开销,并能有效防止数据竞争。

四、对象如何进入老年代

  1. 新生代:刚创建的对象默认进入新生代的 Eden 区
  2. 进入老年代的条件:四种情况
    1. 熬过了多次 minorGC ,每次 MinorGC 过后对象的年龄就会+1,存活超过 15 次之后就会进入老年代。该次数可通过参数控制 -XX:MaxTenuringThreshold
    2. 动态年龄判断机制:MinorGC 后,如果 Survivor 区中的一批对象大雨了这块 Survivor 区的 50%就会将大于等于这批对象年龄最大值的所有对象直接进入老年代。
      1. 举例 S1 中有 年龄为 1 、2、3、4 的一批对象,其中 234 年龄的加起来超过 S1 的 50%,那么年龄大于等于 4 的对象就直接进入老年代了。
    3. Serial 和 ParNew 收集器,大对象直接进入老年代。例如大字符串和数组 可通过-XX:PertenureSizeThreshold 配置 默认为 1M
    4. MinorGC 后,存活的对象太多无法放入 Sruvivor 区域,会触发空间分配担保机制。将存活的对象移入老年代

分配担保机制(空间担保) Allocation Assurance Mechanism

什么是分配担保机制

在 JVM 中,空间分配担保机制(Space Allocation Guarantee Mechanism)是一种确保在进行垃圾收集时,有足够的空间来处理对象晋升分配 的策略。这种机制主要用于新生代垃圾收集(Minor GC)和老年代垃圾收集(Major GC 或 Full GC)之间的协调,以避免出现内存不足的情况。

:::success

用老年代的空间,来担保新生代的垃圾回收可以成功执行并腾出空间。会将新生代存活的对象转移到老年代中。保证新分配内存能直接成功。

  1. young 分区内存不足以创建新对象

:::

内存担保的原理

MinorGC前

  1. 第一步:判断老年代可用内存是否小于新时代对象全部对象大小,如果小于则继续判断,大于则可进行MainorGC
  2. 第二步:老年代小于存活对象,则判断老年代内存是否小于每次MinorGC后进入老年代的平均大小
    1. 小于平均大小,则进行FullGC,再判断是否能保存得下存活对象,放不下则OOM
    2. 大于平均大小,则进行MinorGC

MinorGC后

  1. 如果存活对象小于Survivor区,则直接进入Survivor区
  2. 如果存活对象大于Survivor区,但是小于老年代可用内存,则直接进入老年代
  3. 如果存活对象大于Survivor区,还大于老年代,则尝试进行一次FullGC ,FullGC后再次判断,如果放不下存活对象则会OOM

分配担保的配置

  1. **-XX:HandlePromotionFailure:**这个参数控制是否允许晋升失败。如果设置为 true,JVM 会在 Minor GC 时尝试晋升对象,即使老年代空间不足,也会尝试进行一次 Minor GC。如果失败,则触发 Full GC。这个参数在 Java 6 之后已经被默认取消使用。
  2. -XX:PretenureSizeThreshold:这个参数指定大对象直接在老年代分配的大小阈值。超过该阈值的对象直接分配到老年代,避免在新生代频繁复制。
  3. **-XX:MaxTenuringThreshold:**这个参数控制对象在新生代中经历多少次 GC 后晋升到老年代。较高的阈值可以减少对象晋升,但会增加新生代的 GC 频率。
  4. -XX:TargetSurvivorRatio:这个参数控制每次 Minor GC 后目标存活区(Survivor Space)的利用率。JVM 会根据这个参数调整对象晋升的阈值。

五、验证

大对象直接进入老年代

java 复制代码
/**
* 测试:大对象直接进入到老年代
* -Xmx60m -Xms60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
* -XX:PretenureSizeThreshold
*
*/
public class YoungOldArea {
    public static void main(String[] args) {
        byte[] buffer = new byte[1024*1024*20]; //20M
    }
}

-XX:NewRatio=2 新生代与老年代比值
-XX:SurvivorRatio=8 新生代中,Eden与两个Survivor区域比值
-XX:+PrintGCDetails 打印详细GC日志
-XX:PretenureSizeThreshold 对象超过多大直接在老年代分配,默认值为0,不限制

对象内存分代晋升演示

java 复制代码
/*
-Xmx600m -Xms600m -XX:+PrintGCDetails
*/
public class HeapInstance {
  public static void main(String[] args) {
    List<Picture> list = new ArrayList<>();
    while (true){
      try {
        Thread.sleep(20);
     } catch (InterruptedException e) {
        e.printStackTrace();
     }
      list.add(new Picture(new Random().nextInt(1024 * 1024)));
   }
 }
}
class Picture{
  private byte[] pixels;
  public Picture(int length){
    this.pixels = new byte[length];
 }
}

通过可视化插件可以看到

  1. Eden区满了之后,就会进行MinorGC,MinorGC时会将Survior放不下的对象存到old老年代
  2. 老年代也满了之后,发生了三次MinorGC,未释放出可用空间后,进行了三次FullGc最后抛出了OOM
相关推荐
Amumu121388 小时前
React面向组件编程
开发语言·前端·javascript
有一个好名字8 小时前
力扣-从字符串中移除星号
java·算法·leetcode
IT=>小脑虎8 小时前
Python零基础衔接进阶知识点【详解版】
开发语言·人工智能·python
wjs20248 小时前
C 标准库 - `<float.h>》详解
开发语言
zfj3218 小时前
CyclicBarrier、CountDownLatch、Semaphore 各自的作用和用法区别
java·开发语言·countdownlatch·semaphore·cyclicbarrier
2501_916766548 小时前
【JVM】类的加载机制
java·jvm
Sag_ever8 小时前
Java数组详解
java
张np8 小时前
java基础-ConcurrentHashMap
java·开发语言
早日退休!!!8 小时前
进程与线程的上下文加载_保存及内存映射
开发语言