Java类加载机制

类加载机制流程

Java 是⼀个依赖于 JVM(Java虚拟机)实现的跨平台的开发语⾔。Java 程序在运⾏前需要先编译成 class ⽂件 ,Java 类初始化的时候会调⽤java.lang.ClassLoader加载类字节码,ClassLoader 会调⽤ JVM 的 native ⽅法( defineClass0/1/2)来定义⼀个java.lang.Class实例。

JVM 将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize),链接又分为三个步骤,如图所示。

1.装载:查找并加载类的二进制数据2.链接:* 验证:确保被加载类的正确性* 准备:为类的静态变量分配内存,并将其初始化为默认值* 解析:把类中的符号引用转换为直接引用

3.初始化:为类的静态变量赋予正确的初始值java 编译器将.java文件编译成扩展名为.class的文件,.class文件中保存着 java 转换后,虚拟机将要执行的指令。当需要某个类的时候,java 虚拟机会加载.class文件,并创建对应的 class 对象,将 class 文件加载到虚拟机的内存,这个过程被称为类的加载。

类初始化时机

  • 隐式加载:new 创建类的实例* 显式加载:loaderClass、forName等* 访问某个类或接口的静态变量,或者对该静态变量赋值* 调用类的静态方法* 使用反射方式创建某个类或者接口对象的Class对象* 初始化一个类的子类(会首先初始化子类的父类)* JVM启动时标明的启动类,即文件名和类名相同的那个类类加载器


JVM 的类加载是通过 ClassLoader 及其子类来完成的,类的层次关系和加载顺序可以由下图来描述:

1.引导类加载器(Bootstrap ClassLoader)这个类加载器负责将\lib⽬录下的类库加载到虚拟机内存中,⽤来加载 java 的核⼼库,此类加载器并不继承于java.lang.ClassLoader ,不能被java程序直接调⽤,代码是使⽤C++编写的, 是虚拟机⾃⾝的⼀部分2.扩展类加载器(Extendsion ClassLoader)这个类加载器负责加载\lib\ext⽬录下的类库,⽤来加载 java 的扩展库,开发者可以直接使⽤这个类加载器3.应⽤程序类加载器(Application ClassLoader)这个类加载器负责加载⽤⼾类路径(CLASSPATH)下的类库,⼀般我们编写的java类都是由这个类加载器加载,这个类加载器是 CLassLoader 中的getSystemClassLoader()⽅法的返回值,所以也称为系统类加载器,⼀般情况下这就是系统默认的类加载器4.自定义类加载器(Custom ClassLoader)属于应用程序根据自身需要自定义的 ClassLoader,如 tomcat、jboss 都会根据 j2ee 规范自行实现ClassLoader,加载过程中会先检查类是否被已加载,检查顺序是自底向上,从 Custom ClassLoader 到 BootStrap ClassLoader 逐层检查,只要某个 classloader 已加载就视为已加载此类,保证此类在所有 ClassLoader 只加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。ClassLoader 类有如下核心方法:

1.loadClass(加载指定的Java类)

2.findClass(查找指定的Java类)

3.findLoadedClass(查找JVM已经加载过的类)

4.defineClass(定义一个Java类)

5.resolveClass(链接指定的Java类)

类加载器使用顺序

在 JVM 虚拟机中,如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。

也就是说,对于每个类加载器,只有父类(依次递归)找不到时,才自己加载 。这就是双亲委派模型

为什么需要双亲委派模型呢?这可以提高Java的安全性,以及防止程序混乱。

1.提高安全性假设我们使用一个第三方 Jar 包,该 Jar 包中自定义了一个 String 类,它的功能和系统 String 类的功能相同,但是加入了恶意代码。那么,JVM 会加载这个自定义的 String 类,从而在我们所有用到 String 类的地方都会执行该恶意代码。 如果有双亲委派模型,自定义的 String 类是不会被加载的,因为最顶层的类加载器会首先加载系统的 java.lang.String 类,而不会加载自定义的 String 类,防止了恶意代码的注入。2.防止程序混乱假设用户编写了一个 java.lang.String 的同名类,如果每个类加载器都自己加载的话,那么会出现多个 String 类,导致混乱。如果本加载器加载了,父加载器则不加载,那么以哪个加载的为准又不能确定了,也增加了复杂度。类加载方式

Java 类加载方式分为显式和隐式,显式即我们通常使用Java反射或者ClassLoader来动态加载一个类对象,而隐式指的是类名.方法名()或new类实例。显式类加载方式也可以理解为类动态加载,我们可以自定义类加载器去加载任意的类。

常用的类动态加载方式:

// 反射加载HelloWorld示例
Class.forName("com.h4ckfun.HelloWorld");

// ClassLoader加载HelloWorld示例
this.getClass().getClassLoader().loadClass("com.h4ckfun.HelloWorld"); 

Class.forName()是⼀个静态⽅法,最常⽤的是Class.forname(String className);根据传⼊的类的全限定名返回⼀个 Class 对象。该⽅法在将 Class ⽂件加载到内存的同时,会执⾏类的初始化。

