文章目录
Java的类加载步骤
1、加载
加载阶段的主要任务是将.class文件中的类转化为java.lang.Class对象,具体步骤为:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆中创建一个该类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口
2、连接
2.1、验证
验证的主要目的是确保被加载的类的正确性。包括:
- 文件格式的验证
- 元数据的验证
- 字节码的验证
- 符号引用的验证
验证阶段虽然重要,但是也可以通过Xverify:none跳过验证。
2.2、准备
准备阶段主要是为类变量分配内存并设置类变量初始值。
2.3、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用(指针、偏移量)的过程。
3、初始化
初始化阶段主要是对类变量进行初始化。这种初始化有两种方式:
- 在声明类变量时指定初始值
static int i = 1;
- 使用静态代码块为类变量指定初始值
static String s; static { s = ""; }
4、准备和初始化的区别:
public static int i=123;
在准备阶段会先把i赋值为int类型的默认值0,在初始化阶段才会把i赋值为123
public static final int j=123;
但是对于final
修饰的变量,则会直接在准备阶段将其初始化为123。因为编译时javac会为j生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置对j直接赋值:
JVM的初始化步骤:
- 如果该类还没有被加载和连接,则先加载并连接该类
- 如果该类的直接父类还没有被初始化,那么先初始化它的直接父类(初始化直接父类时就会回到第一步,所以总是Object类先被初始化)
- 如果类中有初始化语句,则系统依次执行这些初始化语句
类加载器
类加载子系统的“加载”步骤是由ClassLoader类的一些子类完成的,JVM中提供了三层的ClassLoader:
- Bootstrap ClassLoader
- Extension ClassLoader
- Application ClassLoader
- 另外还允许用户自定义类加载器 Custom ClassLoader 继承自
ClassLoader
抽象类并重写findClass()
方法
我们可以用以下代码看看各个类加载器的加载路径都有哪些:
System.out.println("Bootstrap ClassLoader加载的目录:");
String bootstrapLoadingPath = System.getProperty("sun.boot.class.path");
String[] bootstrapLoadingPaths = bootstrapLoadingPath.split(";");
for (String s : bootstrapLoadingPaths) {
System.out.println(s);
}
System.out.println();
System.out.println("Extension ClassLoader加载的目录:");
String extensionLoadingPath = System.getProperty("java.ext.dirs");
String[] extensionLoadingPaths = extensionLoadingPath.split(";");
for (String s : extensionLoadingPaths) {
System.out.println(s);
}
System.out.println();
System.out.println("Application ClassLoader加载的目录:");
String applicationLoadingPath = System.getProperty("java.class.path");
String[] applicationLoadingPaths = applicationLoadingPath.split(";");
for (String s : applicationLoadingPaths) {
System.out.println(s);
}
结果:
Bootstrap ClassLoader加载的目录:
D:\Environments\jdk-8u131\jre\lib\resources.jar
D:\Environments\jdk-8u131\jre\lib\rt.jar
D:\Environments\jdk-8u131\jre\lib\sunrsasign.jar
D:\Environments\jdk-8u131\jre\lib\jsse.jar
D:\Environments\jdk-8u131\jre\lib\jce.jar
D:\Environments\jdk-8u131\jre\lib\charsets.jar
D:\Environments\jdk-8u131\jre\lib\jfr.jar
D:\Environments\jdk-8u131\jre\classes
Extension ClassLoader加载的目录:
D:\Environments\jdk-8u131\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
Application ClassLoader加载的目录:
D:\Environments\jdk-8u131\jre\lib\charsets.jar
D:\Environments\jdk-8u131\jre\lib\deploy.jar
D:\Environments\jdk-8u131\jre\lib\ext\access-bridge-32.jar
D:\Environments\jdk-8u131\jre\lib\ext\cldrdata.jar
D:\Environments\jdk-8u131\jre\lib\ext\dnsns.jar
D:\Environments\jdk-8u131\jre\lib\ext\jaccess.jar
D:\Environments\jdk-8u131\jre\lib\ext\jfxrt.jar
D:\Environments\jdk-8u131\jre\lib\ext\localedata.jar
D:\Environments\jdk-8u131\jre\lib\ext\nashorn.jar
D:\Environments\jdk-8u131\jre\lib\ext\sunec.jar
D:\Environments\jdk-8u131\jre\lib\ext\sunjce_provider.jar
D:\Environments\jdk-8u131\jre\lib\ext\sunmscapi.jar
D:\Environments\jdk-8u131\jre\lib\ext\sunpkcs11.jar
D:\Environments\jdk-8u131\jre\lib\ext\zipfs.jar
D:\Environments\jdk-8u131\jre\lib\javaws.jar
D:\Environments\jdk-8u131\jre\lib\jce.jar
D:\Environments\jdk-8u131\jre\lib\jfr.jar
D:\Environments\jdk-8u131\jre\lib\jfxswt.jar
D:\Environments\jdk-8u131\jre\lib\jsse.jar
D:\Environments\jdk-8u131\jre\lib\management-agent.jar
D:\Environments\jdk-8u131\jre\lib\plugin.jar
D:\Environments\jdk-8u131\jre\lib\resources.jar
D:\Environments\jdk-8u131\jre\lib\rt.jar
D:\Code\Code-Java\out\production\test
D:\ProgramSoftwares\IntelliJ IDEA 2021.2.1\lib\idea_rt.jar
类加载器间的继承关系如图:
类加载的详细流程
比如说这里有一个com.philpy.HelloWorld
类
-
java.exe会调用底层的jvm.dll创建一个JVM实例以及Bootstrap ClassLoader
-
Bootstrap ClassLoader会创建一个
sun.misc.Launcher
的实例launcher
;在创建launcher
的同时,Extension ClassLoader和Application ClassLoader也被创建好了,因为ExtClassLoader和AppClassLoader都是Launcher的静态内部类 -
使用
launcher
的getClassLoader()
方法获取自己的类加载器loader
,实例默认为AppClassLoader实例(AppClassLoader的父加载器为ExtClasssLoader): -
classLoader
调用loadClass()
方法加载要运行的类HelloWorld
:loader.loadClass("com.philpy.HelloWorld");
-
加载完成时JVM会执行HelloWorld类的main()方法入口
-
程序执行完成
-
JVM销毁
双亲委派机制
类加载器loader
实例默认为AppClassLoader实例,因此loader
会先通过调用自身重写的loadClass()
方法通过全限定类名来查找该类的Class模板对象,在查找之前会先调用findLoadedClass()
方法判断该类是否已被加载,如果该方法的返回值不为空,说明该类已被加载,直接返回;否则调用父类ClassLoader
的loadClass()
方法查找:
在抽象父类ClassLoader
的loadClass()
方法中,依旧是先调用findLoadedClass()
方法判断该类是否已被加载,如果该方法的返回值不为空,说明该类已被加载,直接返回;否则,调用自己父类加载器的loadClass()
方法尝试加载,如果返回值不为空,说明该类已被加载,直接返回;否则继续递归调用自己的父类加载器的loadClass()
方法。如果父类加载器为空,说明此时向上委托已经到头,到达了Bootstrap ClassLoader,就会调用findBootstrapClassOrNull()
方法(内部调用了本地方法findBootstrapClass()
)继续判断该类有没有被Bootstrap ClassLoader加载过,此时向上委托已结束:
如果依旧返回空,那么则开始调用findClass()
方法进行向下查找。ClassLoader
的findClass()
就基本什么都没有做,算是专门用来被子类实现的(自定义类加载器就需要继承ClassLoader
并重写findClass()
方法):
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
因此默认的fincClass()
方法则是URLClassLoader
的重写方法。在findClass()
方法中主要就是做了一件事:将com.philpy.HelloWorld
格式的包名转化为com/philpy/HelloWorld.class
格式的class文件,如果可以加载到该文件,则尝试通过SecureClasssLoader
的define本地方法将其转化为Class对象并返回,如果期间发生ClassNotFoundException或返回null,则回到loadClass()
中继续向下查找,直到查找到该类为止或始终没有查找到最终抛出ClassNotFoundException:
即:
自定义类加载器
自定义类加载器的核心是继承ClassLoader
类并重写findClass()
方法,我们可以先跟着Java官方提供的例子照葫芦画瓢:
package com.philpy;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyClassLoader extends ClassLoader {
private final String baseDir;
public MyClassLoader(String baseDir) {
this.baseDir = baseDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
try {
byte[] b = loadClassData(baseDir + path);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadClassData(String name) throws IOException {
FileInputStream is = new FileInputStream(name);
int len = is.available();
byte[] bytes = new byte[len];
is.read(bytes);
is.close();
return bytes;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
MyClassLoader loader = new MyClassLoader("D:\\test\\");
Class<?> clazz = loader.loadClass("com.philpy.HelloWorld");
System.out.println(clazz.getClassLoader());
Object o = clazz.getConstructor().newInstance();
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(o);
}
}
测试:
但是,当我的MyClassLoader路径下也由一个HelloWorld类的时候:
此时再次运行代码:
发现,类加载器变成了AppClassLoader。这就是因为有双亲委派机制的存在,向下查找的时候,在AppClassLoader的路径下发现了同包名同类名的类,就会停止继续向下查找,直接返回。因而得不到我们想要的结果。那么如何破坏双亲委派机制呢?
破坏双亲委派机制
其实破坏双亲委派机制也很简单,双亲委派机制主要是由于loadClass()
方法中调用了parent.loadClass()
方法导致其向上委派,因此想要破坏双亲委派机制,就需要重写loadClass()
方法。
package com.philpy;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyClassLoader extends ClassLoader {
private final String baseDir;
public MyClassLoader(String baseDir) {
this.baseDir = baseDir;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c == null) {
// 我只想让"com.philpy"下的类不遵循双亲委派
if (!name.startsWith("com.philpy")) {
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
}
return c;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
try {
byte[] b = loadClassData(baseDir + path);
return defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadClassData(String name) throws IOException {
FileInputStream is = new FileInputStream(name);
int len = is.available();
byte[] bytes = new byte[len];
is.read(bytes);
is.close();
return bytes;
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
MyClassLoader loader = new MyClassLoader("D:\\test\\");
Class<?> clazz = loader.loadClass("com.philpy.HelloWorld");
System.out.println(clazz.getClassLoader());
Object o = clazz.getConstructor().newInstance();
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(o);
}
}
(JDK9以后的)模块化类加载器
上面所说的类加载器都是JDK1.8的类加载器,碰到双亲委派保护机制的报错如下:
但是如果在JDK9以后引入了Java模块化系统,类加载器也有了一些变化:
模块化加载器主要有这些变动:
- 扩展类加载器(Extension Class Loader)被平台类加载器(Platform Class Loader)取代,这其实是一个很顺理成章的变动,既然整个JDK都基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件),其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留<JAVA_HOME>\lib\ext目录
- 平台类加载器和应用程序类加载器都不再派生自java.net.URLClassLoader,如果有程序直接
依赖了这种继承关系,或者依赖了URLClassLoader类的特定方法,那代码很可能会在JDK 9及更高版本的JDK中崩溃。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader,在BuiltinClassLoader中实现了新的模块化架构下类如何从模块中加载的逻辑,以及模块中资源可访问性的处理 - 启动类加载器现在是在Java虚拟机内部和Java类库共同协作实现的类加载器。尽管有了BootClassLoader这样的Java类,但为了与之前的代码保持兼容,所有在获取启动类加载器的场景(譬如Object.class.getClassLoader())中仍然会返回null来代替,而不会得到BootClassLoader的实例
JDK9以后的双亲委派模型也产生了一些变化:当平台及应用程序类加载器收到类加载请求,在(findLoadedClass找不到)委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中(findLoadedModule),如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载: