一、类加载过程

- 类加载检查
- 当java虚拟机(jvm)遇到
new关键字时,它会先检查要创建的对象类是否已经被加载、链接和初始化。如果尚未加载,jvm会通过类加载器(classloader)加载对应类的.class文件。
- 当java虚拟机(jvm)遇到
- 类加载
- 类加载包括三个子步骤:加载、连接、初始化。
- 加载:通过权限定类名,读取 class 文件内容为二进制流;二进制流转换成方法区(永久代或元数据区)的运行时 c++类字节码对象 klass;最后再在堆上生成一个 class 对象,用来间接获取元数据区的类定义信息,静态对象也保存在 class 对象中。
- 连接:
- **验证:**验证文件格式、字节码元数据、语法、符号引用;
- 准备:为类的静态变量分配内存,并赋予默认初始值(例如0或null),但不会执行任何实际的初始化代码。
- **解析:**将符号引用替换为直接引用。
- 初始化:类的静态变量赋予正确的初始值,执行静态块;可能涉及父类初始化。
- 类加载包括三个子步骤:加载、连接、初始化。
- 内存分配
- jvm为新创建的对象分配内存空间。对象内存主要包括对象头、实例数据以及可能的对齐填充。
- 对象头:存储对象自身的元数据如哈希码、锁状态标志、gc分代年龄等信息,以及指向其类元数据(class对象)的指针。
- 实例数据:存储类中定义的字段的实际数据。
- 对齐填充:非必须,为了满足jvm对内存地址对其的要求而填充的额外空间。
- jvm为新创建的对象分配内存空间。对象内存主要包括对象头、实例数据以及可能的对齐填充。
- 初始化零值:
- 分配内存后,jvm会对对象的所有字段(包括实例变量)分配默认的初始值。
- 显式初始化:接下来,如果有在字段声明时直接赋予的初始值(例如int a = 10;),这些值会在构造函数执行前被赋予相应的变量。
- **对象头 必要信息设置 **主要是对象头中类的源数据信息,哈希码 ,对象 gc 分代年龄等。
- 初始化
- 构造器初始化:调用类的构造方法(即构造器)进行初始化,执行构造器中的初始化代码,此时才会给实例变量赋予程序员指定的初始值。
- 如果类中有父类并且还没有被初始化,则先初始化父类。
- 对象构造完成:
- 构造方法执行完毕后,对象就完全构造出来了,可以被程序正常使用。
二、对象内存分配方式
内存分配方式根据不同的收集器策略可分为两种,不同的收集器的堆内存规整程度不一致所以有两种分配策略。
指针碰撞 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是一种硬件级别的原子操作,允许线程在不加锁的情况下比较并交换内存中的值,从而减少锁带来的性能开销,并能有效防止数据竞争。
四、对象如何进入老年代
- 新生代:刚创建的对象默认进入新生代的 eden 区
- 进入老年代的条件:四种情况
- 熬过了多次 minorgc ,每次 minorgc 过后对象的年龄就会+1,存活超过 15 次之后就会进入老年代。该次数可通过参数控制
-xx:maxtenuringthreshold - 动态年龄判断机制:minorgc 后,如果 survivor 区中的一批对象大雨了这块 survivor 区的 50%就会将大于等于这批对象年龄最大值的所有对象直接进入老年代。
- 举例 s1 中有 年龄为 1 、2、3、4 的一批对象,其中 234 年龄的加起来超过 s1 的 50%,那么年龄大于等于 4 的对象就直接进入老年代了。
- serial 和 parnew 收集器,大对象直接进入老年代。例如大字符串和数组 可通过-xx:pertenuresizethreshold 配置 默认为 1m
- minorgc 后,存活的对象太多无法放入 sruvivor 区域,会触发空间分配担保机制。将存活的对象移入老年代
- 熬过了多次 minorgc ,每次 minorgc 过后对象的年龄就会+1,存活超过 15 次之后就会进入老年代。该次数可通过参数控制
分配担保机制(空间担保) allocation assurance mechanism

什么是分配担保机制
在 jvm 中,空间分配担保机制(space allocation guarantee mechanism)是一种确保在进行垃圾收集时,有足够的空间来处理对象晋升和分配的策略。这种机制主要用于新生代垃圾收集(minor gc)和老年代垃圾收集(major gc 或 full gc)之间的协调,以避免出现内存不足的情况。
:::success
用老年代的空间,来担保新生代的垃圾回收可以成功执行并腾出空间。会将新生代存活的对象转移到老年代中。保证新分配内存能直接成功。
- young 分区内存不足以创建新对象
:::
内存担保的原理
minorgc前
- 第一步:判断老年代可用内存是否小于新时代对象全部对象大小,如果小于则继续判断,大于则可进行mainorgc
- 第二步:老年代小于存活对象,则判断老年代内存是否小于每次minorgc后进入老年代的平均大小
- 小于平均大小,则进行fullgc,再判断是否能保存得下存活对象,放不下则oom
- 大于平均大小,则进行minorgc
minorgc后
- 如果存活对象小于survivor区,则直接进入survivor区
- 如果存活对象大于survivor区,但是小于老年代可用内存,则直接进入老年代
- 如果存活对象大于survivor区,还大于老年代,则尝试进行一次fullgc,fullgc后再次判断,如果放不下存活对象则会oom
分配担保的配置
- **-xx:handlepromotionfailure:**这个参数控制是否允许晋升失败。如果设置为 true,jvm 会在 minor gc 时尝试晋升对象,即使老年代空间不足,也会尝试进行一次 minor gc。如果失败,则触发 full gc。这个参数在 java 6 之后已经被默认取消使用。
- -xx:pretenuresizethreshold:这个参数指定大对象直接在老年代分配的大小阈值。超过该阈值的对象直接分配到老年代,避免在新生代频繁复制。
- **-xx:maxtenuringthreshold:**这个参数控制对象在新生代中经历多少次 gc 后晋升到老年代。较高的阈值可以减少对象晋升,但会增加新生代的 gc 频率。
- -xx:targetsurvivorratio:这个参数控制每次 minor gc 后目标存活区(survivor space)的利用率。jvm 会根据这个参数调整对象晋升的阈值。
五、验证
大对象直接进入老年代
/**
* 测试:大对象直接进入到老年代
* -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,不限制
对象内存分代晋升演示
/*
-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];
}
}

通过可视化插件可以看到
- eden区满了之后,就会进行minorgc,minorgc时会将survior放不下的对象存到old老年代
- 老年代也满了之后,发生了三次minorgc,未释放出可用空间后,进行了三次fullgc最后抛出了oom
到此这篇关于java对象创建的过程的文章就介绍到这了,更多相关java对象创建内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论