在Java语言中,每个类或接口都会被编译器编译程一个个class
字节码文件。
类加载则是将这些class
字节码文件的二进制数据读入到内存中,并且对数据进行校验、解析、初始化。最终,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。
- 类的生命周期需要经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载
- 类的加载过程则是前面5个阶段,分别是加载、验证、准备、解析、初始化,其中 验证、准备、解析 可以归纳为 “连接” 阶段。
加载阶段是类加载的第一个阶段。
在这个阶段,JVM的目的是将字节码从各个位置转化为二进制字节流加载到内存中,接着会为这个类在JVM的方法区创建一个对应的Class
对象,这个 Class
对象就是这个类各种数据的访问入口。
也就是在这个阶段,虚拟机需要完成下面三件事情:
class
文件class
文件内的二进制数据读取出来,转化成方法区的运行时数据结构java.lang.Class
对象,作为对方法区中这些数据的访问入口注意:Java虚拟机并没有规定类的字节流必从.class文件中加载,在加载阶段,程序员可以通过自定义的类加载器,自行定义读取的地方,例如通过网络、数据库等。
在验证阶段,JVM完成了加载class
字节码文件的步骤,并且在方法区创建对应的Class
对象之后,JVM便会对这些字节码流进行校验,只有符合JVM规范的文件才能被JVM正确执行。
验证的过程,分为以下两个类型:
0x cafe babe
开头,主次版本号是否在当前虚拟机处理范围之内等。int
类型的参数,但是使用它的时候却传入了一个 String
类型的参数。验证这个阶段虽然十分重要,但是不是必须,它对于程序的运行期没有影响。
如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用
-Xverify:none
参数来关闭大部分的类验证措施。
在准备阶段中,JVM将类变量分配内存并初始化。
Java 中的变量有类变量
和类成员变量
两种类型。「类变量」指的是被 static
修饰的变量,而其他所有类型的变量都属于类成员变量
。
在准备阶段,JVM 只会为类变量
分配内存,而不会为类成员变量
分配内存。类成员变量
的内存分配需要等到初始化阶段才开始。
public static int classVariable = 3;
public String classMemberVariable = "Java is good";
上述代码在准备阶段只会为classVariable
分配内存而不会给classMemberVariable
分配内存
在准备阶段,JVM 会为类变量
分配内存并为其初始化。这里的初始化
指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
public static int sector = 3;
上述代码中,sector
的值将会是0,而不会被赋值为3。
如果一个变量是常量(被 static final
修饰)的话,那么在准备阶段,变量便会被赋予用户希望的值。
final
关键字用在变量上,表示该变量不可变,一旦赋值就无法改变。所以,在准备阶段中,对类变量初始化赋值时,会直接赋予用户希望的值。
public static int sector = 3;
上述代码中,sector
的值将会是3
这个阶段,虚拟机会把这个Class文件中,常量池内的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。
可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程。
什么是符号引用?
Java代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会用一个符号引用,来表示具体引用的目标是"谁"。Java虚拟机规范中明确定义了符号引用的形式,符合这个规范的前提下,符号引用可以是任意值,只要能通过这个值能定位到目标。什么是直接引用?
直接引用就是可以直接或间接指向目标内存位置的指针或句柄。引用的类型,还未加载初始化怎么办?
当出现这种情况,会触发这个引用对应类型的加载和初始化。
初始化是类加载的最后一个步骤,初始化的过程,也就是执行类构造器
方法的过程
方法的作用是什么?
() 在准备阶段,已经对类中static修饰的变量赋予了初始值。
方法的作用,就是给这些变量赋予程序员实际定义的“值”。同时类中如果存在static代码块,也会执行这个静态代码块里面的代码。
()
方法是什么?
()
方法 和
() 方法是不同的,它们一个是“类构造器”,一个是实例构造器。
Java虚拟机会保证子类方法在执行前,父类的
() 已经执行完毕。而
() 方法则需要显性的调用父类的构造器。
方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块但时候,才会自动生成
() 方法。
()
当 JVM 遇到下面 5 种情况的时候会触发初始化
new、getstatic、putstatic、invokestatic
这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
new
关键字实例化对象的时候、读取或设置一个类的静态字段(被 final
修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。main()
方法的那个类),虚拟机会先初始化这个主类。java.lang.invoke.MethodHandle
实例最后的解析结果是 REF_getstatic
、REF_putstatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,则需要先出触发其初始化。当编译器将 Java 源码编译为字节码之后,虚拟机便可以将字节码读取进内存,从而进行解析、运行等整个过程。我们将这个过程称为 Java 虚拟机的类加载机制
。
类加载机制
中,通过类加载器(classloader
)来完成类加载的过程。
通过一个类全限定名称来获取其二进制文件(.class
)流的工具,被称为类加载器(classloader
)。
Java中支持4中类加载器
启动类加载器(Bootstrap ClassLoader
)
Bootstrap ClassLoader
的 parent
属性为 null
标准扩展类加载器(Extention ClassLoader
)
sun.misc.Launcher$ExtClassLoader
实现JAVA_HOME
下 libext
目录下的或者被 java.ext.dirs
系统变量所指定的路径中的所有类库应用类加载器(Application ClassLoader
)
sun.misc.Launcher$AppClassLoader
实现用户自定义类加载器(User ClassLoader
)
java.lang.ClassLoader
类。如果不想打破双亲委派模型,那么只需要重写 findClass
方法即可;如果想打破双亲委派模型,则需要重写 loadClass
方法classloader
类存在一个 parent
属性,可以配置双亲属性。默认情况下,JDK 中设置如下。
ExtClassLoader.parent=null;
AppClassLoader.parent=ExtClassLoader
//自定义
XxxClassLoader.parent=AppClassLoader
「类加载机制」中,通过「类加载器(classloader
)」来完成类加载的过程。Java 中的类加载机制,有如下 3 个特点
双亲委派机制是一种任务委派模式,是 Java 中通过加载工具(classloader
)加载类文件的一种具体方式。 具体表现为
BootstrapClassLoader
。AppClassLoader
)也无法加载此类,则抛出异常。protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 检查该类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2. 有上级的话,委派上级 loadClass
c = parent.loadClass(name, false);
} else {
// 3. 如果没有上级了(ExtClassLoader),则委派
BootstrapClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
// 4. 每一层找不到,调用 findClass 方法(每个类加载器自己扩展)来加载
c = findClass(name);
// 5. 记录耗时
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
上述代码的主要步骤如下
loadClass()
方法进行加载ClassNotFoundException
异常后,再调用自己的 findClass()
方法进行加载ClassLoader
中和类加载有关的方法有很多,前面提到了 loadClass()
,除此之外,还有 findClass()
和 defineClass()
等。这3个方法的区别如下
loadClass()
:默认的双亲委派机制在此方法中实现findClass()
:根据名称或位置加载 .class
字节码definclass()
:把 .class
字节码转化为 Class
对象双亲委派可以保证一个类不会被多个类加载器重复加载,并且保证核心 API 不会被篡改,其优点如下
其缺点如下:
Java 提供了很多服务提供者接口(SPI,Service Provider Interface
),它可以允许第三方为这些接口提供实现,比如数据库中的 SPI 服务 - JDBC。这些 SPI 的接口由 Java 核心类提供,实现者确是第三方。如果继续沿用双亲委派,就会存在问题,提供者由 Bootstrap ClassLoader 加载,而实现者是由第三方自定义类加载器加载。这个时候,顶层类加载就无法使用子类加载器加载过的类。
参考:
- JVM类加载机制、双亲委派和SPI机制 - 掘金