Java类加载
知识储备:字节码文件
Java的字节码文件与类加载息息相关,在了解类加载之前必须先了解下字节码文件是什么。
首先,我们在IDEA编写的代码文件(.java)叫源代码文件,这种文件程序员能看懂,但机器不一定或者说识别起来有困难,所以必须转化成机器看的懂的文件(.class),这种转化过程是通过编译器(java compile)使用javac命令实现的。
下面分别区别一下源代码文件和字节码文件:
//源代码文件
package com.cmower.java_demo;
public class Test {
public static void main(String[] args) {
System.out.println("111");
}
}
//字节码文件
xxd Test.class
00000000: cafe babe 0000 0034 0022 0700 0201 0019 .......4."......
00000010: 636f 6d2f 636d 6f77 6572 2f6a 6176 615f com/cmower/java_
00000020: 6465 6d6f 2f54 6573 7407 0004 0100 106a demo/Test......j
00000030: 6176 612f 6c61 6e67 2f4f 626a 6563 7401 ava/lang/Object.
00000040: 0006 3c69 6e69 743e 0100 0328 2956 0100 ..<init>...()V..
00000050: 0443 6f64 650a 0003 0009 0c00 0500 0601 .Code...........
00000060: 000f 4c69 6e65 4e75 6d62 6572 5461 626c ..LineNumberTabl
类加载过程
Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。验证、准备、解析这三个过程也可统称为链接。
下面简析这几个过程都发生了什么。
1)Loading(载入)
JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、也可能是 jar 包,甚至网络)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class
对象。
简单来说,就是通过一个类的完全限定查找(全类名查找)此类字节码文件,并利用字节码文件创建一个Class对象。
2)Verification(验证)
JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
- 是否所有方法都遵守访问控制关键字的限定。
- 方法调用的参数个数和类型是否正确。
- 确保变量在使用之前被正确初始化了。
- 检查变量是否被赋予恰当类型的值。
3)Preparation(准备)
JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等),这些有可能并非是我们设定的值,只有常量(用static final修饰)才会赋上代码设定的值。
代码举例:
public String a = "非静态变量";
public static String b = "静态变量";
public static final String c = "静态常量";
a 不会被分配内存,而 b 会;但 b 的初始值不是“静态变量”而是 null,c 的值是“静态常量”。
4)Resolution(解析)
该阶段将常量池中的符号引用转化为直接引用。
符号引用是指以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。
在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.Test
类引用了com.TestFather
类,编译时 Test
类并不知道TestFather
类的实际内存地址,因此只能使用符号 com.TestFather
。
直接引用通过对符号引用进行解析,找到引用的实际内存地址。
5)Initialization(初始化)
在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值。换句话说,初始化阶段是执行类构造器方法的过程。
若该类具有静态类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
比如:public static String b = "静态变量";
此时的b就不再是null了,而是赋值为“静态变量”。
类加载器
下面我们对载入这一过程进行深究,也就是对将.class文件加载入内存这一过程进行研究,这一过程是通过类加载器完成的。
类加载器大致分为两种:一种是JVM自带的类加载器,一种是我们自定义的类加载器,一般来说JVM自带的已经足够满足我们大部分需求。
而JVM自带的类加载器又分为了以下三个:
-
启动类加载器(Bootstrap Class-Loader)
它是最底层的类加载器,是虚拟机的一部分,由C++实现,且没有父类加载器,也没有继承ClassLoader类,主要负责加载jre/lib
包下面的 jar 文件,比如说常见的 rt.jar。 -
扩展类加载器(Extension or Ext Class-Loader)
它是由java语言编写,父加载器是根类加载器。负责加载加载jre/lib/ext
包下面的 jar 文件 -
应用类加载器(Application or App Clas-Loader)
它也是纯java类,父类加载器是扩展类加载器,它负责从classpath环境变量
或者系统属性java.class.path
所指定的目录中加载类。它是用户自定义的类加载器的默认父加载器。一般情况下,它也是程序中的默认类加载器,可以通过
ClassLoader.getSystemClassLoader
直接获得。
类加载机制:双亲委派机制
每个类加载器都是有父类加载器的,除了根类加载器。但类加载器都很懒,当要加载一个类时,它会先提交给他的父类加载器处理,它的父类加载器又交给它的父类加载器处理,一直传递到根类加载器。如果父类加载器没办法加载这个类,又会把任务传回子类。
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过类名.class.getClassLoader()
可以获取到此引用;然后通过 加载器名.loader.getParent()
可以获取类加载器的上层类加载器。我们可以通过这两个方法来验证这个机制。
public class Test {
public static void main(String[] args) {
ClassLoader classLoader1 = Object.class.getClassLoader();
System.out.println("Object的类加载器:"+classLoader1);
ClassLoader classLoader2 = DNSNameService.class.getClassLoader();
System.out.println("DNS的类加载器:"+classLoader2);
ClassLoader classLoader3 = Test.class.getClassLoader();
System.out.println("自建类的类加载器:"+classLoader3);
}
}
Object的类加载器是:NULL
DNS的类加载器是:sun.misc.Launcher$ExtClassLoader
自建类的类加载器是:sun.misc.Launcher$AppClassLoader
其实这三个类的默认类加载器都是系统类加载器,他们都使用了双亲委托机制,他们都将加载任务传到顶层加载器,然后看是否能加载。由于Obejct类能被根类加载器加载,DNS类能被扩展类加载器加载,自建类只能被系统类加载器加载,所以只有自建类的类加载器是系统类加载器,其他两个都被顶层加载器处理了。
这里特别说明一下,Object类加载器打印出来是NULL,并不是说它没有类加载器,而是根类加载器比较特殊,它是虚拟机一部分,无法直接打应出来。
那说了这么多,这个机制有什么作用呢?下面来说一下双亲委派机制的作用。
1)采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子类加载器再加载一次。
2)其次是考虑到安全因素,保证了java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的Integer.class
,这样便可以防止核心API库被随意篡改。
注意,假如我们写了一个类,当它的全路径名是java.lang.Test
。但该类并不存在java.lang
中,经过双亲委托模式,传递到根加载器中,但是根无法加载该类,所以向下一直传递到系统类加载器加载该类。但由于java.lang
是核心API包,需要访问权限,所以系统类加载器也无法加载,所以此时会报出异常。故包名正确书写很重要。
ClassLoader类
所有类加载器除了根类加载器外,都必须继承java.lang.ClassLoader
这个类,当我们要自定义类加载器时,也要继承这个类。他是一个抽象类,主要方法有以下这些:
-
loadClass(String,boolean resolve)
该方法使用指定的二进制名称(全类名)来加载该类,这个方法也是双亲委派模式的实现代码,在我们自定义加载器时不该复写这个方法,其执行逻辑按以下顺序:- 调用
findLoadClass(String)
,检查缓存中是否已经加载此类 - 使用双亲委派模式,将类交给父类加载器处理
- 父加载器都处理不了,调用自身的
findclass(String name)
来加载
对于第二个参数
boolean resolve
,是一个标志,表示是否调用resolveClass(Class<?> c)
方法。 - 调用
-
findClass(String)
这个方法就是主要的加载方法,需要我们去复写。需要注意的是ClassLoader类中并没有实现findClass()
方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException
异常。
//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
-
defineClass(byte[] b, int off, int len)
这个方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象。如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,
defineClass()
方法通常与findClass()
方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()
方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass(
)方法生成类的Class对象。举个栗子:
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}
-
resolveClass(Class<?> c)
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析,其解析操作需要等待初始化阶段进行。因为在loadClass方法里如果第二个参数为true是有调用了resolveClass这个方法的。
URLClassLoader
在java.net
包中,JDK为我们提供了一个功能更加强大的类加载器,叫URLClassLoader,是ClassLoader的扩展。
构造方法:
-
URLClassLoader(URL[] urls)
:指定要加载的类的URL地址,父类加载器为系统加载器。 -
URLClassLoader(URL[] urls,ClassLoader parent)
:不仅能指定加载的类的URL地址,还能指定父类加载器。
URLClassLoader有两个应用,一个是加载我们本地磁盘上的类,一个是加载网络上的类(部署在Tomcat上的类)。
public class Test {
//加载磁盘上的类
public static void main(String[] args) throws Exception {
File file = new File("d:/");
URI uri = file.toURI();
URL url = uri.toURL();
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
classLoader.loadClass("com.test.Test");
}
}
public class Test {
//加载网络上的类
public static void main(String[] args) throws Exception {
URL url = new URL("http://localhots:8080/test");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
classLoader.loadClass("com.test.Test");
}
}
自定义类加载器
自定义的文件类加载器
public class Test extends ClassLoader{
private String directory;//被加载类所在的目录
public Test(ClassLoader parent, String directory) {
super(parent);
this.directory = directory;
}
public Test(String directory) {
this.directory = directory;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//把类名换为目录
String file = directory+File.separator+name.replace("。",File.separator)+".class";
//构建输入流
InputStream in = new FileInputStream(file);
//构建字节输出流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte bytes[] = new byte[1024];
int len = -1;
while((len = in.read(bytes)) != -1){
byteArrayOutputStream.write(bytes,0,len);
}
//读取到的字节码的二进制数据
byte data[] = byteArrayOutputStream.toByteArray();
in.close();
byteArrayOutputStream.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
e.printStackTrace();
}
}
}
自定义的网络类加载器
public class Test extends ClassLoader{
private String url;
public Test(ClassLoader parent, String url) {
super(parent);
this.url = url;
}
public Test(String url) {
this.url = url;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
String path = url + "/" + name.replace(".","/")+".class";
URL url = new URL(path);
InputStream in = url.openStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] bytes = new byte[1024];
int len = -1;
while((len = in.read(bytes)) != -1){
byteArrayOutputStream.write(bytes,0,len);
}
byte[] data = byteArrayOutputStream.toByteArray();
in.close();
byteArrayOutputStream.close();
return defineClass(name,data,0,data.length);
} catch (IOException e) {
e.printStackTrace();
}
}
}
本文地址:https://blog.csdn.net/meiziziLOLOLO/article/details/107348432
推荐阅读
-
Python 类与元类的深度挖掘 I【经验】
-
java文件名判断练习
-
Android自定义网络连接工具类HttpUtil
-
基本数据类型和包装类的区别
-
java调用sqlldr报错:Message 2100 not found
-
java字符串截取
-
样式加载不出来,浏览器控制台报错:Resource interpreted as Stylesheet but transferred with MIME type text/html
-
浅析:电商类网站如何玩转网络推广
-
C#中实现判断某个类是否实现了某个接口
-
Java并发——结合CountDownLatch源码、Semaphore源码及ReentrantLock源码来看AQS原理