ClassLoader加载机制

ClassLoader加载机制


Java类加载机制

​ 我们编写”*.java”的文件需要先编译成”*.class”文件,当程序运行时,如果需要用到某个类,JVM通过类加载器把对应的class文件加载到内存中,然后再执行代码


JAVA的类加载器体系


三大类加载器:


package com.test;

import org.apache.commons.dbutils.DbUtils;

public class ClassLoaderDemo1 {
public static void main(String[] args) throws ClassNotFoundException {
//父子关系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader
ClassLoader cl1 = ClassLoaderDemo1.class.getClassLoader();
System.out.println("cl1 > " + cl1);
System.out.println("parent of cl1 > " + cl1.getParent());
//BootStrap ClassLoader由C++开发,是JVM虚拟机的一部分,本身不是JAVA类
System.out.println("grant parent of cl1 > " + cl1.getParent().getParent());

//String,Int等继承类由BootStrap Classloader加载
ClassLoader cl2 = String.class.getClassLoader();
System.out.println("cl2 > "+cl2);
System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());

//java指令可以通过增加-verbose:class -verbose:gc 参数在启动时打印出类加载情况
//BootStap ClassLoader,加载java基础类。这个属性不能在java指令中指定,推断不是java语言处理
System.out.println("BootStrap ClassLoader加载目录:" + System.getProperty("sun.boot.class.path"));
//Extention ClassLoader,加载JAVA_HOME/ext下的jar包,可通过-D java.ext.dirs另行指定目录
System.out.println("Extention ClassLoader加载目录:" + System.getProperty("java.ext.dirs"));
//AppClassLoader 加载CLASSPATH,应用下的Jar包,可通过-D java.class.path另行指定目录
System.out.println("Application ClassLoader加载目录:" + System.getProperty("java.class.path"));
}
}

第一层结果分析:

首先就是我们程序本身加载的Classloader是AppClassLoader

我们通过getParent可以看到父类是ExtClassLoader,而ExtClassLoader父类是null

//父子关系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader
ClassLoader cl1 = ClassLoaderDemo1.class.getClassLoader();
System.out.println("cl1 > " + cl1);
System.out.println("parent of cl1 > " + cl1.getParent());
//BootStrap ClassLoader由C++开发,是JVM虚拟机的一部分,本身不是JAVA类
System.out.println("grant parent of cl1 > " + cl1.getParent().getParent());

返回结果

cl1 > sun.misc.Launcher$AppClassLoader@18b4aac2
parent of cl1 > sun.misc.Launcher$ExtClassLoader@74a14482
grant parent of cl1 > null

第二层分析:

我们可以看到String、int、List类是通过BootStrap Classloader加载的,返回的都是null

//String,Int等继承类由BootStrap Classloader加载
ClassLoader cl2 = String.class.getClassLoader();
System.out.println("cl2 > "+cl2);
System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());

返回结果:

cl2 > null
null

java类加载体系

通过前面我们可以得到下面

BootStrap Classloader > ExtClassLoader > AppClassLoader

第三层分析:

我们可以看到不同的加载器有不同的加载目录

//java指令可以通过增加-verbose:class -verbose:gc 参数在启动时打印出类加载情况
//BootStap ClassLoader,加载java基础类。这个属性不能在java指令中指定,推断不是java语言处理
System.out.println("BootStrap ClassLoader加载目录:" + System.getProperty("sun.boot.class.path"));
//Extention ClassLoader,加载JAVA_HOME/ext下的jar包,可通过-D java.ext.dirs另行指定目录
System.out.println("Extention ClassLoader加载目录:" + System.getProperty("java.ext.dirs"));
//AppClassLoader 加载CLASSPATH,应用下的Jar包,可通过-D java.class.path另行指定目录
System.out.println("Application ClassLoader加载目录:" + System.getProperty("java.class.path"));

返回结果:

格式化后

