任何一个类型在使用之前都必须经历过完整的加载、连接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,它就可以随时随地被使用了,开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态方法、静态字段),或者使用new关键字为其创建对象实例。当然从一个类型被加载进JVM中开始算起,直至最终被卸载出内存为止,它的整个生命周期也就随之而结束了。
1、类加载器
类加载器是JVM执行类加载机制的前提。简单来说,类加载器的主要任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转换为一个与目标类对应的java.lang.Class对象实例。
无论类加载器的类型如何划分,在程序中我们最觉的类加载器始终只有3个,如下所示:
- Bootstrap ClassLoader;
- ExtClassLoader;
- AppClassLoader;
BootStrap ClassLoader
也称之为启动类加载器,它由C++语言编写并嵌套在JVM内部,主要负责加载“JAVA_HOME/lib”
目录中的所有类型,或者由选项“-Xbootclasspath”
指定路径中的所有类型。ExtClassLoader
和AppClassLoader
派生于ClassLoader
,并且都是采用Java语言进行编写的,前者主要负责加载“JAVA_HOME/lib/ext”
扩展目录中的所有类型,而后者则主要负责加载ClassPath目录中的所有类型。
如果当前的类加载器无法满足我们的需求时,便可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。在程序中编写一个自定义类加载器只需要继承抽象类ClassLoader并重写其findClass()方法即可。使用时调用loadClass()方法来实现类加载操作。
2、双亲委派模型(Parents Delegation Model)
除了启动类加载器之外,程序中每一个类加载器都应该拥有一个超类加载器,比如AppClassLoader的超类加载器就是ExtClassLoader,而开发人员自己编写的自定义类加载器的超类就是AppClassLoader。当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,而是将加载任务委派给它的超类加载器去执行,每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止。如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载。
由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
双亲委派模型代码如下:**
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在超类加载器,就委派给超类加载器执行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果不存在超类加载器,就直接委派给顶层的启动类加载器执行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果找不到类,则引发ClassNotFoundException
// 当抛出 ClassNotFoundException 异常时,意味着超类加载器加 载失败
}
if (c == null) {
// 如果超类加载器无法加载时,则自行加载
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义类加载器
package com.jvm;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String byteCode_path;
public MyClassLoader(String byteCode_path) {
this.byteCode_path = byteCode_path;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] value = null;
BufferedInputStream in = null;
try {
in = new BufferedInputStream(new FileInputStream(byteCode_path + name + ".class"));
value = new byte[in.available()];
in.read(value);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 将byte数组转为一个类的 Class 对象实例
return defineClass(value, 0, value.length);
}
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classloader = new MyClassLoader("/Users/jarvan/");
System.out.println("加载目标类的类加载器->" + classloader.loadClass("NumberTest").getClassLoader().getClass().getName());
System.out.println("当前类加载器的超类加载器->" + classloader.getParent().getClass().getName());
}
}
输出如下:
加载目标类的类加载器->com.jvm.MyClassLoader
当前类加载器的超类加载器->jdk.internal.loader.ClassLoaders$AppClassLoader
3、类的加载过程
类加载器所执行的加载操作仅仅只是属于JVM中类加载过程中的一个阶段,一个完整的类加载过程必须经历加载、连接和初始化这3个步骤。
加载阶段:
简单来说,类的加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到 JVM 内部,并存储在运行时内存区中的方法区内,然后将其转换成一个与目标类型对应的 java.lang.Class 对象实例,这个 Class 对象在日后就会作为方法区中该类的各种数据的访问入口
连接阶段:
连接阶段要做的事情就是将已经加载到 JVM 中的二进制字节流的类数据信息合并到 JVM 的运行时状态中。
连接阶段由验证、准备和解析3个阶段构成,其中验证阶段的主要任务就是验证类数据信息是否符合 JVM 规范,是否是一个有效的字节码文件,而验证的内容涵盖了类的类数据信息的格式验证、语义分析、操作验证等;
准备阶段的主要任务就是为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,因此实例变量将不在此操作范围内);
解析阶段的主要任务是将常量池中所有的符号引用全部转换为直接引用,不过 Java 虚拟机规范并没有明确要求解析阶段一定要按顺序执行,因此解析阶段可以等到初始化之后再执行
初始化:
初始化阶段中 JVM 会将一个类中所有被 static 关键字标示的代码统统执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖掉之前在准备阶段中 JVM 为其设置的初始值,当然如果没有显示赋值静态变量,那么所持有的值仍是之前的初始值;如果执行的是 static 代码块,那么在初始化阶段中,JVM 会执行 static 代码块中的所有操作。
一个类或者接口应该在首次主动使用时执行初始化操作
- 为一个类型创建一个新的对象实例时(new关键字、反射或序列化)
- 调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
- 调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过使用final关键字修饰的静态字段除外,它被初始化为一个编译时的常量表达式
- 调用 Java API 中的反射方法时(比如 java.lang.Class 中的方法或者 java.lang.reflect 包中其他类的方法)
- 初始化一个类的派生类时(Java 虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作)
- JVM 启动包含 main() 方法的启动类时
尽管一个类在初始化之前必须要求它的超类提前完成初始化操作,但对于接口而言并不适用。只有在某个接口中声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的派生接口或派生类要初始化而被初始化。
演示一个类发生的变量
package com.jvm;
public class LoadingTest {
public static LoadingTest obj = new LoadingTest();
public static int value1;
public static int value2 = 0;
public LoadingTest() {
value1 = 10;
value2 = value1;
System.out.println("before valuel->" + value1);
System.out.println("before value2->" + value2);
}
public static void main(String[] args) throws Exception {
System.out.println("after valuel->" + value1);
System.out.println("after value2->" + value2);
}
}
程序输出如下
before valuel->10
before value2->10
after valuel->10
after value2->0
将示例代码中的代码位置调换顺序
public static int value1;
public static int value2 = 0;
public static LoadingTest obj = new LoadingTest();
程序输出如下
before valuel->10
before value2->10
after valuel->10
after value2->10
原因
当类加载器将 Loading 类加载进 JVM 内部后,会在方法区中生成一个与该类型对应的 java.lang.Class 对象实例。
当进入到准备阶段时,JVM 便会为 Loading 类中的3个静态变量分配内存空间,并为其设置初始值(value1和value2为0,obj为null)。
当经历到类加载过程的初始化阶段时,程序最终的输出结果就会和代码的执行顺序有关了。
在上述示例中,静态变量obj是优先初始化的,所以初始化后value1和value2的值都是10。接下来 JVM 会检查静态变量value1是否也需要执行初始化,由于value1并没有显示的进行赋值,因此将会跳过它到静态变量value2上,这里显示赋值为value2=0等于重新覆盖掉之前在构造方法中的赋值操作,这就是会输出为0的原因,位置调换后,最后赋值的为10所以和预期一致。
1、加载阶段
类加载器根据一个类的全限定名来读取此类的二进制字节流到 JVM 内部,并存储在运行时内存区中的方法区内,然后将其转换成一个与目标类型对应的 java.lang.Class对象实例,这个 Class 对象在日后就会作为方法区中该类的各种数据的访问入口。
2、验证阶段
验证阶段中的主要任务是检查当前正在加载的字节码文件是否符合 JVM 规范,是否一是个有效的字节码文件,如果不是一个有效的字节码文件 JVM 就将会抛出 java.lang.VerifyError异常。
格式验证检查字节码文件中的前四个字节是否为 0xCAFEBABE,如果高版本 JDK 编译的字节码文件不能在低版本的 JVM 中运行,否则 JVM 会抛出java.lang.UnsupportedClassVersionError异常
只有当成功通过格式验证之后,类加载器才会成功将类的二进制数据信息加载到方法区中。而后续的其他验证操作都直接在方法区中进行。接下来 JVM 就会开始执行下一阶段的验证任务
- 检查一个被标记为 final 的类型是否包含派生类;
- 检查一个类中的 final 方法是否被派生类进行重写;
- 确保超类与派生类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
当成功执行完语义验证后,JVM 会进入操作验证阶段。此阶段中JVM会对类型的方法执行验证,确保一个类的方法在执行时,不会对JVM产生不良的影响,不会因此导致JVM的进程出现崩溃(比如入栈2个int类型后,却把它们当做long类型去操作)。
验证阶段最后一步,对常量池中的各种符号引用执行验证,在JVM的具体实现中连接阶段中的解析操作往往发生在初始化之后,因此这一阶段的验证会在解析阶段中才会执行。简单来说解析阶段的主要任务是将常量池中所有的符号引用全部转换为直接引用,那么符号引用验证的主要任务就是验证需要被转换为直接引用的这些符号引用是否正确。比如是否能否通过符号引用中通过字符串描述的全限定名定位到指定的类型上,或者是符号引用中的类成员信息的访问修饰符是否能够被当前类执行访问操作等
3、准备阶段
此阶段对存放在方法区中的类数据信息的类变量执行初始化,为类中的所有静态变量分配内存空间,并为其亩一个初始值(由于还没有产生对象,因此实例变量将不在此操作范围内)
JVM 内部实现其实并不支持boolean类型,在JVM内部,boolean类型往往被实现为一个int类型,初始值为0也就代表着false。
4、解析阶段
由于Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行,因此解析阶段可以等到初始化之后再执行。
解析阶段的主要任务就量将字节码常量池中的符号引用全部转换为直接引用,包括类、接口、方法和字段的符号引用。
5、初始化阶段
类加载过程中的最后一个阶段就是初始化,初始化阶段中, JVM 会将一个类中所有被 static 关键字标示的代码统统执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖掉之前在准备阶段中 JVM 为其设置的初始值,当然如果没有显示赋值静态变量,那么所持有的值仍是之前的初始值;如果执行的是 static 代码块,那么在初始化阶段中,JVM 会执行 static 代码块中的所有操作。
标签:初始化,JVM,阶段,超类,机制,执行,加载 From: https://blog.51cto.com/u_11906056/7061404