深入理解类加载机制

admin 2023年3月12日17:17:53深入理解类加载机制已关闭评论40 views字数 7997阅读26分39秒阅读模式

前言

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口

类加载机制分析

java类的加载方式

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

```
// 反射加载TestHelloWorld示例
Class.forName("com.jvm.Hello");

// ClassLoader加载TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.jvm.Hello");
```

Class.forName("类名")默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器),而ClassLoader.loadClass默认不会初始化类方法。
安全问题

package com.jvm;
import java.io.IOException;
public class run_exec{
  static {
      try {
          Runtime.getRuntime().exec("calc");
      } catch (IOException e) {
          e.printStackTrace();
      }
  }
}

当Class.forName("com.jvm.run_exec")会自动执行static静态方法导致命令执行

类加载流程

Math类

```
package com.jvm;
public class Math {
  public static final int initData = 666;
  public static User user = new User();

public int compute() { //一个方法对应一块栈帧内存区域
      int a = 1;
      int b = 2;
      int c = (a + b) * 10;
      return c;
  }
  public static void main(String[] args) {
      Math math = new Math();
      math.compute();
  }
}
```

Math类加载流程(先大体了解):
c++实现创建Java虚拟机
jvm调用了许多java加载器
类加载器调用加载了Math类

深入理解类加载机制

在这其中对我们最重要的就是loadClass
其中loadClass的类加载过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个****代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值,比如int为0 布尔为false (常量会直接赋值)
  • 解析:将符号引用(例如常量名,类名都是符号)替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接**是在程序运行期间完成的将符号引用替换为直接引用
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块

深入理解类加载机制

Math字节码浅析

所有的类都在out下生成对应位置的字节码文件

深入理解类加载机制

用sublime text打开,其中cafe babe是是字节码的标识

深入理解类加载机制

*我们使用javap -v 对字节码文件进行反编译

深入理解类加载机制

反编译出的内容其实对应的就是字节码文件
首先看常量池 Constant pool

深入理解类加载机制

那例如那我们的math.compute()方法是如何在字节码文件中加载的呢?

深入理解类加载机制

调用main方法中invokevirtual

深入理解类加载机制

我们可以看到他的结果是Method compute:(),首先调用了常量池#4,#4给他返回了这个结果

深入理解类加载机制

但是#4的结果是从哪里来的呢?我们发现他调用了#2.#39

深入理解类加载机制

深入理解类加载机制

2因为调用了#38最终返回了 com/jvm/Math

深入理解类加载机制

深入理解类加载机制

39因为调用了#24:#25最终组成compute:()

最终互相拼接形成完整的com/jvm/Math.compute:()I
总结:类名方法名这种符号,被编译为class字节码时候会被放进静态常量池,一旦被加载这些常量池就会变成运行时常量池,也就是都有内存地址的指向,他们之间会根据内存地址来互相指向
注意:主类在运行过程中如果使用到其它类,会逐步加载这些类。jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

类加载器以及双亲委派机制

我们上面所说的类加载流程主要都是通过类加载器来实现的
类加载器主要分为四类

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
  • 自定义加载器:负责加载用户自定义路径下的类包

public class TestClassLoader {
  public static void main(String[] args) {
      System.out.println(String.class.getClassLoader());
      System.out.println(sun.security.ec.CurveDB.class.getClassLoader());
      System.out.println(TestClassLoader.class.getClassLoader());
  }
}

深入理解类加载机制

第一个由于是String核心类,由引导类加载器加载,因为引导类加载器是C++编写的所以java获取不到
第二个sun.security.ec.CurveDB由于是ext扩展文件下的,所以为ext扩展加载器
第三个是我们自身的加载器,所以调用应用程序加载器
注意:我们可以看到所有的类都是从sun.misc.Launcher类启动的,这是一个很核心的类

深入理解类加载机制

我们进入到launchar类看看

深入理解类加载机制

双亲委派机制

jvm类加载器是有层级的

深入理解类加载机制

委派流程
1.当应用程序加载器加载类时,会查询已加载的类中是否有这个类,如果有则直接加载。如果没有,则委派扩展类加载器进行加载
2.扩展类加载器从已加载的类中查找,如果找到则直接加载,如果没有则委托引导类加载器进行加载
3.引导类加载器从已加载的类中查找,如果找到则直接加载,如果没有则从/jre/lib目录寻找这个类进行加载,如果也没有找到,则委托扩展类加载器进行加载
4.扩展类加载器进行寻找类进行加载,如果找到则进行加载,如果没找到则委托应用程序类加载器进行加载
5.应用程序类加载器进行加载
为什么不直接从引导类加载器往下加载呢?这样只需要一次路程就可以加载到