BootStrap ClassLoader加载目录:
E:\jar\commons-dbutils-1.7.jar;
D:\environment\jdk\jre\lib\resources.jar
D:\environment\jdk\jre\lib\rt.jar;
D:\environment\jdk\jre\lib\sunrsasign.jar;
D:\environment\jdk\jre\lib\jsse.jar;
D:\environment\jdk\jre\lib\jce.jar;
D:\environment\jdk\jre\lib\charsets.jar;
D:\environment\jdk\jre\lib\jfr.jar;
D:\environment\jdk\jre\classes

Extention ClassLoader加载目录:
D:\environment\jdk\jre\lib\ext;
C:\WINDOWS\Sun\Java\lib\ext

Application ClassLoader加载目录:
D:\environment\jdk\jre\lib\charsets.jar;
D:\environment\jdk\jre\lib\deploy.jar;
D:\environment\jdk\jre\lib\ext\access-bridge-64.jar;
D:\environment\jdk\jre\lib\ext\cldrdata.jar;
D:\environment\jdk\jre\lib\ext\dnsns.jar;
D:\environment\jdk\jre\lib\ext\jaccess.jar;
D:\environment\jdk\jre\lib\ext\jfxrt.jar;
D:\environment\jdk\jre\lib\ext\localedata.jar;
D:\environment\jdk\jre\lib\ext\nashorn.jar;
D:\environment\jdk\jre\lib\ext\sunec.jar;
D:\environment\jdk\jre\lib\ext\sunjce_provider.jar;
D:\environment\jdk\jre\lib\ext\sunmscapi.jar;
D:\environment\jdk\jre\lib\ext\sunpkcs11.jar;
D:\environment\jdk\jre\lib\ext\zipfs.jar;
D:\environment\jdk\jre\lib\javaws.jar;
D:\environment\jdk\jre\lib\jce.jar;
D:\environment\jdk\jre\lib\jfr.jar;
D:\environment\jdk\jre\lib\jfxswt.jar;
D:\environment\jdk\jre\lib\jsse.jar;
D:\environment\jdk\jre\lib\management-agent.jar;
D:\environment\jdk\jre\lib\plugin.jar;
D:\environment\jdk\jre\lib\resources.jar;
D:\environment\jdk\jre\lib\rt.jar;
E:\ideaProject\classTest\target\classes;
D:\apache-maven-3.8.3\maven-repo\commons-dbutils\commons-dbutils\1.7\commons-dbutils-1.7.jar;
D:\IntelliJ IDEA 2020.1.1\lib\idea_rt.jar

双亲委派

一个java类加载到JVM的过程

从图看每个加载器都有一个加载目录,加载的类都有一个缓存


总结:

  1. 每个加载器对它加载过的类都有一个缓存
  2. 向上委托查找,向下委托加载

为什么要有双亲委派?


JDK的类加载对象

与上面java类加载器的体系分开


基于一个classLoader接口,实现子接口SecureClassLoader,下面最关键实现类URLClassLoader,后面就是基于URLClassLoader的子类

ClassLoader -> SecureClassLoader -> URLClassLoader -> ExtClassLoader,AppClassLoader

代码拆分

通过URLClassLoader进行代码拆分,使得我们要写的代码分离

我们也可以封装为jar包,放在web服务器上、maven仓库(drools规则引擎)、本地等

LoadClassTest.java

package com.test;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///E:\ideaProject\classTest\out\artifacts\classTest_jar\Test.jar!/")});
Class<?> c = urlClassLoader.loadClass("Test");
c.newInstance();
}
}

Test.jar

import java.io.IOException;

public class Test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {

}
}

代码混淆

jar包我们都可以进行反编译,我们可以对jar包进行混淆

  1. 修改class文件的后缀,有效避免反编译

通过本地路径来读取class文件

我们将class后缀修改为myclass,加载的时候需要将后缀修改为class才能被执行,这样我们就要自定义一个类加载器


LoadClassTest.java

package com.test;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
TestClassLoader testClassLoader = new TestClassLoader("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\");
Class<?> c = testClassLoader.loadClass("Test");
c.newInstance();
}
}

