中文版见下图:
类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
通过一个类的全限定名获取定义此类的二进制字节流;然后将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最终在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载 .class 文件的几种方式:
特别注意:系统中出现大的实例就是从这个环节出现的。
Linking阶段主要分为三部分,分别是验证(Verify)、准备(Prepare)与解析(Resolve)三个环节。详细说明如下:
初始化阶段就是执行类构造器方法
构造器方法中指令按语句在源文件中出现的顺序执行。关于如何查看该方法,可以根据以下示例方法查看。比如创建一个这么的类并生成字节码文件:
随后在电脑里安装以下软件后,双击打开指定的 Java 类字节码文件,如下图所示:
查看软件下载
随后在打开软件的页面里找到 Methods 目录,展开可以查看到该类自动生成的
此外这里在说明一个示例,是与平常编写不一样的例子。就是某个静态变量的赋值在前面,声明在后面的。就好比下面的实例:
package org.blnp.cn.demos.chapter01;
/**
* @ClassName org.blnp.cn.demos.chapter01.ClassInitTest
* @Description
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-19 21:22
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-19 21:22 新建
* ----------------------------------------------
*
*/
public class ClassInitTest {
private static int num = 10;
static {
numbers = 30;
}
private static int numbers = 5;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);
System.out.println("numbers " + numbers);
}
}
原因:原因也就是上文说的 “构造器方法中指令按语句在源文件中出现的顺序执行”,因为在 连接(Linking)过程的准备(Prepare)阶段时,number变量就已经被赋值为零值了;而当进入到初始化(Initialization)阶段时被赋值为 “30” 了,当程序按顺序执行到下文时 number 变量最终被赋值为 5 了。
Linking --> Prepare:number = 0 --> initialization:number = 30 --> number = 5
根据Java代码可以得知 number 变量是先赋值成 30 在最后赋值成 5 的,此时我们通过工具查看重新生成的字节码文件核对查看下,可以得知以下结果:
可以看到也是和预计的结果是一样的。虽然在静态代码块里可以对后声明的变量进行赋值,但是并不能调用,如果进行调用的话将会提示以下错误(Illegal forward reference:非法的前向引用)。
可能会疑问,那如果是这样的话还有什么用?看下面示例就清楚了:
package org.blnp.cn.demos.chapter01;
/**
* @ClassName org.blnp.cn.demos.chapter01.DeadThredTest
* @Description
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-25 22:49
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-25 22:49 新建
* ----------------------------------------------
*
*/
public class DeadThredTest {
public static void main(String[] args) {
Runnable run = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThreads dead = new DeadThreads();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread thread1 = new Thread(run, "线程1");
Thread thread2 = new Thread(run, "线程2");
thread1.start();
thread2.start();
}
}
class DeadThreads {
static {
if (true){
System.out.println(Thread.currentThread().getName() + " 初始化当前类! ");
while (true) {
}
}
}
}
又叫启动类加载器(引导类加载器,Bootstrap ClassLoader)。这个类加载使用C/C++语言实现的,嵌套在JVM内部;
Bootstrap ClassLoader用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jarbsun.boot.class.path路径下的内容),用于提供JVM自身需要的类;此外 Bootstrap ClassLoader并不继承自java.lang.ClassLoader,它没有父加载器;并且出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类;
package org.blnp.cn.demos.chapter01;
import java.net.URL;
/**
* @ClassName org.blnp.cn.demos.chapter01.LoaderTest
* @Description
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-26 20:27
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-26 20:27 新建
* ----------------------------------------------
*
*/
public class LoaderTest {
public static void main(String[] args) {
System.out.println("\n**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = java.security.Provider.class.getClassLoader();
System.out.println(classLoader);//null 引导类加载器是获取不到的
System.out.println("\n***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
System.out.println("\n***********扩展类加载器2*************");
//获取系统类加载器:输出 => sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//获取其上层,扩展类加载器: => sun.misc.Launcher$ExtClassLoader@1b6d3586
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);
//在获取它的上层,结果无法获取到: null
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);
//对于用户创建的类来说,默认使用的是系统类加载器: => sun.misc.Launcher$AppClassLoader@18b4aac2
ClassLoader classLoader1 = LoaderTest.class.getClassLoader();
System.out.println(classLoader1);
//String 类使用的是引导类加载器进行加载的(Java的核心类库都是使用引导类加载器进行加载的): =》 null
ClassLoader classLoader2 = String.class.getClassLoader();
System.out.println(classLoader2);
}
}
这里的自定义并非是说由开发人员自定义的类加载器。从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类 ClassLoader 的类加载器都划分为自定义类加载器。
是通过 Java 语言编写的,由sun.misc.Launcher$ExtClassLoader实现。并派生于ClassLoader类,其父类加载器为启动类加载器。
从 java.ext.dirs 系统属性所指定的目录中加载类库, 或从JDK的安装目录的 jre/lib/ext 子目录(扩展目录) 下加载类库。如果用户创建的JAR放在此目录下, 也会自动由扩展类加载器加载。
也是java语言编写,由sun.misc.Launcher$AppClassLoader实现,并派生于ClassLoader类;其父类加载器为扩展类加载器。
它负责加载环境变量 classpath 或系统属性 java.class.path 指定路径下的类库,该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。通过ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。
什么是用户自定义类加载器?
在Java的日常应用程序开发中, 类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。
为什么要自定义类加载器?
用户自定义类加载器的实现步骤?
1.开发人员可以通过继承抽象类 java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
2.在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadclass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadclass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
3.在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
package org.blnp.cn.demos.chapter01;
import java.io.FileNotFoundException;
/**
* @ClassName org.blnp.cn.demos.chapter01.CustomClassLoaders
* @Description 用户自定义实现类加载器示例
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-27 23:03
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-27 23:03 新建
* ----------------------------------------------
*
*/
public class CustomClassLoaders extends ClassLoader {
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
try {
//从指定路径中加载具体的类,以二进制流的方式进行读取
byte[] result = getClassFromCsutomPath(name);
if (null == result) {
throw new FileNotFoundException();
}else {
return defineClass(name,result,0,result.length);
}
}catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCsutomPath(String name){
//todo 从自定义路径中加载指定类
return null;
}
public static void main(String[] args) {
CustomClassLoaders classLoaders = new CustomClassLoaders();
try {
Class> oneTest = Class.forName("OneTest", true, classLoaders);
Object instance = oneTest.newInstance();
System.out.println(instance.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
方法名称 | 描述 |
getParent | 返回该类加载器的超类加载器 |
loadClass(String name) | 加载名称为name的类,返回结果为java.lang.Class类的实例 |
findClass(String 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> c) | 连接指定的一个Java类 |
获取 ClassLoader 的几种方式:
方式一:获取当前类的 ClassLoader
clazz.getClassLoader()
方式二:获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
方式三:获取系统的classLoader
ClassLoader.getSystemClassLoader()
方式四:获取调用者的classLoader
DriverManager.getCallerClassLoader()
完整示例代码:
package org.blnp.cn.demos.chapter01;
/**
* @ClassName org.blnp.cn.demos.chapter01.ClassLoaderTest2
* @Description
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-27 23:30
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-27 23:30 新建
* ----------------------------------------------
*
*/
public class ClassLoaderTest2 {
public static void main(String[] args) {
try {
//1.
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
//2.
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
//3.
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
什么是双亲委派机制?
Java虚拟机对class文件采用的是按需加载的方式, 也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时, Java虚拟机采用的是双亲委派模式, 即把请求交由父类处理,它是一种任务委派模式。
工作原理?
package org.blnp.cn.demos.chapter01;
/**
* @ClassName org.blnp.cn.demos.chapter01.StringTaskTest
* @Description 双亲委派机制案例演示
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-27 23:46
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-27 23:46 新建
* ----------------------------------------------
*
*/
public class StringTaskTest {
public static void main(String[] args) {
String str = new String();
System.out.println("双亲委派机制演示……");
StringTaskTest taskTest = new StringTaskTest();
System.out.println("taskTest.getClass().getClassLoader() = " +
taskTest.getClass().getClassLoader());
}
}
package java.lang;
/**
* @ClassName java.lang.String
* @Description
* @Author liaoyibin [email protected]
* @CreateDate 2023-12-27 23:48
* @Version v1.01
* @ModifyRecord
* 版本 修改人 修改时间 修改内容描述
* ----------------------------------------------
* 1.00 liaoyibin 2023-12-27 23:48 新建
* ----------------------------------------------
*
*/
public class String {
static {
System.out.println("我是自定义的,不合规案例演示对象!!");
}
}
输出结果:
Connected to the target VM, address: '127.0.0.1:34703', transport: 'socket'
Disconnected from the target VM, address: '127.0.0.1:34703', transport: 'socket'
双亲委派机制演示……
taskTest.getClass().getClassLoader() = sun.misc.Launcher$AppClassLoader@18b4aac2
Process finished with exit code 0
根据输出结果可以得知,Java 类的加载机制是向上进行委托加载的。否则在当前工程内已有一个同包同名的类必然被加载访问并输出结果。但明显是没有,使用的是Java包的String类。
当然,还有一个最简单的验证方式。那就是在自己定义的 String 方法里直接执行 main 方法,结果如下所示:
自定义string类,但是在加载自定义string类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\string.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。
JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。
Java程序对类的使用方式分为:主动使用和被动使用。主动使用又分为七种情况:
除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。