因为在实际开发过程中90%以上的类在应用程序加载器都能加载的到,这样这种委派模型可以提高效率
在我们的类加载流程里面loadClass就实现了双亲委派机制

深入理解类加载机制

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

实验:
当我们加载如下类会报错显示找不到main方法

package java.lang;
public class String {
  public static void main(String[] args) {
      System.out.println("test");
  }
}

深入理解类加载机制

因为双亲委派机制导致String类从引导类加载器加载,虽然我们写的类有main方法,但是引导类加载器之前已经自动把原本的String加载好了,原本的String类是没有main方法的,导致报错,因此可以保护防止原本的类被篡改
注意双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型,但是JDK中也有较大规模破坏双亲模型的情况

自定义类加载器

前置知识(必看)

若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。**
自定义ClassLoader一般重写findClass方法(父类的ClassLoader的loadClass方法有双亲委托逻辑,一般不重写)
findClass方法:加载指定位置的文件,最终由默认的defindClass转换class字节数组到Class对象

手写class加载器详解

加载的目标类

package com.jvm;
public class User {
  public void hello(){
      System.out.println(66666666);
  }
}

类加载器

package com.jvm;
import java.lang.reflect.Method;
public class TestClassLoader extends ClassLoader {
  // TestHelloWorld类名
  private static String testClassName = "com.jvm.User";
  // TestHelloWorld类字节码
  private static byte[] testClassBytes = new byte[]{
          -54, -2, -70, -66, 0, 0, 0, 52, 0, 27, 10, 0, 6, 0, 14, 9, 0, 15, 0, 16, 3, 3, -7, 64, -86, 10, 0, 17, 0, 18, 7, 0, 19, 7, 0, 20, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 9, 85, 115, 101, 114, 46, 106, 97, 118, 97, 12, 0, 7, 0, 8, 7, 0, 21, 12, 0, 22, 0, 23, 7, 0, 24, 12, 0, 25, 0, 26, 1, 0, 12, 99, 111, 109, 47, 106, 118, 109, 47, 85, 115, 101, 114, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 121, 115, 116, 101, 109, 1, 0, 3, 111, 117, 116, 1, 0, 21, 76, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 59, 1, 0, 19, 106, 97, 118, 97, 47, 105, 111, 47, 80, 114, 105, 110, 116, 83, 116, 114, 101, 97, 109, 1, 0, 7, 112, 114, 105, 110, 116, 108, 110, 1, 0, 4, 40, 73, 41, 86, 0, 33, 0, 5, 0, 6, 0, 0, 0, 0, 0, 2, 0, 1, 0, 7, 0, 8, 0, 1, 0, 9, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 1, 0, 11, 0, 8, 0, 1, 0, 9, 0, 0, 0, 37, 0, 2, 0, 1, 0, 0, 0, 9, -78, 0, 2, 18, 3, -74, 0, 4, -79, 0, 0, 0, 1, 0, 10, 0, 0, 0, 10, 0, 2, 0, 0, 0, 6, 0, 8, 0, 8, 0, 1, 0, 12, 0, 0, 0, 2, 0, 13
  };
  @Override
  public Class<?> findClass(String name) throws ClassNotFoundException {
      // 只处理TestHelloWorld类
      if (name.equals(testClassName)) {
          // 调用JVM的native方法定义TestHelloWorld类
          return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
      }
      return super.findClass(name);
  }
  public static void main(String[] args) {
      // 创建自定义的类加载器
      TestClassLoader loader = new TestClassLoader();
      try {
          // 使用自定义的类加载器加载TestHelloWorld类
          Class testClass = loader.loadClass(testClassName);
          // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
          Object testInstance = testClass.newInstance();
          // 反射获取hello方法
          Method method = testInstance.getClass().getMethod("hello");
          // 反射调用hello方法,等价于 String str = t.hello();
          method.invoke(testInstance);
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}

加载器写法分析(应该是相对网上比较详细的):

1.首先我们要调用类加载器这个类就去要继承ClassLoader类,其实我们自定义的加载器的加载方法都是ClassLoader类帮我们写好的不需要自己写,最重要的就是defineClass方法
用法:defineClass(类名,字节码,起始位置,结束位置)
2.前置知识也说了我们要重写findClass方法,为什么要重写他呢?这就涉及到了双亲委派机制的历史,因为自定义类加载器的方法在JDK1.2之前就出现了,但是双亲委派机制在JDK1.2之后才出现,我们之前说过loadClass方法是双亲委派,如果要重写类加载器必然要重写loadClass方法,这样就破坏了双亲委派机制(当然重写也是允许的但是没必要),于是java引入了findClass()方法(该方法默认是空实现,由用户自己去实现),如果loadClass加载失败,就会自动调用自己的findClass(name)来加载,这样既没有打破loadClass的双亲委派又可以自定义类加载器
所以这里的loadClass方法也可以换为findClass方法,因为加载的class文件使我们要用字节码来加载的,testClassName这个路径他必然找不到这个文件,所以还是会调用我们重写的findClass方法

深入理解类加载机制

3.字节码中保存着原本Java的文件位置,也就是testClassName变量中保存的位置,我们在重写findClass方法里面用equals作比较和defineClass来生成这个类都要用到,所以原本类在哪个位置,这里就先哪不能任意更改,否者就等着报异常吧

深入理解类加载机制

4.关于Class类如何转换为byte类型字节码,我看网上的师傅们没说怎么生成,大多直接就在类加载器中使用了,我自己研究出一个方法,可以供大家参考,如果有更好的办法师傅们可以跟我说说(我菜鸡
我们首先使用javac编译这个文件生成class文件,放到我们的010editor中,点击文件—>导出16进制就会生成这种的16进制字节码

深入理解类加载机制

转换思路:首先两个为一组(这里010editor自动分成2个一组很合心意),将16进制转换为10进制,在将10进制的数转换为byte长度的十进制,byte的长度为8位,也就是-128-127,大于127就减256,不懂这个数学算法的自己百度搜一下溢出学一学,或者顺着我这个图转一下就明白了了

深入理解类加载机制

python脚本附上

```
byte='''CA FE BA BE 00 00 00 34 00 1B 0A 00 06 00 0E 09
00 0F 00 10 03 03 F9 40 AA 0A 00 11 00 12 07 00
13 07 00 14 01 00 06 3C 69 6E 69 74 3E 01 00 03
28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E
65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 05 68
65 6C 6C 6F 01 00 0A 53 6F 75 72 63 65 46 69 6C
65 01 00 09 55 73 65 72 2E 6A 61 76 61 0C 00 07
00 08 07 00 15 0C 00 16 00 17 07 00 18 0C 00 19
00 1A 01 00 0C 63 6F 6D 2F 6A 76 6D 2F 55 73 65
72 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62
6A 65 63 74 01 00 10 6A 61 76 61 2F 6C 61 6E 67
2F 53 79 73 74 65 6D 01 00 03 6F 75 74 01 00 15
4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74
72 65 61 6D 3B 01 00 13 6A 61 76 61 2F 69 6F 2F
50 72 69 6E 74 53 74 72 65 61 6D 01 00 07 70 72
69 6E 74 6C 6E 01 00 04 28 49 29 56 00 21 00 05
00 06 00 00 00 00 00 02 00 01 00 07 00 08 00 01
00 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7
00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00
00 00 03 00 01 00 0B 00 08 00 01 00 09 00 00 00
25 00 02 00 01 00 00 00 09 B2 00 02 12 03 B6 00
04 B1 00 00 00 01 00 0A 00 00 00 0A 00 02 00 00
00 06 00 08 00 08 00 01 00 0C 00 00 00 02 00 0D

'''
list1=[]
byte=byte.split()
for i in byte:
  dec=int(i,16)
  if(dec>127):
      list1.append(dec-256)
  else:
      list1.append(dec)
print(list1)
```

深入理解类加载机制

复制我们生成的字节码粘贴到类加载器即可

5.loadclass生成类之后就是反射的事情了

深入理解类加载机制

成功调用

深入理解类加载机制

我们也可以把方法改为命令执行Runtime等

总结

这篇文章从最开始的类加载到双亲委派机制到最后的手动写类加载器,写的非常详细,希望帮到大家

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月12日17:17:53
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   深入理解类加载机制https://cn-sec.com/archives/1599511.html