Java创建对象的过程(包含类加载)

最后一次更新时间:Monday, August 24th 2020, PM

 

在Java中,说到创建对象,我们大多数情况下使用的都是new指令。那么明面上一条new指令,暗地里经过JVM多少处理呢?

1 类加载检查

当JVM遇到一条new指令时,先检查其参数能否在常量池中定位到该类的符号引用。再检查该符号引用是否被加载过、解析过、初始化过。

注:符号引用相当于自定义的变量名等;引用除了符号引用以外,还有直接引用。直接引用指的是指针、偏移量之类的东西。

JVM并不是编译时,就加载所有类进内存。而是第一次运行某个类时才加载,且只加载这一次。

若有,则直接进行第二步分配内存。若没有,则执行类加载

类加载的过程分为三步:加载 -> 链接 -> 初始化
而链接又分为三步:验证 -> 准备 -> 解析

加载和链接实际上是交叉进行的。加载未结束,链接已开始。

不妨多了解一点,类的生命周期:加载 -> 链接 -> 初始化 -> 使用 -> 卸载

1.1 加载

加载这一步的内容是,把class文件装入内存。

在JVM中,内置的类加载器(ClassLoader)有三层,自顶向下分别是

  1. BootstrapClassLoader 启动类加载器(最顶层的ClassLoader,由C++实现)
  2. ExtensionClassLoader 扩展类加载器
  3. AppClassLoader 应用程序类加载器

除了三层内置的ClassLoader以外,JVM还允许用户自定义类加载器(最底层)。

实际上,每一个类都有适合它的类加载器。如何快速的找到该类加载器?类加载器在协同工作时,默认会使用双亲委派模型(Parents Delegation Model)

双亲委派模型(Parents Delegation Model)

这里的“双亲”指的是“父母那一辈”的意思。

什么是双亲委派模型:
在类加载的时候,首先把该请求委派给该父类加载器loadClass()处理。
因此所有的请求最后都会传到BootStrapClassLoader尝试处理。当父类加载器无法处理时,才会由自己处理。
若父类加载器为null,会自动把BootstrapClassLoader作为父类加载器。

注意:类加载器的“父子”关系并不是继承,而是优先级!

优点:可以避免类的重复加载,也保证Java核心API不被篡改(相同的类被不同的ClassLoader加载可能产生不同的类)。

如果不想使用双亲委派模型,可以自定义一个ClassLoader(继承自java.lang.ClassLoader),然后重写loadClass()

1.2 验证

保证加载的字节流符合规范。

1.3 准备

为static变量(非实例变量)分配内存,并赋值。

1.4 解析

将常量池的符号引用替换为直接引用。

1.5 初始化

初始化static代码块,构造器等。

2 分配内存

所需的内存大小,在类加载完成后,便可以确定。

内存分配的方式有两种,分别是指针碰撞法以及空闲列表法

分配完内存,就会在内存上创建实例化对象。

JVM在创建对象时,必须保证线程安全,通常用两种方法保证。

  1. CAS + 失败重试机制
    CAS是乐观锁(乐观锁是一种思想,没有实际的锁。它假设不会出现冲突,所以不用加锁,直接去申请资源,失败就重试,直到成功为止)。CAS + 失败重试 可以保证操作的原子性,从而线程安全。

  2. TLAB
    全称ThreadLocalAllocBuffer,于 HotSpot1.6 引入,原理与指针碰撞法类似。在每个线程初始化时,同时申请一块指定大小的内存绑定给它。当线程需要内存时,就在自己的这块内存上分配,若容量不够,再去Eden区正常申请。优点:每个线程都有自己的专属指针,性能高。

3 初始化零值

分配内存完成后,要把这些内存初始化为0。
所以成员变量可以不用初始化

4 设置对象头

初始化零值后,JVM要对对象实例进行设置。例如:

  1. 该对象是哪个类的实例
  2. 如何找到该类的元数据
  3. 对象的HashCode
  4. 对象的GC年龄
  5. 锁状态标识

这些信息都存放在对象头中。PS:实例对象分为三个部分:对象头、实例数据、对齐填充(非必须)

5 执行init()方法

从JVM的角度来看,执行完上述4步,一个新对象已经产生。

执行完new指令后紧接着执行init(),才能完成对象实例的初始化(成员变量、成员方法等)。


除特别声明外,本站所有文章均采用 CC BY-SA 4.0 协议 ,转载请注明出处!