Java创建对象的过程(包含类加载)
最后一次更新时间:Monday, August 24th 2020, PM
在Java中,说到创建对象,我们大多数情况下使用的都是new指令。那么明面上一条new指令,暗地里经过JVM多少处理呢?
1 类加载检查
当JVM遇到一条new指令时,先检查其参数能否在常量池中定位到该类的符号引用。再检查该符号引用是否被加载过、解析过、初始化过。
注:符号引用相当于自定义的变量名等;引用除了符号引用以外,还有直接引用。直接引用指的是指针、偏移量之类的东西。
JVM并不是编译时,就加载所有类进内存。而是第一次运行某个类时才加载,且只加载这一次。
若有,则直接进行第二步分配内存。若没有,则执行类加载。
类加载的过程分为三步:加载 -> 链接 -> 初始化。
而链接又分为三步:验证 -> 准备 -> 解析。
加载和链接实际上是交叉进行的。加载未结束,链接已开始。
不妨多了解一点,类的生命周期:加载 -> 链接 -> 初始化 -> 使用 -> 卸载
1.1 加载
加载这一步的内容是,把class文件装入内存。
在JVM中,内置的类加载器(ClassLoader)有三层,自顶向下分别是
- BootstrapClassLoader 启动类加载器(最顶层的ClassLoader,由C++实现)
- ExtensionClassLoader 扩展类加载器
- 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在创建对象时,必须保证线程安全,通常用两种方法保证。
CAS + 失败重试机制
CAS是乐观锁(乐观锁是一种思想,没有实际的锁。它假设不会出现冲突,所以不用加锁,直接去申请资源,失败就重试,直到成功为止)。CAS + 失败重试 可以保证操作的原子性,从而线程安全。TLAB
全称ThreadLocalAllocBuffer,于 HotSpot1.6 引入,原理与指针碰撞法类似。在每个线程初始化时,同时申请一块指定大小的内存绑定给它。当线程需要内存时,就在自己的这块内存上分配,若容量不够,再去Eden区正常申请。优点:每个线程都有自己的专属指针,性能高。
3 初始化零值
分配内存完成后,要把这些内存初始化为0。
所以成员变量可以不用初始化
4 设置对象头
初始化零值后,JVM要对对象实例进行设置。例如:
- 该对象是哪个类的实例
- 如何找到该类的元数据
- 对象的HashCode
- 对象的GC年龄
- 锁状态标识
这些信息都存放在对象头中。PS:实例对象分为三个部分:对象头、实例数据、对齐填充(非必须)
5 执行init()方法
从JVM的角度来看,执行完上述4步,一个新对象已经产生。
执行完new指令后紧接着执行init(),才能完成对象实例的初始化(成员变量、成员方法等)。
除特别声明外,本站所有文章均采用 CC BY-SA 4.0 协议 ,转载请注明出处!