注: A a = (A)Class.forName("package.A").newInstance(); 和 A a = new A(); 是⼀样的效果,它们的区别在于创建对象的⽅式不⼀样,前者是使⽤类加载机制,后者是创建⼀个新类。

而ClassLoader.loadClass默认不会初始化类方法。

自定义类加载器

我们先自己编写一个需要被加载的类:

Test.java

package com.h4ckfun;

public class Test {public Test() {System.out.println("Test: " + getClass().getClassLoader());System.out.println("Test Parent: " + getClass().getClassLoader().getParent());}public String hello() {return "Hello World!";}
} 

使用javac编译为 class 文件:

javac Test.java 

使用javap查看反汇编:

javap -c -p -l Test.class 

我们可以自定义类加载器,只需继承 ClassLoader 抽象类,并重写 findClass 方法(如果要打破双亲委派模型,需要重写 loadClass 方法)。

**注意:**一般尽量不要覆写已有的 loadClass 方法中的委派逻辑,一般在 JDK1.2 之前的版本才这样做,而且事实证明,这样做极有可能引起系统默认的类加载器不能正常工作。在 JVM 规范和 JDK 文档中(1.2 或者以后版本中),都没有建议用户覆写 loadClass 方法,相反,明确提示开发者在开发自定义的类加载器时覆写 findClass 逻辑。

具体原因可以看 loadClass 的源码:

 protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> 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 statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;}} 

这个是 ClassLoader 中的 loadClass 方法,大致流程如下:

1.检查类是否已加载,如果是则不用再重新加载了

2.如果未加载,则通过父类加载(依次递归)或者启动类加载器(bootstrap)加载

3.如果还未找到,则调用本加载器的findClass方法

而 ClassLoader 中 findClass 方法源码如下:

 protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);} 

直接返回 ClassNotFoundException,并且注释中明确说明自定义类加载器时需要覆写该方法。

我们自己写一个自定义加载器:

package com.h4ckfun;

import java.io.FileInputStream;
import java.lang.reflect.Method;
import java.util.Arrays;

class HelloWorldClassLoader extends ClassLoader {// Test类名private String testClassName;// Test类字节码private byte[] testClassBytes;@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {try {return defineClass(name, testClassBytes, 0, testClassBytes.length);} catch (Exception e) {e.printStackTrace();throw new ClassNotFoundException();}}private byte[] readByte(String classPath) throws Exception {FileInputStream fis = new FileInputStream(classPath);int len = fis.available();byte[] data = new byte[len];fis.read(data);fis.close();return data;}public static void main(String[] args) {// 创建自定义的类加载器HelloWorldClassLoader loader = new HelloWorldClassLoader();try {loader.testClassName = "com.h4ckfun.Test";loader.testClassBytes = loader.readByte("/tmp/Test.class");System.out.println("class byte: " + Arrays.toString(loader.testClassBytes));// 使用自定义的类加载器加载Test类Class testClass = loader.loadClass(loader.testClassName);// 反射创建Test类,等价于 Test t = new Test();Object testInstance = testClass.newInstance();// 反射获取hello方法Method method = testInstance.getClass().getMethod("hello");// 反射调用hello方法,等价于 String str = t.hello();String str = (String) method.invoke(testInstance);System.out.println(str);} catch (Exception e) {e.printStackTrace();}}
} 

代码中注意重写 findClass 方法,然后进行自定义加载前注意要把之前生成的Test.class移动到 CLASSPATH 之外的目录,这里我移动到了/tmp/Test.class,还有Test.java如果和加载器是同一项目下,记得删除或者重命名为其他文件,比如Test.java.bak,否则 Test 类会被 AppClassLoader 加载(自定义类加载器的 parent 是 AppClassLoader)。

加载结果:

远程类加载

URLClassLoader继承了ClassLoader,URLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。

我们先写一个被远程执行的恶意类:

import java.io.IOException;

public class Cmd {public static Process exec(String cmd) throws IOException {return Runtime.getRuntime().exec(cmd);}
} 

然后使用javac编译为 class 文件,单独打包为 jar 包:

jar cvf Cmd.jar Cmd.class 

将Cmd.java重命名为非java文件后,简单使用 python 启动 web 服务:

python3 -m http.server 9000 

编写URLClassLoader的代码:

package com.h4ckfun;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URLClassLoader;
import java.net.URL;

public class RemoteURLClassLoader {public static void main(String[] args) {try {// 定义远程加载的jar路径URL url = new URL("http://127.0.0.1:9000/Cmd.jar");// 创建URLClassLoader对象,并加载远程jar包URLClassLoader ucl = new URLClassLoader(new URL[]{url});// 定义需要执行的系统命令String cmd = "whoami";// 通过URLClassLoader加载远程jar包中的CMD类Class cmdClass = ucl.loadClass("Cmd");// 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);// 获取命令执行结果的输入流InputStream in = process.getInputStream();ByteArrayOutputStream baos = new ByteArrayOutputStream();byte[]b= new byte[1024];int a= -1;// 读取命令执行结果while ((a = in.read(b)) != -1) {baos.write(b, 0, a);}// 输出命令执行结果System.out.println(baos.toString());} catch (Exception e) {e.printStackTrace();}}
} 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值