原文链接:Java反序列化基础篇-类加载器
0x01 前言 这篇文章/笔记的话,打算从类加载器,双亲委派到代码块的加载顺序这样来讲。最后才是加载字节码。 0x02 类加载器及双亲委派 说类加载器有些师傅可能没听过,但是说 Java ClassLoader,相信大家耳熟能详。 1. 类加载器有什么用 加载 Class 文件 以这段简单代码为例 Student student = new Student(); 我们知道,Student 本身其实是一个抽象类,是通过 new 这个操作,将其实例化的,类加载器做的便是这个工作。 ClassLoader 的工作如图所示 加载器也分多种加载器,每个加载器负责不同的功能。 主要分为这四种加载器 虚拟机自带的加载器 启动类(根)加载器 扩展类加载器 应用程序加载器 2. 几种加载器 引导类加载器 引导类加载器(BootstrapClassLoader),底层原生代码是 C++ 语言编写,属于 JVM 一部分。 不继承java.lang.ClassLoader类,也没有父加载器,主要负责加载核心 java 库(即 JVM 本身),存储在/jre/lib/rt.jar目录当中。(同时处于安全考虑,BootstrapClassLoader只加载包名为java、javax、sun等开头的类)。 扩展类加载器(ExtensionsClassLoader) 扩展类加载器(ExtensionsClassLoader),由sun.misc.Launcher$ExtClassLoader类实现,用来在/jre/lib/ext或者java.ext.dirs中指明的目录加载 java 的扩展库。Java 虚拟机会提供一个扩展库目录,此加载器在目录里面查找并加载 java 类。 App类加载器(AppClassLoader) App类加载器/系统类加载器(AppClassLoader),由sun.misc.Launcher$AppClassLoader实现,一般通过通过(java.class.path或者Classpath环境变量)来加载 Java 类,也就是我们常说的 classpath 路径。通常我们是使用这个加载类来加载 Java 应用类,可以使用ClassLoader.getSystemClassLoader()来获取它。 3. 双亲委派机制 在 Java 开发当中,双亲委派机制是从安全角度出发的。 我们这里以代码先来感受一下,双亲委派机制确实牛逼。 从报错的角度感受双亲委派机制 尽量别尝试,看看就好了。要不然整个文件夹挺乱的,如果想上手尝试一下的话,我建议是新建一个项目,不要把其他的文件放一起。 新建一个 java.lang的文件夹,在其中新建 String.java的文件。 String.java package java.lang; // 双亲委派的错误代码 public class String { public String toString(){ return "hello"; } public static void main(String[] args) { String s = new String(); s.toString(); } } 看着是不是没有问题,没有错误吧? 我们自己定义了一个java.lang的文件夹,并在文件夹中定义了 String.class,还定义了 String 这个类的 toString 方法。我们跑一下程序。(这里如果把 Stirng 类放到其他文件夹会直接报错,原因也是和下面一样的) 结果居然报错了!而且非常离谱 我这不是已经定义了 main 方法吗??为什么还会报错,这里就提到双亲委派机制了,双亲委派机制是从安全角度出发的。 首先,我们要知道 Java 的类加载器是分很多层的,如图。 我们的类加载器在被调用时,也就是在 new class 的时候,它是以这么一个顺序去找的 BOOT ---> EXC ----> APP 如果 BOOT 当中没有,就去 EXC 里面找,如果 EXC 里面没有,就去 APP 里面找。 所以我们之前报错的程序当中,定义的java.lang.String在 BOOT 当中是有的,所以我们自定义 String 时,会报错,如果要修改的话,是需要去 rt.jar 里面修改的,这里就不展开了。 从正确的角度感受双亲委派机制 前文提到我们新建的java.lang.String报错了,是因为我们定义的 String 和 BOOT 包下面的 String 冲突了,所以才会报错,我们这里定义一个 BOOT 和 EXC 都没有的对象试一试。 在其他的文件夹下,新建 Student.java Student.java package src.DynamicClassLoader; // 双亲委派的正确代码 public class Student { public String toString(){ return "Hello"; } public static void main(String[] args) { Student student = new Student(); System.out.println(student.getClass().getClassLoader()); System.out.println(student.toString()); } } 并把加载器打印出来 我们定义的 Student 类在 APP 加载器中找到了。 0x03 各场景下代码块加载顺序 这里的代码块主要指的是这四种 静态代码块:static{} 构造代码块:{} 无参构造器:ClassName() 有参构造器:ClassName(String name) 场景一、实例化对象 这里有两个文件,分别介绍一下用途: Person.java:一个普普通通的类,里面有静态代码块、构造代码块、无参构造器、有参构造器、静态成员变量、普通成员变量、静态方法。 Main.java:启动类 Person.java package src.DynamicClassLoader; // 存放代码块 public class Person { public static int staticVar; public int instanceVar; static { System.out.println("静态代码块"); } { System.out.println("构造代码块"); } Person(){ System.out.println("无参构造器"); } Person(int instanceVar){ System.out.println("有参构造器"); } public static void staticAction(){ System.out.println("静态方法"); } } Main.java package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) { Person person = new Person(); } } 运行结果如图 结论: 通过new关键字实例化的对象,先调用静态代码块,然后调用构造代码块,最后根据实例化方式不同,调用不同的构造器。 场景二、调用静态方法 直接调用类的静态方法 Person.java 不变,修改 Main.java 启动器即可。 Main.java package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) { Person.staticAction(); } } 结论: 不实例化对象直接调用静态方法,会先调用类中的静态代码块,然后调用静态方法 场景三、对类中的静态成员变量赋值 Main.java package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) { Person.staticVar = 1; } } 结论: 在对静态成员变量赋值前,会调用静态代码块 场景四、使用 class 获取类 package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) { Class c = Person.class; } } // 空屁 结论: 利用class关键字获取类,并不会加载类,也就是什么也不会输出。 场景五、使用 forName 获取类 这里要抛出异常一下。 我们写三种forName的方法调用。 修改 Main.java package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) throws ClassNotFoundException{ Class.forName("src.DynamicClassLoader.Person"); } } // 静态代码块 package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) throws ClassNotFoundException{ Class.forName("src.DynamicClassLoader.Person", true, ClassLoader.getSystemClassLoader()); } } // 静态代码块 package src.DynamicClassLoader; // 代码块的启动器 public class Main { public static void main(String[] args) throws ClassNotFoundException{ Class.forName("src.DynamicClassLoader.Person", false, ClassLoader.getSystemClassLoader()); } } //没有输出 结论: Class.forName(className)和Class.forName(className, true, ClassLoader.getSystemClassLoader())等价,这两个方法都会调用类中的静态代码块,如果将第二个参数设置为false,那么就不会调用静态代码块 场景六、使用 ClassLoader.loadClass() 获取类 Main.java package com.xiinnn.i.test; public class Main { public static void main(String[] args) throws ClassNotFoundException { Class.forName("com.xiinnn.i.test.Person", false, ClassLoader.getSystemClassLoader()); } } //没有输出 结论: ClassLoader.loadClass()方法不会进行类的初始化,当然,如果后面再使用newInstance()进行初始化,那么会和场景一、实例化对象一样的顺序加载对应的代码块。
|