TestClassLoader.java

package com.test;

import com.sun.xml.internal.ws.util.ByteArrayBuffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader;

/**
* 从*.myclass来加载对象
*/
public class TestClassLoader extends SecureClassLoader {
//文件路径
private String classPath;

//构造方法传入文件路径
public TestClassLoader(String classPath){
this.classPath = classPath;
}

//需要实现从.myclass,重写父类方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//获取myclass完整文件路径
String filePath = this.classPath + name.replace(".","\\").concat(".myclass");
FileInputStream fis;
ByteArrayBuffer ba = new ByteArrayBuffer();
byte[] b;
int code;

//文件读取
try {
fis =new FileInputStream(new File(filePath));
while ((code = fis.read()) != -1){
ba.write(code);
}
//转换为byte字节
b = ba.toByteArray();
//字节码转化为Class
return defineClass(name,b,0,b.length);
} catch (Exception e) {
throw new ClassNotFoundException("自定义类文件不存在");
}
}
}


Test.myclass

import java.io.IOException;

public class Test {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {

}
}

通过读取jar包来读取class文件


读取jar包的文件,并且将文件myclass后缀修改为class后加载

package com.test;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
// TestClassLoader testClassLoader = new TestClassLoader("\"E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\Test.jar\"");
TestClassLoader2 testClassLoader2 = new TestClassLoader2("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\Test.jar");
Class<?> c = testClassLoader2.loadClass("Test");
c.newInstance();
}
}

TestClassLoader2.java

读取的是jar包中的Test.myclass文件

package com.test;

import com.sun.xml.internal.ws.util.ByteArrayBuffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.security.SecureClassLoader;

/**
* 实现jar包中找到class文件来加载
*/
public class TestClassLoader2 extends SecureClassLoader {
//用一个jar包来模拟工程
private String jarFile;

public TestClassLoader2(){
this.jarFile = jarFile;
}

public TestClassLoader2(String jarFile) {
this.jarFile = jarFile;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//获取myclass完整文件路径
URL fileURL;
InputStream inputStream;
String classPath = name.replace(".","/").concat(".myclass");
FileInputStream fis;
ByteArrayBuffer ba = new ByteArrayBuffer();
byte[] b;
int code;

//文件读取
try {
fileURL = new URL("jar:file:\\"+this.jarFile+"!/"+classPath);
inputStream = fileURL.openStream();
while ((code = inputStream.read()) != -1){
ba.write(code);
}
//转换为byte字节
b = ba.toByteArray();
//字节码转化为Class
return defineClass(name,b,0,b.length);
} catch (Exception e) {
throw new ClassNotFoundException("自定义类文件不存在");
}
}
}

  1. 通过改动二进制文件或者是加密来进行混淆

我们先写一个改变二进制文件,在文件头前面加上一个字符

package com.test;

import java.io.*;

public class FileCLass {
public static void main(String[] args) throws IOException {
File file1 = new File("E:\\ideaProject\\classTest\\target\\classes\\Test.class");
File file2 = new File("E:\\ideaProject\\classTest\\target\\classes\\Test.myclass");

//读取文件1
FileInputStream fis = new FileInputStream(file1);
//读取文件2
FileOutputStream fos = new FileOutputStream(file2);
int code = 0;
//加上1个字符
fos.write(1);
//写入文件2
while ((code = fis.read())!= -1){
fos.write(code);
}
}
}

自定义ClassLoader

只要在读取文件的时候,去掉一个字节即可

package com.test;

import com.sun.xml.internal.ws.util.ByteArrayBuffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader;

