一. 类的加载过程
1. 类的加载过程大致是个什么过程?
我们编写产生.java
文件,这些.java
文件经过Java编译器编译成拓展名为.class
的文件,.class
文件中保存着Java代码经转换后的虚拟机指令,我们需要将类的.class
文件通过类加载器加载成为二进制流进入内存,即JVM运行时数据区中的方法区中成为一种数据结构
。然后,在运行时数据区中(没有明确规定是在堆中)生成一个java.lang.Class
对象,这个对象用来封装加载到方法区的类的数据结构,便于向Java开发者提供用来访问方法区中类的数据结构的接口。
再来先放一张刻在Java程序员DNA里的图便于直观的讨论
2. 类的具体加载过程
2.1 加载(Loading)
- 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等)
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
2.2 验证(Verification)
验证是连接阶段的第一步,这一过程的目的是为了确保.Class
文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合
.Class
文件格式的规范(例如,是否以魔术0xCAFEBABE
开头(cafe babe咖啡宝贝,就问你够不够骚气)、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型) - 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(例如:这个类是否有父类,除了
java.lang.Object
之外) - 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
- 符号引用验证:确保解析动作能正确执行
一个正儿八经的.class
文件字节码格式要求如下,以下数据项的数量和顺序都是严格限制死的
u2,u4,u8
分别对应2字节,4字节,8字节无符号数
以_info
结尾表明该数据是表形式
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interface_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | method_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.3 准备(Preparation)
准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,是连接阶段的第二步,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值。
假设一个类变量的定义为: public static int value = 123;
那么,变量value在准备阶段过后的值为0
而不是123
因为这时候尚未开始执行任何Java方法,而把value赋值为123的 public static
指令是在程序编译后,存放于类构造器<clinit>()
方法之中的,所以把value赋值为123的动作将在初始化阶段才会执行。
注意:如果是static final
类型的常量属性,它会被直接赋予所给定的初始值
假设一个常量定义为:public static final int constValue = 123;
那么,变量constValue准备阶段后的值就为123
原因是在加载生成.class
字节码文件时,在attributes
属性表中有一个ConstantValue
属性类型,如果类变量是static final
修饰的常量,就会生成一个ConstantValue
属性来进行初始化。
2.4 解析(Resolution)
解析阶段连接的最后一部,也是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
2.5 初始化(Initialization)
类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)。
在准备阶段,变量已经赋过一次系统要求的初始值(零值);而在初始化阶段,则根据开发者通过程序制定的主观计划去初始化类变量和其他资源,或者更直接地说:初始化阶段是执行类构造器<clinit>()
方法的过程。<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}
中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量。
(PS:类构造器使用<clinit>()
进行初始化,而实例构造器使用的是<init>()
方法
注意:如果该类的直接父类还没有被初始化,那么先初始化其直接父类,也就是先执行父类的<clinit>
方法
3.类的加载器
在虚拟机提供了三种类加载器:
- 启动(Bootstrap)类加载器
- 扩展(Extension)类加载器
- 系统(System)类加载器(也称应用类加载器)
3.1 启动类加载器(Bootstrap)
当我们每天打开自己的电脑时,第一个运行的就是我们的引导程序,即Bootstrap,所以看这加载器的名字就知道,是一个非常底层的类加载器,它主要加载的是JVM自身需要的类,这个类是由C++
实现的,是虚拟机自身的一部分。
它负责将 <JAVA_HOME>/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar
,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的,出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
启动类加载器是无法被Java程序直接引用的。
3.2 扩展类加载器(Extension)
该加载器由sun.misc.Launcher$ExtClassLoader
实现,它负责加载 JDK\jre\lib\ext
目录中,或者由 java.ext.dirs
系统变量指定的路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
3.3 系统类加载器(System/Application)
该类加载器由 sun.misc.Launcher$AppClassLoader
来实现,它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath
路径,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
注意:
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。
3.4 双亲委派模型
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
注意不同层的类加载器不是继承关系,而是通过组合实现类加载器调用上一级加载功能。
双亲委派机制:
1、当 AppClassLoader
加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
2、当 ExtClassLoader
加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader
去完成。
3、如果 BootStrapClassLoader
加载失败(例如在 $JAVA_HOME/jre/lib
里未查找到该class),会使用 ExtClassLoader
来尝试加载;
4、若ExtClassLoader
也加载失败,则会使用 AppClassLoader
来加载,如果 AppClassLoader
也加载失败,则会报出异常 ClassNotFoundException
。
实现代码:
protected sychronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//检查请求的类是否已经被加载
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){
c = parent.loadClass(name, false);
}
else{
c = findBootstrapClassOrNull(name);
}
}
catch(ClassNotFoundException e){
//父类无法完成加载
}
if(c == null){
//父类无法加载,调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}
二. 创建对象的过程
当一个对象被创建时,虚拟机就会为其在堆中分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏但也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。(从父类到子类)
一个对象创建过程的伪代码,instance = new Singleton();
memory = allocate(); //1.分配内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址
由于JVM的重排序,又可能步骤2和步骤3的过程互换。
-
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求
Object
对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用 -
我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用普通代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。
实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。所以从最终的初始化顺序来看,这些初始化操作在构造器之前完成。 -
类构造器
<clinit>()
与实例构造器<init>()
不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()
执行之前,父类的类构造<clinit>()
执行完毕。由于父类的构造器<clinit>()
先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。
特别地,类构造器<clinit>()
对于类或者接口来说并不是必需的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()
。此外,在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器<clinit>()
最多会被虚拟机调用一次,而实例构造器<init>()
则会被虚拟机调用多次,只要程序员还在创建对象。 -
一个实例变量在对象初始化的过程中会被赋值几次?
我们知道,JVM在为一个对象在堆中分配完内存之后,会给每一个实例变量赋予默认零值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x
的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x
做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x
做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。当然了,一般不会这样做,除非特殊情况。
总的来说就是:父类的类构造器<clinit>()
->子类的类构造器<clinit>()
-> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数
有关Java静态域、块
,非静态域、块
、构造函数
的初始化顺序?
对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序以此是(静态变量、静态初始化块
)>(实例变量、初始化块
)> 构造器
。静态代码块是在类加载时自动执行的,非静态代码块是在创建对象时自动执行的代码,不创建对象不执行该类的非静态代码块。
静态代码块 与 静态方法?
一般情况下,如果有些代码必须在项目启动的时候就执行的时候,需要使用静态代码块,这种代码是主动执行的;需要在项目启动的时候就初始化,在不创建对象的情况下,其他程序来调用的时候,需要使用静态方法,这种代码是被动执行的。
两者的区别就是:静态代码块是自动执行的;静态方法是被调用的时候才执行的。
作用:静态代码块可用来初始化一些项目最常用的变量或对象;静态方法可用作不创建对象也可能需要执行的代码;