JAVA安全之类加载器
作者介绍
类加载器的介绍
我们写的代码都是.java
结尾,然后通过javac编译成.class
的字节码文件,这个字节码文件存放在本地
.class
字节码保存着转换后的虚拟机指令,使用到对象的时候,会吧这个.class
字节码就要加载到java虚拟机内存里面,这个过程就叫做类加载
在开发过程中,了解类加载是很有作用的,ClassLoader
会把我们的class
文件加载到jvm虚拟机里面,加载到jvm虚拟机里面就可以运行了,他不会吧把全部的class的全部东西加载到jvm虚拟机里面,他会去动态加载调用,想想也是的,一次性加载那么多jar包那么多class那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制
java虚拟机有两个分区:
• 方法区:这个区存放着字节码的二进制数据
• 堆区:这个区会生成class对象,去引用方法区的节码的二进制数据
java内的自带的三个加载器
-
1. Bootstrap ClassLoader(引导类加载器)负责加载java 核心类库,例如 java.lang.xxx包中的类加载的文件夹
xxx/lib
,这个ClassLoader完全是jvm自己控制的,需要加载哪个类是最顶层的加载器,使用本地代码实现C和C++,没有直接的 Java 对象表示 -
2. Extension ClassLoader(扩展类加载器)加载扩展类库,通常是放置在
xxx/lib/ext
目录或通过系统属性由 Java 代码实现,是ClassLoader
的子类 -
3. Application ClassLoader(应用类加载器)面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类
Bootstrap ClassLoader(引导类加载器)查看加载了什么
可以使用sun.boot.class.path
系统属性查看加载了什么东西,sun.boot.class.path
是一个系统属性,用于表示 Bootstrap ClassLoader(引导类加载器) 的类加载路径
代码
package 类加载.java内的自带的三个加载器;
publicclassBootstrapClassLoader引导类加载器{
publicstaticvoidmain(String[] args){
// 获取 Bootstrap ClassLoader 的加载路径
StringbootClassPath=System.getProperty("sun.boot.class.path");
System.out.println("Bootstrap ClassLoader加载的路径:\n"+ bootClassPath);
}
}
运行结果
Bootstrap ClassLoader加载的路径:
/home/zss/YingYong/jdk1.8.0_65/jre/lib/resources.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/rt.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/sunrsasign.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jsse.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jce.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/charsets.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jfr.jar:/home/zss/YingYong/jdk1.8.0_65/jre/classes
Extension ClassLoader(扩展类加载器)查看加载了什么
java.ext.dirs
系统属性查看
package 类加载.java内的自带的三个加载器;
publicclassExtensionClassLoader扩展类加载器{
publicstaticvoidmain(String[] args){
System.out.println(System.getProperty("java.ext.dirs"));
}
}
运行结果
/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext:/usr/java/packages/lib/ext
Application ClassLoader(应用类加载器)查看加载了什么
java.ext.dirs
系统属性查看
package 类加载.java内的自带的三个加载器;
publicclassApplicationClassLoader应用类加载器{
publicstaticvoidmain(String[] args){
// 获取 AppClassLoader 的类路径
System.out.println(System.getProperty("java.class.path"));
}
}
运行结果
/home/zss/YingYong/jdk1.8.0_65/jre/lib/charsets.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/deploy.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/cldrdata.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/dnsns.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/jaccess.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/jfxrt.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/localedata.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/nashorn.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/sunec.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/sunjce_provider.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/sunpkcs11.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/ext/zipfs.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/javaws.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jce.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jfr.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jfxswt.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/jsse.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/management-agent.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/plugin.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/resources.jar:/home/zss/YingYong/jdk1.8.0_65/jre/lib/rt.jar:/home/zss/笔记/代码/java/javaAll3/target/classes:/home/zss/YingYong/idea-IU-241.18034.62/lib/idea_rt.jar
加载器加载顺序
-
1. 检查类是否已加载 每个类加载器在加载类之前,会先检查目标类是否已经被加载。如果已加载,则直接返回,不重复加载
-
2. 双亲委派机制(下面详细说) 当某个类加载器收到加载类的请求时,按照以下顺序进行处理:
-
1. 委托父加载器加载 首先将加载请求委托给父加载器(即上一层的加载器)
-
2. 父加载器加载失败时,尝试自身加载 如果父加载器无法加载目标类,则当前加载器会尝试自行加载
-
我们先了解一下getParent()方法
每个 ClassLoader
都有一个父加载器,通过调用 getParent()
方法,我们可以获取到该类加载器的父加载器
代码演示加载
创建一个类abc的java类
package 类加载.java加载器加载顺序;
public class abc {
}
我们加载这个abc这个类看看他的加载器
package 类加载.java加载器加载顺序;
publicclassrun{
publicstaticvoidmain(String[] args){
// 获取 abc 类的类加载器(应用类加载器)
ClassLoaderappClassLoader= abc.class.getClassLoader();
System.out.println(appClassLoader);// 输出 abc 类的类加载器
// 获取 abc 类的父加载器
System.out.println(appClassLoader.getParent());// 输出父加载器(扩展类加载器)
// 获取父加载器的父加载器
System.out.println(appClassLoader.getParent().getParent());// 输出引导类加载器(为 null)
}
}
运行结果
sun.misc.Launcher$AppClassLoader@14dad5dc
sun.misc.Launcher$ExtClassLoader@1540e19d
null
为什么没有显示Bootstrap ClassLoader因为由于引导类加载器(Bootstrap ClassLoader)是 JVM 内部实现的,他是c++编写的
双亲委派
ClassLoader
类使用委托模型来搜索类和资源,每个 ClassLoader
实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader
实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器,原理详情下面的loadClass分析代码执行过程就是双亲委派机制
ClassLoader 类的方法
ClassLoader是加载类的核心类,在程序运行时,并不会一次性加载所有的 class 文件进入内存,而是通过 Java 的类加载机制(ClassLoader)进行动态加载,从而转换成 java.lang.Class 类的一个实例
方法名 | 作用 | 备注 |
loadClass(String name) | 加载指定名称的类,遵循双亲委派模型。 | 常用方法,默认实现先调用 findClass 。 |
findClass(String name) | 寻找类的定义,通常由自定义加载器重写。 | 默认抛出 ClassNotFoundException 。 |
defineClass(String name, byte[] b, int off, int len) | 将二进制字节数组转换为 Java 类对象。 | 用于自定义类加载器加载字节码。 |
resolveClass(Class<?> c) | 解析类及其依赖关系。 | 通常在加载类后手动调用以完成解析。 |
findLoadedClass(String name) | 检查类是否已被加载,如果已加载则返回 Class 对象。 | 防止重复加载同一个类。 |
getParent() | 获取当前类加载器的父加载器。 | 返回父类加载器;引导类加载器返回 null 。 |
getSystemClassLoader() | 获取系统类加载器(即 AppClassLoader )。 | 用于加载应用程序类路径下的类。 |
getResource(String name) | 查找指定名称的资源,返回资源的 URL。 | 可用于加载配置文件等资源。 |
getResourceAsStream(String name) | 以输入流形式查找资源,便于读取资源内容。 | 配合流操作读取文件资源。 |
getResources(String name) | 查找所有与指定名称匹配的资源,返回一个枚举的 URL 集合。 | 用于加载多个相同名称的资源。 |
clearAssertionStatus() | 清除当前类加载器及其加载的类的断言状态。 | 重置断言状态。 |
setDefaultAssertionStatus(boolean enabled) | 设置默认断言状态,适用于当前类加载器加载的所有类。 | 改变全局默认断言行为。 |
setClassAssertionStatus(String className, boolean enabled) | 设置特定类的断言状态。 | 单独调整某个类的断言启用状态。 |
setPackageAssertionStatus(String packageName, boolean enabled) | 设置特定包的断言状态。 | 对某个包中的所有类调整断言启用状态。 |
loadClass对象方法介绍
loadClass
是 ClassLoader
类中最核心的方法的其中一个,用于加载指定名称的类。它遵循 双亲委派模型
点进去
在点进去
里面的源代码是如下:
protected Class<?> loadClass(String name,boolean resolve)throwsClassNotFoundException{
synchronized(getClassLoadingLock(name)){// 这段代码的作用是保证类加载过程的线程安全性
Class<?> c = findLoadedClass(name);
if(c ==null){
longt0=System.nanoTime();//记录当前时间的纳秒级时间戳
try{
if(parent !=null){// parent是当前类加载器的父类加载器
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
}
if(c ==null){
longt1=System.nanoTime();//记录当前时间的纳秒级时间戳
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);// 计算时间
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);// 计算时间
sun.misc.PerfCounter.getFindClasses().increment();// 计算时间
}
}
if(resolve){
resolveClass(c);
}
return c;
}
}
读一下源代码
1、第一行定义的这个loadClass,他接收两个参数
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
第一个参数是要加载的类的完全限定名称,第二个参数如果设置是true
则调用 resolveClass(Class<?> c)
2、第三行,
Class<?> c = findLoadedClass(name);
用于检查某个类是否已经被当前类加载器加载过,如果已经加载直接返回该类的 Class对象,如果未加载返回 null
3、第八行
parent.loadClass(name, false);
这个会拿他的父类去加载一遍这个类,看看有没有这个类,这个调用是基于父类委派模型,如果父加载器存在它会尝试加载该类
4、第十行
c = findBootstrapClassOrNull(name);
如果没有父类(parent)等于空就会调用上面的这行代码,findBootstrapClassOrNull是会去尝试
加载类
5、第15行
if (c == null) {
如果类 c 还没有被加载
6、第17行
c = findClass(name);
用于加载指定名称的类,如果当前加载器无法加载该类可能会抛出ClassNotFoundException
findClass对象方法介绍
findClass(String name)
参数是一个类名
findClass方法用于在类加载器中查找并加载指定名称的类,他会根据类的名称查询加载类的字节码返回一个 Class对象,如果没有找到抛出ClassNotFoundException异常
defineClass对象方法介绍
defineClass(String name, byte[] b, int off, int len)
name
:要定义的类的全限定名(包括包名)例如com.xxx.aaabbb
b
:包含类字节码的字节数组,这个字节数组应该包含类的所有字节码(从 .class
文件读取出来的内容)
off
:字节数组的起始偏移量,通常为 0,表示从字节数组的第一个字节开始读取
len
:要读取的字节长度。如果传入的字节数组 b 是整个类的字节码,通常传入该字节数组的长度
defineClass方法是将原始字节码(二进制)转换成 class对象的核心方法,比如.class文件、网络
它允许将已经加载的类字节码转化为可以在jvm 中使用的class实例,如果字节码不符合 jvm 的格式规范会抛出异常
方法使用ClassLoader
loadClass使用
创建一个MyClass方法下面是代码
package 类加载.类加载.test_loadClass;
publicclassMyClass{
privateStringmyField="Hello, World!";
publicStringgetMyField(){
return myField;
}
publicStringStudent(String name){
System.out.println("Student: "+ name);
return name;
}
}
使用loadClass调用上面创建的类
package 类加载.类加载.test_loadClass;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
publicclasstest_loadClass{
publicstaticvoidmain(String[] args)throwsClassNotFoundException,NoSuchFieldException,NoSuchMethodException,InvocationTargetException,InstantiationException,IllegalAccessException{
// 获取当前类加载器
ClassLoaderclassLoader= test_loadClass.class.getClassLoader();
// 使用 loadClass 方法加载类
Class<?> clazz = classLoader.loadClass("类加载.类加载.test_loadClass.MyClass");
System.out.println("Loaded class: "+ clazz.getName());
// 方法返回某个类的所有public方法
Method[] a = clazz.getMethods();
for(Method i:a){
System.out.println(i);
}
}
}
运行结果
Loaded class:类加载.类加载.test_loadClass.MyClass
public java.lang.String类加载.类加载.test_loadClass.MyClass.getMyField()
public java.lang.String类加载.类加载.test_loadClass.MyClass.Student(java.lang.String)
publicfinalvoid java.lang.Object.wait(long,int)throws java.lang.InterruptedException
publicfinalnativevoid java.lang.Object.wait(long)throws java.lang.InterruptedException
publicfinalvoid java.lang.Object.wait()throws java.lang.InterruptedException
publicboolean java.lang.Object.equals(java.lang.Object)
public java.lang.String java.lang.Object.toString()
publicnativeint java.lang.Object.hashCode()
publicfinalnative java.lang.Class java.lang.Object.getClass()
publicfinalnativevoid java.lang.Object.notify()
publicfinalnativevoid java.lang.Object.notifyAll()
Class.forName
Class.forName()也可以对类进行加载,但是Class.forName()和ClassLoader是有区别的
抽象类ClassLoader中实现的方法loadClass,loadClass只是加载,不会解析更不会初始化所反射的类,常用于做懒加载,提高加载速度,使用的时候再通过.newInstance()真正去初始化类
看一下Class.forName()加载一个累
创建一个被加载的类
package 类加载.forName和ClassLoader;
public class MyClass {
static {
System.out.println("aaaaa");
}
}
调用MyClass代码
package 类加载.forName和ClassLoader;
publicclasstest{
publicstaticvoidmain(String[] args)throwsClassNotFoundException{
// 动态加载 MyClass 类
Class<?> clazz =Class.forName("类加载.forName和ClassLoader.MyClass");
}
}
运行结果
aaaaa
自定义类加载
为什么要自定义类加载器,ClassLoader在默认情况下只会加载文件系统或 jar 包中加载类,这个限制很大,我们就可以自定义类加载器去加载网络或其他自定义位置类,可以类文件经过加密处理,然后接收然后解密后加载,增加应用安全性
自定义类加载的步骤
-
1. 继承
ClassLoader
类 自定义类加载器需要继承ClassLoader
并重写其方法,如findClass
-
2. 重写
findClass
方法findClass
是类加载器的核心方法,用于加载类的字节码并将其定义为一个Class
对象 -
3. 通过
defineClass
方法定义类 使用defineClass
将字节码数组转换为Class
对象
代码演示:
1、创建了一个类加载器
package 类加载.自定义类加载;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
publicclassMyClassLoaderextendsClassLoader{
staticString URL="http://127.0.0.1:8081/";
//自定义的类加载器,加载来自网络的字节码
@Override
protectedClass<?> findClass(String name)throwsClassNotFoundException{
byte[] classData =null;
try{
classData = loadClassDataFromNetwork(name);
}catch(IOException e){
thrownewRuntimeException(e);
}
if(classData ==null){
thrownewClassNotFoundException(name);
}
return defineClass(name, classData,0, classData.length);// 定义类
}
//模拟从网络加载类的字节码数据
privatebyte[] loadClassDataFromNetwork(String className)throwsIOException{
StringencodedClassName= className.replace('.','/')+".class";
encodedClassName=java.net.URLEncoder.encode(encodedClassName,"UTF-8").replace("%2F","/");
URLurl=newURL(URL + encodedClassName);//请求从远程服务器加载字节码
InputStreaminputStream= url.openStream();
ByteArrayOutputStreambyteArrayOutputStream=newByteArrayOutputStream();
int byteRead;
while((byteRead = inputStream.read())!=-1){
byteArrayOutputStream.write(byteRead);
}
return byteArrayOutputStream.toByteArray();
}
}
2、创建一个测试的类
名字就叫abc
package 类加载.自定义类加载;
publicclassabc{
privateString message;
publicabc(){
this.message ="aaaaaaaaaaaaa";
}
publicabc(String var1){
this.message = var1;
}
publicStringgetMessage(){
returnthis.message;
}
publicvoidsetMessage(String var1){
this.message = var1;
}
publicvoidprintMessage(){
System.out.println(this.message);
}
}
生成class文件
javac -d . abc.java
3、然后在创建一个测试我们创建的自定义类
TestMyClassLoader.java文件内容
package 类加载.自定义类加载;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
publicclassTestMyClassLoader{
publicstaticvoidmain(String[] args)throwsClassNotFoundException,NoSuchMethodException,InvocationTargetException,InstantiationException,IllegalAccessException,UnsupportedEncodingException{
MyClassLoadermyClassLoader=newMyClassLoader();// 创建自定义类加载器实例
Class<?> loadedClass = myClassLoader.loadClass("类加载.自定义类加载.abc");//使用自定义类加载器加载类
System.out.println("加载器信息:"+ loadedClass.getClassLoader());//打印类加载器信息
// 实例化对象
Objectobj= loadedClass.getDeclaredConstructor().newInstance();
// 调用 printMessage 方法
loadedClass.getMethod("printMessage").invoke(obj);
}
}
整个命令结构:
我们在这个目录下启动web服务
python3 -m http.server 8081
然后我们运行TestMyClassLoader.java可以看见被加载了
参考
https://blog.csdn.net/succing/article/details/123308677
https://blog.csdn.net/succing/article/details/123308677
https://blog.csdn.net/briblue/article/details/54973413
书籍:《java代码审计》
https://gityuan.com/2016/01/24/java-classloader/