简述一下
类加载子系统是 Java 虚拟机(JVM)的一个重要组成部分,它负责从文件系统或网络中加载.class 文件,并将其转换为可以在 JVM 中运行的 Java 字节码。类加载器子系统的主要目的是确保加载的字节流中的信息符合当前虚拟机的要求,以保证被加载类的正确性和安全性。
类加载子系统的主要过程包括加载、链接和初始化。
加载:加载阶段主要完成以下任务:通过类名(地址)获取类的二进制字节流。将字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。在内存中生成一个代表该类的 java.lang.Class 对象,作为该类的各种数据的访问入口。
链接:链接阶段主要包括验证、准备和解析三个过程:验证:确保被加载的类具有正确的内部结构,并与其他类协调一致。这一过程可以防止非法字节码文件的加载和执行,保护虚拟机的安全。准备:为类的静态变量分配内存,并设置初始值。解析:将常量池中的符号引用转换为直接引用。
初始化:初始化阶段主要是执行类的静态初始化代码,包括调用类的静态初始化方法(如:clinit())和实例化对象。
类加载器子系统还存在一个双亲委派机制,它是一种类加载器之间的层级关系。
当一个类需要被加载时,首先会委托给它的父类加载器进行加载。如果父类加载器找不到该类,再由当前类加载器尝试加载。这样的设计可以确保 Java 核心库(如 java.lang 包)的安全和唯一性。在 Java 应用程序开发中,类的加载通常由三种类加载器相互配合执行:引导类加载器、扩展类加载器和应用程序类加载器。引导类加载器负责加载 Java 核心库,扩展类加载器负责加载 Java 扩展库,而应用程序类加载器则负责加载应用程序自身的类。
内存结构概述
简图
详细图
英文版
中文版
注意:
永久代(jdk7及以前的方法去的落地实现)只有HotSpot虚拟机有,J9,JRockit都没有
如果自己想手写一个Java虚拟机的话,主要考虑哪些结构呢?
- 类加载器
- 执行引擎
类加载器与类加载的过程
类加载子系统的作用
- 类加载器子系统负责从文件系统或者网络中加载Class文件,class文件在文件开头有特定的文件标识。
- ClassLoader只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)决定。
- 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
类加载器ClassLoader扮演的角色
- class file(在上图中就是Car.class文件)存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载到JVM当中来根据这个文件实例化出n个一模一样的实例。
- class file加载到JVM中,被称为DNA元数据模板(在上图中就是内存中的Car Class),放在方法区。
- 在.class文件–>JVM–>最终成为元数据模板,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。
类加载过程
概述
示例代码
public class HelloLoader {
public static void main(String[] args) {
System.out.println("谢谢ClassLoader加载我....");
System.out.println("你的大恩大德,我下辈子再报!");
}
}
它的加载过程是怎么样的呢?
- 执行 main() 方法(静态方法)就需要先加载main方法所在类 HelloLoader
- 加载成功,则进行链接、初始化等操作。完成后调用 HelloLoader 类中的静态方法 main
- 加载失败则抛出异常
类加载过程的完整流程图如下所示:
加载链接初始化推荐博客地址:https://www.cnblogs.com/chanshuyi/p/jvm_specification_05_load_link_init.html
加载阶段
加载:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构(静态存储结构指的就是.class字节码文件)转化为方法区的运行时数据结构(运行时数据区的结构指的就是生成的Class对象)
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
自己的理解:
加载是根据特定名称查找类或接口类型的二进制表示(Binary Representation),并由此二进制表示创建类或接口的过程。加载,就是指去寻找类或接口的过程。
补充:加载class文件的方式
- 从本地系统中直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 由其他文件生成,典型场景:JSP应用从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的防Class文件被反编译的保护措施
链接阶段
自己的理解:链接是为了让类或接口可以被 Java 虚拟机执行,而将类或接口并入虚拟机运行时状态的过程。
链接分为三个子阶段:验证 -> 准备 -> 解析
验证(Verify)
- 目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
举例:
使用 BinaryViewer软件查看字节码文件,能被JVM识别的.class文件其开头均为 CAFE BABE ,如果出现不合法的字节码文件,那么将会验证不通过。
准备(Prepare)
- 为类变量(static变量)分配内存并且设置该类变量的默认初始值,即零值
- 初始化值这里不包含用final修饰的static,因为final在编译的时候就会分配好了默认值,准备阶段会显示初始化
注意:这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
举例
变量a在准备阶段会赋初始值,但不是1,而是0,在初始化阶段会被赋值为 1
public class HelloApp {
private static int a = 1;//prepare:a = 0 ---> initial : a = 1
public static void main(String[] args) {
System.out.println(a);
}
}
解析(Resolve)
- 将常量池内的符号引用转换为直接引用的过程
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
符号引用
- 符号引用的意思是它只是一个符号,需要后续通过链接,替换为具体的内存地址。
- 反编译 class 文件后可以查看符号引用,下面带# 的就是符号引用
初始化阶段
- 初始化阶段就是执行类构造器方法()的过程
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。也就是说,当我们代码中包含static变量的时候,就会有clinit方法注意类变量就是静态变量
- ()方法中的指令按语句在源文件中出现的顺序执行
- ()不同于类的构造器。(这个类构造器和我们平时写代码的构造器的关联:构造器是虚拟机视角下的())
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
- 虚拟机必须保证一个类的()方法在多线程下被同步加锁
**自己的理解:**类或接口的初始化是指执行类或接口的初始化方法(clinit)
IDEA 中安装 JClassLib Bytecode viewer 插件,可以很方便的看字节码
说明1、2、3、4
举例:有static变量的情况,会有clinit方法
public class ClassInitTest {
private static int num = 1;
static{
num = 2;
number = 20;// 虽然number这个类变量是在后面进行声明的,但是在类加载过程中的链接阶段的准备过程会初始化变量的零值
System.out.println(num);
//System.out.println(number);//报错:非法的前向引用。
}
/**
* 1、链接阶段(linking)中的准备阶段初始化变量的初始值: number = 0 --> initial: 20 --> 10
* 2、这里因为静态代码块出现在声明变量语句前面,所以之前被准备阶段为0的number变量会首先被初始化为20,再接着被初始化成10(这也是面试时常考的问题哦)
*/
private static int number = 10;
public static void main(String[] args) {
System.out.println(ClassInitTest.num);//2
System.out.println(ClassInitTest.number);//10
}
}
clinit字节码
举例2:无 static 变量
加上类变量
举例3:
我们类的构造方法就是反编译出来的字节码init方法,注意区分init跟clinit不是一个东西
说明5
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
如上代码,加载流程如下:
- 首先,执行 main() 方法需要加载 ClinitTest1 类
- 获取 Son.B 静态变量,需要加载 Son 类
- Son 类的父类是 Father 类,所以需要先执行 Father 类的加载,再执行 Son 类的加载
说明6
虚拟机必须保证一个类的()方法在多线程下被同步加锁
public class DeadThreadTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DeadThread dead = new DeadThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread t1 = new Thread(r,"线程1");
Thread t2 = new Thread(r,"线程2");
t1.start();
t2.start();
}
}
class DeadThread{
static{
if(true){
System.out.println(Thread.currentThread().getName() + "初始化当前类");
while(true){
}
}
}
}
输出结果
线程2开始
线程1开始
线程2初始化当前类
/然后程序卡死了
程序卡死,分析原因:
- 两个线程同时去加载 DeadThread 类,而 DeadThread 类中静态代码块中有一处死循环
- 先加载 DeadThread 类的线程抢到了同步锁,然后在类的静态代码块中执行死循环,而另一个线程在等待同步锁的释放
- 所以无论哪个线程先执行 DeadThread 类的加载,另外一个类也不会继续执行。(一个类只会被加载一次)
类加载器分类
概述
- JVM严格来讲支持两种类型的类加载器 。分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)
- 从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示
查看ExtClassLoader的继承结构
查看AppClassLoader的继承的结构
查看上面两个类的继承结构只是为了证明他俩是自定义类加载器,但是我们需要注意各个类加载器中并没有继承的关系,而是一种包含的关系
public class ClassLoaderTest {
public static void main(String[] args) {
//获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//获取其上层:扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
//获取其上层:获取不到引导类加载器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null
//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
//String类使用引导类加载器进行加载的。---> Java的核心类库都是使用引导类加载器进行加载的。
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null
}
}
- 我们尝试获取引导类加载器,获取到的值为 null ,这并不代表引导类加载器不存在,因为引导类加载器由 C/C++ 语言编写的,我们获取不到
- 两次获取系统类加载器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,这说明系统类加载器是全局唯一的
虚拟机自带的加载器
启动类加载器(Bootstap ClassLoader)
启动类加载器我们也称之为引导类加载器——Bootstap ClassLoader
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部
- 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
- 并不继承自java.lang.ClassLoader,没有父加载器
- 加载扩展类和应用程序类加载器,并作为他们的父类加载器
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
- 派生于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载
应用程序类加载器(AppClassLoader)
应用程序类加载器也被称之为系统类加载器——AppClassLoader
- Java语言编写,由sun.misc.LaunchersAppClassLoader实现
- 派生于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
- 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
- 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器
测试启动类加载器和扩展类加载器的加载路径
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("**********启动类加载器**************");
//获取BootstrapClassLoader能够加载的api的路径
URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL element : urLs) {
System.out.println(element.toExternalForm());
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);
System.out.println("***********扩展类加载器*************");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}
//从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d
}
}
执行结果:
// 启动类加载器会加载我的电脑上如下路径的文件中的类
**********启动类加载器**************
file:/D:/Java/jdk-8/jre/lib/resources.jar
file:/D:/Java/jdk-8/jre/lib/rt.jar
file:/D:/Java/jdk-8/jre/lib/sunrsasign.jar
file:/D:/Java/jdk-8/jre/lib/jsse.jar
file:/D:/Java/jdk-8/jre/lib/jce.jar
file:/D:/Java/jdk-8/jre/lib/charsets.jar
file:/D:/Java/jdk-8/jre/lib/jfr.jar
file:/D:/Java/jdk-8/jre/classes
null
// 扩展类加载器如下路径中的类
***********扩展类加载器*************
D:\Java\jdk-8\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@4b67cf4d
用户自定义类加载器
什么时候需要自定义类加载器?
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。那为什么还需要自定义类加载器?
- 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)
- 修改类加载的方式
- 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)
- 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)
如何自定义类加载器?
- 开发人员可以通过继承抽象类java.lang.ClassLoader类的方式,实现自己的类加载器,以满足一些特殊的需求
- 在JDK1.2之前,在自定义类加载器时,总会去继承ClassLoader类并重写loadClass()方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findclass()方法中
- 在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URIClassLoader类,这样就可以避免自己去编写findclass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException();
} else {
//defineClass和findClass搭配使用
return defineClass(name, result, 0, result.length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
//自定义流的获取方式
private byte[] getClassFromCustomPath(String name) {
//从自定义路径中加载指定类:细节略
//如果指定路径的字节码文件进行了加密,则需要在此方法中进行解密操作。
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
ClassLoader的使用说明
关于ClassLoader
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类 |
sun.misc.Launcher 它是一个java虚拟机的入口应用
获取ClassLoader途径
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();
}
}
}
输出结果
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d
双亲委派机制
双亲委派机制原理
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
- 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常
双亲委派机制代码演示
演示一个java类加载的过程
public class MyClassLoader extends ClassLoader {
@Override这是重写的加载类的过程
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 首先尝试使用父类加载器加载类
try {
return super.loadClass(name);
} catch (ClassNotFoundException e) {
// 如果父类加载器无法加载类,则自己加载类
return findClass(name);
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 在这里实现自己的类加载逻辑,例如从特定位置加载类文件
// 这里只是一个示例,实际实现需要根据具体需求进行处理
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}
private byte[] loadClassData(String name) {
// 在这里实现加载类文件的逻辑,返回类文件的字节数组
// 这里只是一个示例,实际实现需要根据具体需求进行处理
try {
FileInputStream fis = new FileInputStream(name + ".class");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
fis.close();
bos.close();
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
// 创建自定义类加载器
MyClassLoader classLoader = new MyClassLoader();
try {
// 使用自定义类加载器
Class<?> clazz = classLoader.loadClass("shisi14.Ceshi12");
System.out.println("Class loaded: " + clazz.getName());
} catch (ClassNotFoundException e) {
System.out.println("Class not found");
}
}
}
这是jdk中ClassLoader类加载类封装的方法的源码
他的访问修饰符是被保护的这就代表他是只读的源码级别
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
我们在加载任意类时都会用到双亲委派机制
比如我们要加载一个类
我们向上委托父类找到的是系统类加载器
再向上委托继续找父类找到了扩展类加载器
继续委托我们找到了引导类加载器,他是最终的父类,但由于他是c++编写我们无法打印它。
父类发现找不到,然后到磁盘去查找加载这个类
这是磁盘查找类的源码
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
以下是尚硅谷jvm课程中的案例
举例1
1、我们自己建立一个 java.lang.String 类,写上 static 代码块
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
}
2、在另外的程序中加载 String 类,看看加载的 String 类是 JDK 自带的 String 类,还是我们自己编写的 String 类
public class StringTest {
public static void main(String[] args) {
java.lang.String str = new java.lang.String();
System.out.println("hello,atguigu.com");
StringTest test = new StringTest();
System.out.println(test.getClass().getClassLoader());
}
}
输出结果
hello,atguigu.com
sun.misc.Launcher$AppClassLoader@18b4aac2
程序并没有输出我们静态代码块中的内容,可见仍然加载的是 JDK 自带的 String 类。
把刚刚的类改一下
package java.lang;
public class String {
//
static{
System.out.println("我是自定义的String类的静态代码块");
}
//错误: 在类 java.lang.String 中找不到 main 方法
public static void main(String[] args) {
System.out.println("hello,String");
}
}
由于双亲委派机制一直找父类,所以最后找到了Bootstrap ClassLoader,Bootstrap ClassLoader找到的是 JDK 自带的 String 类,在那个String类中并没有 main() 方法,所以就报了上面的错误。
举例2
package java.lang;
public class ShkStart {
public static void main(String[] args) {
System.out.println("hello!");
}
}
输出结果:
java.lang.SecurityException: Prohibited package name: java.lang
at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:495)
Error: A JNI error has occurred, please check your installation and try again
Exception in thread "main"
Process finished with exit code 1
即使类名没有重复,也禁止使用java.lang这种包名。这是一种保护机制
举例3
当我们加载jdbc.jar 用于实现数据库连接的时候
- 我们现在程序中需要用到SPI接口,而SPI接口属于rt.jar包中Java核心api
- 然后使用双亲委派机制,引导类加载器把rt.jar包加载进来,而rt.jar包中的SPI存在一些接口,接口我们就需要具体的实现类了
- 具体的实现类就涉及到了某些第三方的jar包了,比如我们加载SPI的实现类jdbc.jar包【首先我们需要知道的是 jdbc.jar是基于SPI接口进行实现的】
- 第三方的jar包中的类属于系统类加载器来加载
- 从这里面就可以看到SPI核心接口由引导类加载器来加载,SPI具体实现类由系统类加载器来加载
双亲委派机制的优势
通过上面的例子,我们可以知道,双亲机制可以
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 自定义类:自定义java.lang.String 没有被加载。
- 自定义类:java.lang.ShkStart(报错:阻止创建 java.lang开头的类)
沙箱安全机制
- 自定义String类时:在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java.lang.String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。
- 这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
一些问题
如何判断两个class对象是否相同?
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
- 类的完整类名必须一致,包括包名
- 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同
- 换句话说,在JVM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的
对类加载器的引用
- JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的
- 如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中
- 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的(后面讲)
类的主动使用和被动使用
Java程序对类的使用方式分类:主动使用和被动使用。
主动使用,又分为7中情况:
1. 创建类的实例
2. 访问某个类或接口的静态变量,或者对该静态变量赋值
3. 调用类的静态方法
4. 反射(比如:Class.forName("com.atguigu.Test")
5. 初始化一个子类
6. Java虚拟机启动时被标明为启动类的类
7. JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例解析结果REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化则初始化
除了以上7中情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化(初始化指的就是类加载过程中的初始化阶段)