JDK
版本:1.8
下图为类加载子系统:
class
文件,class
文件在文件开头有特定的文件标识(CA FE BA BE
)。Classloader
只负责class
文件的加载,至于它是否可以运行,则由Execution Engine
决定。jvm
内存中的一块名为Method Area
的内存空间中。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是class
文件中常量池部分的内存映射)。Classloader
的角色class file
源码文件存在于本地硬盘上,这一理解为一个模板。而这个模板在执行的时候是需要加载到JVM
内存中根据这个文件实例化出n
个一摸一样的实例。加载class file
的方式是二进制流。class file
源码被加载到JVM
中,被称为DNA
元数据模板,存放在JVM
内的Method Area
中。.class
文件-->
JVM
-->
最终成为DNA
元数据模板。这个过程需要一个运输工具(类装载器Classloader
)完成。Java
类加载的过程(宏观):
Loading
)阶段加载(Loading
)阶段分为3
步:
java.lang.Class
对象,作为方法区中这个类的各种数据的访问入口。加载.class
文件的方式:
Web Applet
。zip
压缩包中读取,成为日后jar
、war
格式的基础。JSP
应用。.class
文件。Class
文件被反编译的保护措施。Linkging
)阶段链接(Linking
)阶段分为3
个步骤:
验证(Verification
):
Class
文件的二进制字节流中包含的信息符合当前虚拟机的要求。保证被加载类的正确性,不会危害虚拟机自身安全。准备(Preparation
):
final
修饰的static
变量(这已经不能被称为变量,此时已经是常量),因为final
修饰的常量在编译的时候就会分配内存。准备阶段会进行显示初始化,即对其进行具体的赋值。解析(Resolution
):
JVM
在执行完初始化后再执行。《Java虚拟机规范》
的Class
文件格式中。直接引用就是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等。public class HelloLinking {
/**
* Preparation 阶段只是为其赋初始值0
* Initialization 阶段将3赋值给a
*/
private static int a = 3;
public static void main(String[] args) {
System.out.println(a);
}
}
()
的过程。javac
编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。如果类中没有变量需要赋值,是不会出现()
方法的。()
不同于类的构造器。(关联:构造器是虚拟机视角下的()
)。JVM
会保证子类的()
执行前,父类的()
已经执行完毕。()
方法在多线程下被同步加锁。public class HelloClassLoader {
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(getCurrentThreadName() + "开始");
new DeadThread();
System.out.println(getCurrentThreadName() + "结束");
};
Thread a = new Thread(runnable, "A线程");
Thread b = new Thread(runnable, "B线程");
a.start();
b.start();
}
private static class DeadThread {
static {
System.out.println(getCurrentThreadName() + "正在初始化 [DeadThread] 类");
}
}
private static String getCurrentThreadName() {
return Thread.currentThread().getName();
}
}
最终只会有一个线程获取到锁并初始化DeadThread
类。运行程序将会看到只会有一个线程初始化DeadThread
类。
B线程开始
A线程开始
B线程正在初始化 [DeadThread] 类
B线程结束
A线程结束
Process finished with exit code 0
所以一个类的
方法只会被一个线程调用一次。
JVM
支持两种类型的类加载器 。分别为引导类加载器Bootstrap ClassLoader
和自定义类加载器User-Defined ClassLoader
。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但在Java
虚拟机规范中却没有这么定义,而是将所有派生于抽象类ClassLoader
的类加载器都划分为自定义类加载器。
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个:
BootStrap Class Loader
)Extension Class Loader
)System Class Loader
)它们之间的关系是包含关系,并不是上下层或者子父类继承。
可以看到Java
中的扩展类加载器ExtClassLoader
,它是Launcher
中的一个静态内部类。同时也可以获取到AppClassLoader
,其也是定义在Launcher
类中的一个静态内部类。而BootStrap Class Loader
是获取不到的,它是由C
和C++
进行编写的。
使用代码获取加载器:
public class CustomClassLoader {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("systemClassLoader = " + systemClassLoader);
// 获取其上层即扩展类加载器
ClassLoader extensionClassLoader = systemClassLoader.getParent();
// sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println("extensionClassLoader = " + extensionClassLoader);
// 试图获取 BootStrap Class Loader
ClassLoader bootStrapClassLoader = extensionClassLoader.getParent();
// null
System.out.println("bootStrapClassLoader = " + bootStrapClassLoader);
// 获取用户自定义类使用的类加载器
ClassLoader classLoader = CustomClassLoader.class.getClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("classLoader = " + classLoader);
ClassLoader stringClassLoader = String.class.getClassLoader();
// null
System.out.println("stringClassLoader = " + stringClassLoader);
}
}
输出结果:
systemClassLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
extensionClassLoader = sun.misc.Launcher$ExtClassLoader@1b6d3586
bootStrapClassLoader = null
classLoader = sun.misc.Launcher$AppClassLoader@18b4aac2
stringClassLoader = null
Process finished with exit code 0
对于开发者自定义的类,默认使用的是系统类加载器进行加载。
对于Java
的核心类库都是使用的是引导类加载器进行加载。
Bootstrap Class Loader
)BootStrap
类加载使用C/C++
语言实现的,嵌套在JVM
内部。BootStrap
类加载用来加载Java
的核心库(JAVA_HOME/jre/lib/rt.jar
、resources.jar
或sun.boot.class.path
路径下的内容),用于提供JVM
自身需要的类。BootStrap
类加载并不继承自java.lang.ClassLoader
,没有父加载器。BootStrap
类加载器加载扩展类和应用程序类加载器,并指定为他们的父类加载器。Bootstrap
启动类加载器只加载包名为java
、javax
、sun
等开头的类。Extension Class Loader
)Java
语言编写,由sun.misc.Launcher$ExtClassLoader
实现。ClassLoader
类。java.ext.dirs
系统属性所指定的目录中加载类库,或从JDK
的安装目录的jre/lib/ext
子目录(扩展目录)下加载类库。如果用户创建的JAR
放在此目录下,也会自动由扩展类加载器加载。App Class Loader
)Java
语言编写,由sun.misc.Launchers$AppClassLoader
实现。ClassLoader
类。classpath
或系统属性java.class.path
指定路径下的类库。Java
应用的类都是由它来完成加载。ClassLoader#getSystemclassLoader()
方法可以获取到该类加载器。public class TestClassLoader {
private static final String SEMICOLON = ";";
private static final String EXT_URL = "java.ext.dirs";
public static void main(String[] args) {
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Arrays.stream(urLs).forEach(System.out::println);
System.out.println("\n");
// 从上面的路劲中随便挑选一个类, 查看其类加载器 :
ClassLoader proxyClassClassLoader = Proxy.class.getClassLoader();
// null --> BootStrap Class Loader
System.out.println("proxyClassClassLoader = " + proxyClassClassLoader);
System.out.println("\n");
System.out.println("===========extension class loader spilt line===========");
// 寻找 jre/lib/ext 目录下的 class 获取 ClassLoader
ClassLoader ecKeyFactoryClassLoader = ECKeyFactory.class.getClassLoader();
// sun.misc.Launcher$ExtClassLoader@7cca494b
System.out.println("ecKeyFactoryClassLoader = " + ecKeyFactoryClassLoader);
System.out.println("\n");
String extensionUrl = System.getProperty(EXT_URL);
Arrays.stream(extensionUrl.split(SEMICOLON)).forEach(System.out::println);
// 获取自定义类的 ClassLoader
ClassLoader classLoader = CustomClassLoader.class.getClassLoader();
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println("classLoader = " + classLoader);
}
}
在Java
的日常应用程序开发中,类的加载几乎是由BootStrap Class Loader
、Extension Class Loader
、App Class Loader
类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
隔离加载类.。
修改类加载的方式。
扩展加载源。
防止源码泄漏。
用户自定义类加载器实现步骤:
java.lang.ClassLoader
类的方式,实现自己的类加载器,以满足一些特殊的需求。JDK1.2
之前,在自定义类加载器时,总会去继承ClassLoader
类并重写loadClass()
方法,从而实现自定义的类加载类,但是在JDK1.2
之后已不再建议用户去覆盖loadclass()
方法,而是建议把自定义的类加载逻辑写在findClass()
方法中。URLClassLoader
类,可以避免自己去编写findClass()
方法及其获取字节码流的方式,使自定义类加载器编写更加优雅简洁。自定义一个CustomClassLoader
类加载器派生于ClassLoader
:
public class CustomClassLoader extends ClassLoader {
private String classPath;
public CustomClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] classFromCustomPath = getClassFromCustomPath();
if (classFromCustomPath == null) {
throw new FileNotFoundException();
} else {
return super.defineClass(name, classFromCustomPath, 0, classFromCustomPath.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException();
}
/**
* 以二进制流的方式将指定的 class 文件读取到系统中来
* 如果指定路劲的字节码进行了加密, 则需要在此方法中进行解密操作, 解密之后将其还远为字节数组
*
* @return byte[]
*/
private byte[] getClassFromCustomPath() {
if (this.classPath == null || "".equals(this.classPath)) {
return null;
}
File file = new File(this.classPath);
if (file.exists()) {
try (FileInputStream fileInputStream = new FileInputStream(file);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int size;
while ((size = fileInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, size);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
创建一个Log
类使用自定义类加载器进行加载:
public class Log {
public static void main(String[] args) {
System.out.println("Log class load by Custom Class Loader Success!");
}
}
运行Log
类中的main
方法,运行完成之后在out
文件目录下找到该类的class
文件即Log.class
文件。
创建一个TestCustomClassLoader
类用于测试自定义类加载器:
public class TestCustomClassLoader {
// Log 类的全类名
private static final String ALL_PACKAGE_NAME = "com.kapcb.ccc.jvm.classload.Log";
// Log 类的 class 文件路径
private static final String LOG_CLASS_PATH = "D:/DevelopTools/IDEA/IDEA-workspace/Java-Kapcb/out/production/Java-Kapcb/com/kapcb/ccc/jvm/classload/Log.class";
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
CustomClassLoader customClassLoader = new CustomClassLoader(LOG_CLASS_PATH);
Class<?> LogClass = customClassLoader.loadClass(ALL_PACKAGE_NAME);
ClassLoader classLoader = LogClass.getClassLoader();
System.out.println("Log 类的类加载器是 : [ " + classLoader + " ]");
// 获取 Log 类中的 main 方法
Method mainMethod = LogClass.getDeclaredMethod("main", String[].class);
// 实例化 Log 类
Object object = LogClass.newInstance();
// 随便传入一个参数
String[] arg = new String[]{"ad"};
// 反射激活 Log 类中的 main 方法
mainMethod.invoke(object, (Object) arg);
}
}
启动测试类的main
方法输出结果:
Log 类的类加载器是 : [ sun.misc.Launcher$AppClassLoader@18b4aac2 ]
Log class load by Custom Class Loader Success!
Process finished with exit code 0
ClassLoader
的常用API
ClassLoader
类是一个抽象类,其后所有的类加载器都继承自ClassLoader
(不包括启动类加载器),其常用API
有以下几个方法:
方法名称 | 描述 |
---|---|
getParent() |
返回当前类加载器的超类加载器 |
loadClass(String name) |
加载名称为name 的类(这里的name 是全类名),返回结果为java.lang.Class 类的实例 |
findClass(String name) |
查找名称为name 的类(这里的name 是全类名),返回结果为java.lang.Class 类的实例 |
findLoadedClass(String name) |
查找类名为name 的已经被加载过的类,返回结果为java.lang.Class 类的实例 |
defineClass(String name, byte[] b, int off, int len) |
将字节数组b 中的内容转换为一个Java 类,返回结果为java.lang.Class 类的实例 |
resolveClass(Class> cla) |
链接指定的一个Java 类 |
sun.misc.Launcher
是一个java
虚拟机的入口应用:
获取ClassLoader
的途径:
方式一:获取当前类的ClassLoader
clazz.getClassLoader();
方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();
方式三:获取系统的ClassLoader
ClassLoader.getSystemClassLoader();
方式四:获取调用者的ClassLoader
DriverManager.getCallerClassLoader();
GitHub源码地址:https://github.com/kapbc/Java-Kapcb/tree/master/src/main/java/com/kapcb/ccc/jvm
备注:此文为笔者学习
JVM
的笔记,鉴于本人技术有限,文中难免出现一些错误,感谢大家批评指正。