关于类的加载
类的加载过程
当我们 new 一个对象,或者使用对象的静态方法,静态变量等操作时,JVM 会先检查类是否被加载(我们知道,它被加载到方法区),如果未加载,则使用类加载器(ClassLoader
)将类进行加载并初始化。这里需要注意的是,初始化并非是一定发生的。调用类的静态常量(静态常量存在方法区中,在链接阶段被初始化,静态变量也存在于方法区中,不过要在初始化阶段通过<clinit>()
方法初始化)等操作并不会触发类的初始化。
类的生命周期可以分为 7 步——加载,验证,准备,解析,初始化,使用和卸载。其中,类的初始化过程为前五步。
验证,准备,解析过程可以归结为链接过程。因而,一般说类的初始化过程分为三步——加载,链接,初始化。
再次强调,类的初始化不是一定发生的。但是如果要使用一个类,其加载和链接是必定发生的。
加载
加载主要完成——
- 通过全类名获取定义类的二进制字节流(通过 ZIP,JAR 包,Class 文件以及其他)到内存中
- 将字节流代表的静态存储结构(也就是 Class 字节码)转换为方法区的运行时数据结构
- 在堆(!)中生成一个代表该类的
java.lang.Class
对象,它是方法区的这些数据的访问入口
总之,是把 Class 字节码加载到方法区中,然后在堆中生成Class
对象作为其入口。
其中,第一步(获取类的二进制字节流)是高度可自定义的。可以自定义类加载器(重写类加载器的loadClass()
方法)来控制字节流的获取方式。
数组类型不通过类加载器执行,而是 JVM 直接创建。
关于这一步,类加载器和双亲委派机制是需要学习的。
加载阶段和链接阶段是可以并行执行的。
验证
验证是链接的第一步,它主要是验证 Class 文件(当然,已经被加载到方法区了)的合法性。
准备
准备阶段主要为 static 变量在方法区(!)中分配内存并设置初始值。
该阶段,静态变量和静态常量的行为是不同的,比如我们定义public static int variable = 123; public static final int constant = 456;
,在这一步中,variable
会被赋int
型的零值(0),而constant
会被赋值 456。这表明——
- 静态常量的初始化在准备(链接)阶段就被完成。
- 静态变量的真正的初始化在类初始化过程
<clinit>()
方法中进行。在准备阶段只会被赋变量类型的初始值。
因此,调用类的静态常量不会触发类的初始化操作,就是因为其在准备阶段已经被初始化过了,可以直接使用。
解析
解析就是将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口,字段(field),类方法,接口方法,方法类型,方法句柄和调用限定符这 7 类符号引用进行。
这里的直接引用是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。在运行时,这些符号引用必须被转换成直接引用,因此 JVM 才能明确获得它们的位置。比如,当程序要执行一个方法,系统就需要明确知道这个方法所在的位置。JVM 为每个类都准备了一个方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只需要知道它在方法表中的偏移量。(有点麻,这个之后肯定还得继续学习)
总之,解析是虚拟机将常量池中的符号应用替换为直接引用的过程,也就是得到类或字段,方法在内存中的指针或偏移量。
初始化
初始化是类加载的最后一步,其真正地执行类中定义的 Java 程序代码(static 声明和 static 初始化块)。它会合并所有 static 的代码,将其组合为<clinit>()
方法并执行。类的初始化并非是一定会被执行的。
类的初始化只有在主动使用类(静态变量,静态方法,实例变量,实例方法……)时出现,其有五种情况——
- 当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
- 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
- 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
- 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
- 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
- 使用
java.lang.reflect
包的方法对类进行反射调用时如 Class.forname(“…”).newInstance() 等等。 如果类没初始化,需要触发其初始化。- 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
- 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
- MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
- 「补充 issue745」 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
卸载
类的卸载就是该类的Class
对象被 GC。
类的卸载有三个前提——
- 该类所有实例对象被 GC,即堆中不存在该类的实例对象
- 该类没有在其它任何地方被引用
- 该类的类加载器实例被 GC
JDK 自带的三个类加载器 BootstrapClassLoader,ExtClassLoader,AppClassLoader 不会被 GC,因而它们加载的类不会被卸载。
学这些只是让我更加清楚地认识到,JVM 和字节码是必须要深入学习的…
类加载器(ClassLoader)
类加载器的作用就是将。class 文件加载到内存的方法区中。
Java 内置了三个 ClassLoader。除了 BootstrapClassLoader,其他类加载器都由 Java 实现,它们都继承自 java.lang.ClassLoader。按加载的优先级,它们由顶层向底层分为:
BootstrapClassLoader
是启动类加载器,它由 C++实现,加载%JAVA_HOME%/lib
目录下的 jar 包和类,或者被-Xbootclasspath
参数指定的路径中的所有类。ExtensionClassLoader
是扩展类加载器,它加载目录%JRE_HOME%/lib/ext
下的 jar 包和类,或者被java.ext.dirs
系统变量所指定路径下的 jar 包。AppClassLoader
是应用程序类加载器,它面向用户,加载当前应用 classpath 下所有 jar 包和类。
双亲委派模型
每一个类都有加载它的类加载器。类加载器默认使用双亲委派模型。即类加载时,系统(该类加载器)首先判断当前类是否被加载过,如果已加载则直接返回,如果未加载,则尝试加载。
加载时,该类加载器首先会把请求委派给父类加载器的loadClass()
处理。因此,该请求会被递归传送到顶层的启动类加载器BootstrapClassLoader
中。当父类加载器无法处理时,才由自己处理。当父类加载器为 null 时,会启动类加载器BootstrapClassLoader
作为父类加载器。
双亲委派模型可以避免类的重复加载,并保证 Java 的核心 API 不被篡改。
应该注意,这里的“双亲”只是用来指代“辈分”,而不是用来指代“父母”。
自定义类加载器
通过继承ClassLoader
,我们可以自定义自己的类加载器。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!