/**
* 从*.myclass来加载对象
*/
public class TestClassLoader extends SecureClassLoader {
//文件路径
private String classPath;

//构造方法传入文件路径
public TestClassLoader(String classPath){
this.classPath = classPath;
}

//需要实现从.myclass
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//获取myclass完整文件路径
String filePath = this.classPath + name.replace(".","\\").concat(".myclass");
FileInputStream fis;
ByteArrayBuffer ba = new ByteArrayBuffer();
byte[] b;
int code;

//文件读取
try {
fis =new FileInputStream(new File(filePath));
fis.read();
while ((code = fis.read()) != -1){
ba.write(code);
}
//转换为byte字节
b = ba.toByteArray();
//字节码转化为Class
return defineClass(name,b,0,b.length);
} catch (Exception e) {
throw new ClassNotFoundException("自定义类文件不存在");
}
}
}

LoadClassTest.java

执行我们的自定义加载器没变

package com.test;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class LoadClassTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
TestClassLoader testClassLoader = new TestClassLoader("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\");
Class<?> c = testClassLoader.loadClass("Test");
c.newInstance();
}
}

热加载

当我们对jar包进行修改或者删除,需要对服务器进行重启才能生效,我们就需要实现热加载,更新后立即程序

首先我们看下loadClass是如何加载类的

我们可以看到有另一个loadClass类调用

这也是双亲委派的重要关键代码

从代码看,我们可以得到每次加载类都会保留一次缓存,正是这次缓存导致我们无法实现热加载

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经加载,存储到c变量的缓存中
Class<?> c = findLoadedClass(name);
//判断c是否为空,如果为空则是由父类去加载,否则走到下面
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) {
// //如果仍然没找到则按照顺序来加载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;
}
}

我们就可以将我们的远程代码封装为一个方法,每次调用的时候重新加载一次类,而不是从缓存读取


热加载方法

此处while循环是为了模拟运行时的web

可以看到main方法做了循环new 一个ClassLoader方法来实现热加载,这样就不会使用同一个ClassLoader来加载,如果是同一个ClassLoader则会导致读取缓存


LoadClassTest.java

package com.test;

import java.net.MalformedURLException;

public class LoadClassTest {
//模拟运行的时候的服务器
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException, InterruptedException {
while (true){
Loader();
Thread.sleep(5000);
}
}

private static void Loader() throws ClassNotFoundException, IllegalAccessException, InstantiationException {
TestClassLoader2 testClassLoader2 = null;
testClassLoader2 = new TestClassLoader2("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\classTest.jar");
Class<?> c = testClassLoader2.loadClass("Test1");
c.newInstance();
System.out.println("加载成功");
}
}

TestClassLoader2.java

我们的classloader没变动,新增一个提示重写加载类的打印

package com.test;

import com.sun.xml.internal.ws.util.ByteArrayBuffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.security.SecureClassLoader;

/**
* 实现jar包中找到class文件来加载
*/
public class TestClassLoader2 extends SecureClassLoader {
//用一个jar包来模拟工程
private String jarFile;

public TestClassLoader2(String jarFile) {
this.jarFile = jarFile;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("重新加载类:"+name);
//获取myclass完整文件路径
URL fileURL;
InputStream inputStream;
String classPath = name.replace(".","/").concat(".class");
FileInputStream fis;
ByteArrayBuffer ba = new ByteArrayBuffer();
byte[] b;
int code;

//文件读取
try {
fileURL = new URL("jar:file:\\"+this.jarFile+"!/"+classPath);
inputStream = fileURL.openStream();
while ((code = inputStream.read()) != -1){
ba.write(code);
}
//转换为byte字节
b = ba.toByteArray();
//字节码转化为Class
return defineClass(name,b,0,b.length);
} catch (Exception e) {
// throw new ClassNotFoundException("自定义类文件不存在");
System.out.println("文件正在复制中,下次读取时生效");
return null;
}
}
}

这样我们可以替换掉Test.jar文件来进行热加载


为什么loadClass传入resolve


