个人随笔
目录
JVM(五)、浅析JVM类加载过程
2021-02-28 15:00:13

JVM的类加载会经过如下图几个过程:加载、验证、准备、解析、初始化、使用、卸载

加载过程

加载

从硬盘、网络、数据库等读取java类字节码字节流的过程。

验证

校验字节码文件格式的正确性,当然如果可以保证字节码文件一定正确的话,可以使用-Xverfity:none来关闭该过程

准备

给类的静态变量分配内存并进行初始化操作,比如整型会初始化为0,对象会初始化为null

解析

将符号引用替换为直接引用

初始化

对类的静态变量初始化为指定的值,执行静态代码块.

双亲委派机制

几种类加载器

上面的类加载过程主要是通过类加载器来实现的,Java里有如下几种类加载器:

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

如下代码,输出相应的类加载器

  1. public class TestJDKClassLoader {
  2. public static void main(String[] args) {
  3. //启动类加载器是C++语言实现的吗,所以输出的是null
  4. System.out.println(String.class.getClassLoader());
  5. System.out.println(sun.text.resources.cldr.aa.FormatData_aa.class.getClassLoader().getClass().getName());
  6. System.out.println(TestJDKClassLoader.class.getClassLoader());
  7. System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
  8. }
  9. }

运行结果如下

  1. null
  2. sun.misc.Launcher$ExtClassLoader
  3. sun.misc.Launcher$AppClassLoader@c387f44
  4. sun.misc.Launcher$AppClassLoader

启动类加载器是C++语言实现的吗,所以输出的是null

自定义类加载器

自己定义类加载器呢?这主要有两种方式

(1)遵守双亲委派模型:继承ClassLoader,重写findClass()方法。
(2)破坏双亲委派模型:继承ClassLoader,重写loadClass()方法。 通常我们推荐采用第一种方法自定义类加载器,最大程度上的遵守双亲委派模型。

  1. public class MyClassLoaderTest extends ClassLoader{
  2. private String classPath;
  3. public MyClassLoaderTest(String classPath) {
  4. this.classPath = classPath;
  5. }
  6. private byte[] loadByte(String name)throws Exception{
  7. name = name.replaceAll("\\.", "/");
  8. FileInputStream fis = new FileInputStream(classPath+"/"+name+".class");
  9. int len = fis.available();
  10. byte[] data = new byte[len];
  11. fis.read(data);
  12. fis.close();
  13. return data;
  14. }
  15. //不破坏双亲委托机制,则只需要重写findClass即可
  16. @Override
  17. protected Class<?> findClass(String name) throws ClassNotFoundException {
  18. try {
  19. byte[] data = loadByte(name);
  20. return defineClass(name,data, 0, data.length);
  21. } catch (Exception e) {
  22. // TODO Auto-generated catch block
  23. e.printStackTrace();
  24. throw new ClassNotFoundException();
  25. }
  26. }
  27. //打破了双亲委派机制
  28. protected Class<?> loadClass(String name, boolean resolve)
  29. throws ClassNotFoundException
  30. {
  31. synchronized (getClassLoadingLock(name)) {
  32. // First, check if the class has already been loaded
  33. Class<?> c = findLoadedClass(name);
  34. if (c == null) {
  35. long t0 = System.nanoTime();
  36. if (c == null) {
  37. // If still not found, then invoke findClass in order
  38. // to find the class.
  39. long t1 = System.nanoTime();
  40. c = findClass(name);
  41. // this is the defining class loader; record the stats
  42. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  43. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  44. sun.misc.PerfCounter.getFindClasses().increment();
  45. }
  46. }
  47. if (resolve) {
  48. resolveClass(c);
  49. }
  50. return c;
  51. }
  52. }
  53. public static void main(String[] args) throws Exception{
  54. MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/Test");
  55. Class clazz = classLoader.loadClass("com.suibibk.jvm.User");
  56. System.out.println(clazz.getClassLoader().getClass().getName());
  57. }
  58. }

我们先把loadClass方法注释,然后执行代码发现输出结果如下:

  1. sun.misc.Launcher$AppClassLoader

为什么不是打印自定义加载器呢,首先让我们看一下双亲委派机制的概念:当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

我们上面是注释了loadClass方法来测试的,所以是用的第一种遵守双亲委派模型,然后看一下默认的loadClass方法。

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // If still not found, then invoke findClass in order
  21. // to find the class.
  22. long t1 = System.nanoTime();
  23. c = findClass(name);
  24. // this is the defining class loader; record the stats
  25. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  26. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  27. sun.misc.PerfCounter.getFindClasses().increment();
  28. }
  29. }
  30. if (resolve) {
  31. resolveClass(c);
  32. }
  33. return c;
  34. }
  35. }

可以很清楚的看到逻辑,先判断parent是否存在,很明显此时我们的parent:AppClassLoader是存在的,并且我们的classPath目录下也有User.class,所以就直接加载了。所以我们应该要把classpath目录下的User.class删除,然后添加到D:/Test目录下,然后就可以正常输出了。

打破双亲委派机制,加载String

我们把loadClass方法放开,然后main方法执行下面逻辑

  1. MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/Test");
  2. Class clazz = classLoader.loadClass("java.lang.String");
  3. System.out.println(clazz.getClassLoader().getClass().getName());

我们在Test目录把String.class字节码加上,结果会怎么样呢,理论上应该是可以加载的把!

结果如下

  1. java.lang.SecurityException: Prohibited package name: java.lang
  2. at java.lang.ClassLoader.preDefineClass(ClassLoader.java:659)

因为java虚拟机堆相关目录有保护作用,禁止加载 Prohibited package name: java.lang。

打破双亲委派机制,加载自定义类

我们把loadClass方法放开,然后main方法执行下面逻辑

  1. MyClassLoaderTest classLoader = new MyClassLoaderTest("D:/Test");
  2. Class clazz = classLoader.loadClass("com.suibibk.jvm.User");
  3. System.out.println(clazz.getClassLoader());

我们把Test目录把User.class字节码加上,结果会怎么样呢,这次理论上应该是可以加载的把,毕竟跟上面不同,已经是自定义的包了,运行结果如下:

  1. java.io.FileNotFoundException: D:\Test\java\lang\Object.class (系统找不到指定的文件。)
  2. at java.io.FileInputStream.open0(Native Method)
  3. at java.io.FileInputStream.open(FileInputStream.java:195)
  4. at java.io.FileInputStream.<init>(FileInputStream.java:138)
  5. at java.io.FileInputStream.<init>(FileInputStream.java:93)

因为所有自定义类的父类默认都是Object,但是Test目录下没有Object.class,所以也不能加载,打破双亲委派机制还是蛮麻烦的,所以可以在loadClass方法做一些过滤
有些类用父类加载,有些类打破委派。比如在loadClass加上如下代码:

  1. ...
  2. if (c == null) {
  3. if("java.lang.Object".equals(name)) {
  4. System.out.println("111");
  5. c = super.loadClass(name, false);
  6. }
  7. ...

那就可以正常执行啦。

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

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

好了具体可以参考:JVM:Java类加载机制

 261

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2