对象创建过程
一、代码运行的三个阶段
本文章基于 官方jdk8语言规范 梳理,其实类加载创建实例化过程没有什么变化。
在研究对象创建过程,代码执行顺序之前,前置知识必须知道代码运行的三个阶段:
- source源代码阶段
- Class类对象阶段
- runtime运行时阶段
核心需要记住的是:
- 类加载阶段
- 对象创建阶段
二、类加载阶段
提供一个Test.java测试类,从虚拟机启动开始描述整个对象的加载实例化过程。
核心步骤:1.虚拟机启动—>2.加载测试类二进制文件—>3.连接(link)该测试类—>4.初始化Initialize(实例化)—>5.调用main方法—>6.Unloading卸载类—>7.退出程序
1. 虚拟机启动
Java虚拟机通过调用某个指定类或接口的main方法开始执行,向它传递一个字符串数组参数。
public class Test{
public static void main(String[] args){
System.out.println("helloWoniu");
}
}
在使用命令行的主机环境中,通常将类或接口的完全限定名指定为命令行参数,并将随后的命令行参数用作字符串,作为参数提供给方法main。
在IDE中启动该方法后,将会继续执行下面第一步。
2. 加载类Test
在第一步尝试执行类Test
的main方法时,虚拟机发现没有加载类Test
——也就是说,Java虚拟机当前不包含这个类的二进制表示。
加载
是查找具有特定名称的类或接口类型的二进制表示的过程 ,并创建二进制表示的类或接口的动作。
然后,Java虚拟机使用类加载器来尝试找到这样的二进制表示。如果这个过程失败,就会抛出一个错误。
该类加载过程,在后续进行解释。
本操作对应图中的内容:
其中,类加载过程做了三件事::
1.通过一个类的全限定名来获取定义此类的二进制字节流
- 从zip(jar)包获取
2.将这个字节流所代表的静态存储结构转化为方法区(解释见2.1)的运行时数据结构(解释见2.2)
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
想要明白上文提到的类加载,需要了解额外知识方法区,运行时常量池,符号引用(栈帧中的动态连接需要)。下方内容较难,可以跳过。
2.1 方法区
Java虚拟机有一个方法区域它由所有Java虚拟机线程共享。方法区类似于传统语言的编译代码的存储区,或者类似于操作系统进程中程序存储区的“文本”段(text segment)。
程序存储区:除了text segment,额外还有data segment和bss segment(Block Started by Symbol)。
方法区存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法(<init>
和JDK1.7的<clinit>
)用于类和实例初始化以及接口初始化。
方法区域是在虚拟机启动时创建的。
尽管方法区域在逻辑上是堆的一部分,但简单的实现可能选择不进行垃圾收集或压缩。JDK规范中并不强制要求方法区域的位置或用于管理编译代码的策略。方法区域可以是固定的大小,或者可以根据计算的需要进行扩展,并且如果不需要更大的方法区域,可以进行收缩。
方法区域的内存不需要连续。
所以,根据策略,在JDK1.8开始,使用的Metaspace元空间作为方法区的实现。从堆空间移动到了直接内存(JVM虚拟机以外的计算机剩余物理内存),但是还是可以手动设置元空间的大小,通过这个命令进行:-XX:MaxMetaspaceSize。
额外扩展:
通常元空间内存大小虽然是直接内存,但是核心还是由CompressedClassSpaceSize(配置类空间容量)这个默认的1G大小来控制的,所以我们唯一会触碰到的是 Class Space 空间的上限。
一个类,放入元空间中,占用部分分为 Class Space和Non-Class Space。
元空间可以设置整个计算机内存(除去其他必备内存之外),可它同时又受到类空间容量限制,按每个类约 5-7k 的 Non-Class Space 和 600-900 bytes 的 Class Space,我们可以估算出大约 1-150万个类(假设没有碎片、没有浪费)以后会触碰到 Class Space 的 OOM。
Java虚拟机实现(JDK1.8的元空间就是实现)可以为程序员或用户提供对方法区域初始大小的控制,以及在可变大小的方法区域的情况下,对最大和最小方法区域大小的控制。
以下异常情况与方法区域相关联:
- 如果方法区域中的内存无法满足分配请求,Java虚拟机将抛出一个
OutOfMemoryError
。
2.2 运行时常量池
Java虚拟机维护每种类型的常量池(见下表),一种运行时数据结构,它服务于传统编程语言实现的符号表。
每个运行时常量池都是在Java虚拟机的方法区域中。类或接口的运行时常量池是在创建类或接口时通过Java虚拟机构造的。
Constant Type | Value |
---|---|
CONSTANT_Class | 7 |
CONSTANT_Fieldref | 9 |
CONSTANT_Methodref | 10 |
CONSTANT_InterfaceMethodref | 11 |
CONSTANT_String | 8 |
CONSTANT_Integer | 3 |
CONSTANT_Float | 4 |
CONSTANT_Long | 5 |
CONSTANT_Double | 6 |
CONSTANT_NameAndType | 12 |
CONSTANT_Utf8 | 1 |
CONSTANT_MethodHandle | 15 |
CONSTANT_MethodType | 16 |
CONSTANT_InvokeDynamic | 18 |
3.连接Test类: 验证 ,准备, (可选) 解析
类或接口在连接之前,必须是完全加载的,接下来的连接过程,在初始化开始之前的操作。
连接是获取类或接口的二进制形式,并将其组合到Java虚拟机的运行时状态中,以便可以执行的过程。
验证检查加载的Test
是格式良好的,带有适当的符号表。验证还检查实现Test
遵循Java编程语言和Java虚拟机的语义要求。如果在验证过程中检测到问题,就会抛出一个错误。
准备包括静态存储和Java虚拟机实现内部使用的任何数据结构的分配,比如方法表、常量池。
解析是检查符号引用
的过程Test
加载提到的其他类和接口,并检查引用是否正确。
4.初始化Test类: 执行初始值设定
初始化对于类或接口来说,就是执行它的类或接口初始化<init>
方法。
类或接口C只能在以下情况下初始化:
-
任何一个Java虚拟机指令的执行new, getstatic, putstatic,或者invokestatic引用了类。
在执行new指令时,如果被引用的类尚未初始化,则初始化该类。
在执行getstatic, putstatic,或者invokestatic指令中,声明已解析的字段或方法的类或接口(如果尚未初始化)将被初始化。
-
第一次调用
java.lang.invoke.MethodHandle
实例 -
类库中某些反射方法的调用。
-
如果C是一个类,它的一个子类的初始化。
-
如果C是一个接口,它声明一个非
abstract
,非static
方法,直接或间接实现了C接口。 -
如果C是一个类,它被指定为Java虚拟机启动时的初始类。
在初始化之前,必须链接一个类或接口,即验证、准备和有选择地解析。
因为Java虚拟机是多线程
的,所以类或接口C的初始化需要小心的同步(synchronization),因为一些其他线程可能同时试图初始化相同的类或接口。还存在这样的可能性,继承后的递归实例化请求。
Java虚拟机的实现通过使用以下过程负责同步和递归初始化。它假设Class
对象已经过验证和准备,并且Class
对象包含指示以下四种情况之一的状态:
- 这
Class
对象已验证并准备好,但未初始化。 - 这
Class
对象正被某个特定的线程初始化。 - 这
Class
对象已完全初始化,可以使用了。 - 这
Class
对象处于错误状态,可能是因为初始化尝试失败。
重点来了:LC锁。
对于每个类或接口C,有一个唯一的初始化锁LC
。从C到LC
的映射关系由Java虚拟机实现来决定。举个例子,LC
可能是C的类对象或与之相关联的监视器锁。
初始化的过程C是这样的:
-
在C类或接口实例化时,获取初始化锁LC用于同步。这同步过程会出现等待,直到当前线程可以获取
LC
. -
如果其他线程正在对
Class
的对象进行初始化,然后释放LC
之前,当前线程将会阻塞,直到其他线程初始化已经完成后,当前线程再重复该初始化过程。线程中断状态不受初始化过程执行的影响。
-
如果C的
Class
对象显示正在对其进行递归的初始化请求,同时释放LC
并正常完成初始化。 -
如果C的
Class
对象显示C已经初始化完成,则不需要进一步的操作,直接释放LC
并正常完成初始化。 -
如果
Class
的对象C处于错误状态,则初始化是不可能完成的。直接释放LC
抛出一个错误NoClassDefFoundError
. -
除此(前面几条)之外,记录下当前线程正在初始化C的类对象,并释放
LC
.然后,按照字段在类文件结构中出现的顺序,初始化final static属性为默认的常量值(0或者null);
-
接下来,如果C是一个类而不是一个接口,并且它的任意一个父类还没有初始化成功。
如果其中一个父类的初始化由于抛出异常而突然结束,那么获取LC,将C的类对象标记为错误,通知所有等待的线程。然后释放LC,抛出与初始化父类相同的异常。
-
接下来,通过查询C类定义的类加载器,来确定C是否启用了断言。
-
接下来,执行的类或接口初始化方法C.
-
如果类或接口初始化方法的执行正常完成,则获取
LC
,标记Class
的对象C完全初始化后,通知所有等待的线程后,释放LC
,并正常完成此初始化过程。 -
否则,类或接口初始化方法一定是通过抛出某个
异常E
而突然完成。如果的异常E
不是Error
或者它的一个子类,然后创建该类的一个新实例ExceptionInInitializerError
随着E作为参数,并使用此对象代替E在接下来的步骤中。如果的新实例ExceptionInInitializerError
无法创建,因为OutOfMemoryError
发生时,请使用OutOfMemoryError
对象来代替E在接下来的步骤中。 -
获得
LC
,标记Class
的对象C作为错误,通知所有等待线程,释放LC
,抛出上一步确认的异常。
当Java虚拟机实现可以确定类的初始化已经完成时,它可以省略步骤1中的锁获取(以及步骤4/5中的释放同步锁操作)来优化该过程。这种优化策略必须先确认:类以及完成初始化,并且在此情况,从java内存模型的角度看,获取同步锁时,具备的那些happens-before顺序规则(),在执行优化有仍能得到遵守。
5. 使用
无……
6. 卸载
当某个线程调用Runtime
类或System
类的exit方法,或Runtime
的halt
方法,并且java安全管理器也允许本次exit
或者halt
操作。
此外,使用JNI(Java Native interface)规范调用其API来加载或卸载Java虚拟机时,Java虚拟机的终止。
三、对象创建过程
1. 原理图
当new一个对象,进行实例化,复杂版的对象创建过程是:
简略版对象创建过程:
2. 代码执行流程
2.1 代码块和构造方法
根据简略版原理图,首先确认代码块和构造方法的执行时机:
package com.j89class;
public class SonClazz {
String name;
int age;
{
System.out.println("SonClazz.instance initializer:"+name+"---"+age);
}
public SonClazz() {
this.age=6;
this.name="蜗牛学苑";
System.out.println("SonClazz.SonClazz:"+name+"---"+age);
}
public static void main(String[] args) {
new SonClazz();
}
}
//打印结果:
//SonClazz.instance initializer:null---0
//SonClazz.SonClazz:蜗牛学苑---6
2.2 静态代码块
执行顺序:静态代码块 > 代码块 > 构造方法
静态代码块在类加载过程执行,只能对静态属性进行赋值操作。未赋值则为默认值,在
准备
阶段进行初始化默认值。
package com.j89class;
public class SonClazz{
String name;
int age;
static int aa;
static {
System.out.println("静态代码块执行赋值,只能对静态属性赋值:"+aa);
aa=123;
System.out.println("静态代码块执行赋值,只能对静态属性赋值:"+aa);
}
{
System.out.println("普通代码块");
}
public SonClazz() {
this.age=6;
this.name="蜗牛学苑";
System.out.println("SonClazz.SonClazz:"+name+"---"+age);
}
public static void main(String[] args) {
new SonClazz();
}
}
执行结果:
静态代码块执行赋值,只能对静态属性赋值:0
静态代码块执行赋值,只能对静态属性赋值:123
普通代码块
SonClazz.SonClazz:蜗牛学苑---6
如果静态代码块中,new对象,会发生什么呢?
修改上述代码一部分:
static {
new SonClazz();
System.out.println("静态代码块执行赋值,只能对静态属性赋值:"+aa);
aa=123;
System.out.println("静态代码块执行赋值,只能对静态属性赋值:"+aa);
}
打印结果:
普通代码块
SonClazz.SonClazz:蜗牛学苑---6
静态代码块执行赋值,只能对静态属性赋值:0
静态代码块执行赋值,只能对静态属性赋值:123
普通代码块
SonClazz.SonClazz:蜗牛学苑---6
说明类加载过程和实例化对象过程,并非是同步操作,而是异步的。并不会因为加载初始化赋值问题,而无法实例化。不过,普通代码块中,拿到的数据依然都是默认值(包括静态属性)。
2.3 静态属性对象
package com.j89class;
public class SonClazz{
String name;
int age;
static int aa;
static SonClazz sonClazz = new SonClazz("属性",123);
static {
new SonClazz("静态代码块实例化对象",123);
System.out.println("静态代码块执行赋值,只能对静态属性赋值:"+aa);
aa=123;
System.out.println("静态代码块执行赋值,只能对静态属性赋值:"+aa);
}
{
System.out.println("普通代码块");
}
public SonClazz() {
this.age=6;
this.name="蜗牛学苑";
System.out.println("SonClazz.SonClazz:"+name+"---"+age);
}
public SonClazz(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造,查看静态属性顺序:"+this.name+"--"+this.age);
}
public static void main(String[] args) {
new SonClazz();
}
}
提供一个有参构造,使用静态属性实例化对象,和静态代码块实例化对象,得出结论:
静态代码块和静态属性,谁在前,先执行谁。
普通代码块
有参构造,查看静态属性顺序:属性--123
普通代码块
有参构造,查看静态属性顺序:静态代码块实例化对象--123
静态代码块执行赋值,只能对静态属性赋值:0
静态代码块执行赋值,只能对静态属性赋值:123
普通代码块
SonClazz.SonClazz:蜗牛学苑---6
交换静态代码块和静态属性位置:再次测试,验证结论;
静态代码块和静态属性是安装顺序
从上往下执行
。
普通代码块
有参构造,查看静态属性顺序:静态代码块实例化对象--123
静态代码块执行赋值,只能对静态属性赋值:0
静态代码块执行赋值,只能对静态属性赋值:123
普通代码块
有参构造,查看静态属性顺序:属性--123
普通代码块
SonClazz.SonClazz:蜗牛学苑---6
2.4 有父类情况
有父类的情况:new一个子类,代码如下:
package com.j89class;
class FatherClazz{
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类代码块!");
}
public FatherClazz() {
System.out.println("父类构造方法");
}
}
public class SonClazz extends FatherClazz{
String name;
int age;
static int aa;
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类代码块");
}
public SonClazz() {
this.age=6;
this.name="蜗牛学苑";
System.out.println("子类构造方法");
}
public static void main(String[] args) {
new SonClazz();
}
}
顺序如下:
父类静态代码块
子类静态代码块
父类代码块!
父类构造方法
子类代码块
子类构造方法
静态代码块在类加载的时候执行,优先执行父类!
实例化对象过程,优先实例化父类。
也就是现有父,才能有子。
2.5 父类+子类静态属性对象
子类添加静态属性static SonClazz sonClazz = new SonClazz(); 且静态属性实例化 放在 静态代码块之
前
;这种情况比较特殊:子类静态代码块将
不会
再父类构造方法之前执行。
package com.j89class;
class FatherClazz{
static {
System.out.println("父类静态代码块");
}
{
System.out.println("父类代码块!");
}
public FatherClazz() {
System.out.println("父类构造方法");
}
}
public class SonClazz extends FatherClazz{
String name;
int age;
static SonClazz sonClazz = new SonClazz();
static {
System.out.println("子类静态代码块");
}
{
System.out.println("子类代码块");
}
public SonClazz() {
this.age=6;
this.name="蜗牛学苑";
System.out.println("子类构造方法");
}
public static void main(String[] args) {
new SonClazz();
}
}
结果如下:
父类静态代码块
父类代码块!
父类构造方法
子类代码块
子类构造方法
子类静态代码块
父类代码块!
父类构造方法
子类代码块
子类构造方法
如果子类添加静态属性static SonClazz sonClazz = new SonClazz(); 且静态属性实例化 放在 静态代码块之
后
;同时静态代码块只会执行一次。
结果如下:
父类静态代码块
子类静态代码块
父类代码块!
父类构造方法
子类代码块
子类构造方法
父类代码块!
父类构造方法
子类代码块
子类构造方法
再次说明,并非是实例化对象就会调用静态代码块,其实还分情况,如果静态属性在静态代码块之前加载,则会先实例化对象,跳过子类静态代码块的执行。
这个答案并非一定是正确的,且听且理解:
类加载和实例化对象并非是同步操作,可能会在加载的时候,判断需要实例化对象,于是,优先进行实例化操作,为
连接
过程中的解析
步骤,提供一个实例化对象的地址,方便把符号引用转换为直接引用,随后进行下一步静态代码块的执行,以及后续的初始化操作。