一个类的类加载过程通常分为加载、连接、初始化三个部分,具体的行为在java虚拟机规范中都有详细的定义,
这里只是大致的说明一下。

  1. 加载Loading:这个过程是Java将字节码数据从不同的数据源读取到VM中,并映射成为JVM认可的数据结构。而如果输入的Class不符合JVM的规范,就会抛出异常。这个阶段是用户可以参与的阶段,我们自定义的类加载Loading:这个过程是Java将字节码数据从不同的数据源读取到VM中,并映射成为JVM认可的数据结构。
    而如果输入的Class不符合JVM的规范,就会抛出异常。这个阶段是用户可以参与的阶段,我们自定义的类加
    载器,就是工作在这个过程。

  2. 连接Linking:这个是核心的步骤,又可以大致分为三个小阶段:

    1. 验证:检查JVM加载的字节信息是否符合Java虚拟机规范,否则就会报错。这一阶段是JVM的安全大门,防止黑客的恶意信息或者不合规信息危害JVM的正常运行。
    2. 准备:这一阶段创建类或接口的静态变量,并给这些静态变量赋一个初始值(不是最终指定的值),这一部分的作用更大的是预分配内存。
    3. 解析:这一步主要是将常量池中的符号引用替换为直接引用。例如我们有个类A调用了类B的方法,这些在代码层次还好只是一些对计算机没有意义的符号引用,在这一阶段就会转换成计算机所能理解的堆栈、引用等这些直接引用。
  3. 初始化Initialization:这一步才是 真正去执行类初始化的代码逻辑。包括执行static静态代码块,给静态变量赋值等

实际上resolve参数就是表示需不需要进行连接阶段,也就是说初始化阶段。从热加载机制另一个很大问题:热加载将一些在编译阶段可以检查出来的问题全都延迟到了运行时,这对整个程序的安全性是个很大的威胁


打破双亲委派

我们在项目中留一个Test.java文件

可以看到会先执行本地的Test类,而不是我们ClassLoader路径下的文件类

结果输出的是AppClassLoader,这也就可以看到我们的双亲委派是委托父类开始先加载,如果父类加载不成,才是子类进行加载

package com.test;

import java.net.MalformedURLException;

public class LoadClassTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
TestClassLoader testClassLoader = new TestClassLoader("E:\\ideaProject\\classTest\\out\\artifacts\\classTest_jar\\");
Class<?> c = testClassLoader.loadClass("Test");
System.out.println(c.getClassLoader()); //AppClassLoader@18b4aac2
c.newInstance();
}
}

我们要如何打破双亲委派机制?

我们可以重写loadClass方法来打破这个机制

package com.test;

import com.sun.xml.internal.ws.util.ByteArrayBuffer;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URL;
import java.security.SecureClassLoader;

/**
* 实现jar包中找到class文件来加载
*/
public class TestClassLoader2 extends SecureClassLoader {
//用一个jar包来模拟工程
private String jarFile;

public TestClassLoader2(String jarFile) {
this.jarFile = jarFile;
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
//判断是否是com.test的包下,如果是则调用当前我们定义的findclass来加载
if (name.startsWith("com.test")){
return this.findClass(name);
}
//不是则直接调用父类的loadclass加载
return super.loadClass(name,resolve);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("重新加载类:"+name);
//获取myclass完整文件路径
URL fileURL;
InputStream inputStream;
String classPath = name.replace(".","/").concat(".class");
FileInputStream fis;
ByteArrayBuffer ba = new ByteArrayBuffer();
byte[] b;
int code;

//文件读取
try {
fileURL = new URL("jar:file:\\"+this.jarFile+"!/"+classPath);
inputStream = fileURL.openStream();
while ((code = inputStream.read()) != -1){
ba.write(code);
}
//转换为byte字节
b = ba.toByteArray();
//字节码转化为Class
return defineClass(name,b,0,b.length);
} catch (Exception e) {
// throw new ClassNotFoundException("自定义类文件不存在");
System.out.println("文件正在复制中,下次读取时生效");
return null;
}
}
}

我们运行看看这次结果

重新加载类:com.test.Test
com.test.TestClassLoader2@7f31245a
上一篇

java代码审计-网校在线系统