写本篇的动因只是一段看起来很诡异的代码, 让我感觉有必要认识一下 ClassLoader
- ----[Counter.java]-------------------------
- public class Counter {
- private static Counter sCounter = new Counter();//<---- tag1
- public static int count = 10;//<---- tag2
- private Counter() {
- count++;
- }
- public static Counter getInstance() {
- return sCounter;
- }
- }
- ----[Client.java]-------------------------
- public class Client {
- public static void main(String[] args) {
- Counter counter = Counter.getInstance();
- System.out.println(counter.count);//10
- }
- }
|-- 当 tag1 和 tag2 换一下位置, 得到的是 11
一, Java 类加载流程
1.Java 虚拟机结构
上一篇讲了 Java 虚拟机, 关于类加载器一笔带过, 本篇详细讲一下
java 文件通过 javac 可以编译成. class 文件, 类加载器就是将. calss 加载到内存里
2. 类加载的流程
关于 Class 实例在堆中还是方法区中? 这里找了一篇文章, 讲得挺深 https://www.cnblogs.com/xy-nb/p/6773051.html
2.1: 加载
将字节码 (二进制流) 载入方法区
堆内存中生成 java.lang.Class 对象, 作为方法区中该类各种数据的操作入口
|-- .class 文件主要来源 --------------------
-- 磁盘中直接加载
-- 网络加载. class 文件
-- 从 zip ,jar 等文件中加载. class 文件
-- 从专有数据库中提取. class 文件
-- 将 Java 源文件动态编译为. class 文件
2.2: 连接 - 验证
验证加载进来的字节流信息是否符合虚拟机规范
[1]. 文件格式验证: 字节流是否符合 class 文件格式规范
[2]. 元数据验证: 是否符合 java 的语言语法的规范
[3]. 字节码验证: 方法体进行校验分析, 保证运行时没危害出现
[4]. 符号引用验证 : 常量池中的各种符号引用信息进行匹配性校验
2.3: 连接 - 准备
为类静态变量分配内存并设置为[对应类型的初始值]
- ----[Counter.java]-------------------------
- public class Counter {
- private static Counter sCounter = new Counter();
- public static int count = 1;
- private Counter() {
- count++;
- }
- public static Counter getInstance() {
- return sCounter;
- }
- }
如上: 在准备阶段 count 的值为 int 的默认值 = 0
2.4: 连接 - 解析
常量池内的符号引用替换为直接引用的过程, 也就是字面量转化为指针.
主要解析: 类, 接口, 字段, 类方法, 接口方法, 方法类型, 方法句柄和调用点限定符引用
2.5 : 初始化
按顺序查找静态变量以及静态代码块对用户自定义类变量的赋值,
- // 现在 count=0, 调用后 new Counter()时 count++, 变为 1
- private static Counter sCounter = new Counter();
- public static int count = 10;// 此时 count 赋值为 10
二, 类被初始化的时机
1. 类被初始化的时机代码测试
1. 创建实例
2. 访问静态变量或者对该静态变量赋值
3. 调用静态方法
4. 反射
5. 初始化一个类的子类
6.JVM 启动时被标明为启动类(main)
---->[Shape 类]------------------
- public class Shape {
- public static String color = "白色";
- static {
- System.out.println("----- 初始化于 Shape-----");
- }
- public static void draw() {
- }
- }
---->[Shape 子类: Rect]------------------
- public class Rect extends Shape {
- public static int radius = 20;
- static {
- System.out.println("----- 初始化于 Rect-----");
- }
- }
- new Shape(); //1. 创建实例
- String color = Shape.color;//2. 访问静态变量
- Shape.color = "黑色";//2. 对该静态变量赋值
- Shape.draw();//3. 调用静态方法
- Class.forName("classloader.Shape");//4. 反射
- Rect.radius = 10;//5. 初始化一个类的子类
2.final 对初始化的影响
|-- 访问编译期静态常量 [不会] 触发初始化
|-- 访问运行期静态常量 [会] 触发初始化
- public class Shape {
- ...
- public static final int weight = 1;
- public static final int height = new Random(10).nextInt();
- ...
- }
- int w = Shape.weight;// 编译期静态常量不会触发初始化
- int h = Shape.height;// 运行期静态常量会触发初始化
|-- 其中 height 在运行时才可以确定值, 访问会触发初始化
3. 初始化的其他小点
|-- 类初始化时并不会初始化它的接口
|-- 子接口初始化不会初始化父接口
|-- 声明类变量时不会初始化
|-- 子类再调用父类的静态方法或属性时, 子类不会被初始化
- Shape shape;// 声明类变量, 不会初始化
- String color = Rect.color;// 只初始化 Shape
- Rect.draw();// 只初始化 Shape
三, 关于类加载器
1. 系统类加载器(应用类加载器)
通过 ClassLoader.getSystemClassLoader()可以获取系统类类加载器
debug 一下, 可以看到系统类加载器: 类名为 AppClassLoader, 所以也称应用类加载器
- ClassLoader loader = ClassLoader.getSystemClassLoader();
- System.out.println(loader);
- Shape shape = new Shape();
- ////sun.misc.Launcher$AppClassLoader@18b4aac2
- ClassLoader loader = shape.getClass().getClassLoader();
- String name = "toly";
- ClassLoader loaderSting = name.getClass().getClassLoader();
- System.out.println(loaderSting);//null
- // 可见 String 的类加载器为 null, 先说一下, 为 null 时由 Bootstrap 类加载器加载
|-- 还有一点想强调一下, 类加载器加载类后, 不会触发类的初始化
- ClassLoader loader = ClassLoader.getSystemClassLoader();
- Class<?> shapeClazz = loader.loadClass("classloader.Shape");// 此时不初始化
- Shape shape = (Shape) shapeClazz.newInstance();// 创建实例时才会初始化
2. 父委托机制(或双亲委托机制)
这里的父并不是指继承, 而是 ClassLoader 类中有一个 parent 属性是 ClassLoader 类型
所以是认干爹, 而不是亲生的. 就像 Android 中的 ViewGroup 和 View 的父子 View 关系
认了干爹之后, 有事先让干爹来摆平, 干爹摆不平, 再自己来, 都摆不平, 就崩了呗.
- ---->[ClassLoader# 成员变量]----------------
- private final ClassLoader parent;
- ---->[ClassLoader# 构造函数一参]----------------
|-- 可以在一参构造函数中传入 parent, 认个干爹, 瞟了一下源码, 貌似是 parent 初始化的唯一途径
- protected ClassLoader(ClassLoader parent) {
- this(checkCreateClassLoader(), parent);
- }
|-- 关于父委托机制 loadClass 方法完美诠释:
- ---->[ClassLoader#loadClass]------------------
- public Class<?> loadClass(String name) throws ClassNotFoundException {
- return loadClass(name, false);
- }
- ---->[ClassLoader#loadClass(String,boolean)]------------------------------
- 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 {// 没有干爹, 让大佬 Bootstrap 类加载器加载
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- }
- if (c == null) {// 干爹和大佬都加载不了
- 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;
- }
- }
3. 三个 JVM 中的类加载器
Bootstrap ClassLoader : 引导类加载器(启动类加载器 / 根类加载器)
|-- C++ 语言实现, 负责加载 jre/lib 路径下的核心类库
- System.out.println(System.getProperty("sun.boot.class.path"));
- //D:\M\JDK1.8\jre\lib\resources.jar;
- // D:\M\JDK1.8\jre\lib\rt.jar;
- // D:\M\JDK1.8\jre\lib\sunrsasign.jar;
- // D:\M\JDK1.8\jre\lib\jsse.jar;
- // D:\M\JDK1.8\jre\lib\jce.jar;
- // D:\M\JDK1.8\jre\lib\charsets.jar;
- // D:\M\JDK1.8\jre\lib\jfr.jar;
- // D:\M\JDK1.8\jre\classes
Launcher$ExtClassLoader : 拓展类加载器
|-- Java 语言实现, 负责加载 jre/lib/ext
- System.out.println(System.getProperty("java.ext.dirs"));
- //D:\M\JDK1.8\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
Launcher$AppClassLoader : 系统类加载器
|-- Java 语言实现, 加载环境变量路径 classpath 或 java.class.path 指定路径下的类库
- String property = System.getProperty("java.class.path");
- //D:\M\JDK1.8\jre\lib\charsets.jar;
- // D:\M\JDK1.8\jre\lib\deploy.jar;
... 略若干 jre 的 jar 路径...
- // J:\FileUnit\file_java\base\out\production\classes; <--- 当前项目的输出路径
- // C:\Program Files\JetBrains\IntelliJ IDEA 2018.1.3\lib\idea_rt.jar
四, 自定义类本地磁盘类加载器
1. 自定义类加载器的干爹
- ---->[ClassLoader# 构造函数]------------------------------------------
- protected ClassLoader(ClassLoader parent) {
- this(checkCreateClassLoader(), parent);
- }
- protected ClassLoader() {
- this(checkCreateClassLoader(), getSystemClassLoader());
- }
这里可以看出无参构造是默认干爹是: getSystemClassLoader, 也就是系统类加载器加载器
当然也可以使用一参构造认干爹
|-- 上面分析: 在 ClassLoader#loadClass 方法中, 当三个 JVM 的类加载器都找不到时
|-- 会调用 findClass 方法来初始化 c , 那我们来看一下 findClass:
---->[在 ClassLoader#findClass]------------------------
- protected Class<?> findClass(String name) throws ClassNotFoundException {
- throw new ClassNotFoundException(name);
- }
就问你一句: 人家直接抛异常, 你敢不覆写吗?
2. 自定义 LocalClassLoader
- /**
- * 作者: 张风捷特烈
- * 时间: 2019/3/7/007:14:05
- * 邮箱: 1981462002@qq.com
- * 说明: 本地磁盘类加载器
- */
- public class LocalClassLoader extends ClassLoader {
- private String path;
- public LocalClassLoader(String path) {
- this.path = path;
- }
- @Override
- protected Class<?> findClass(String name) {
- byte[] data = getBinaryData(name);
- if (data == null) {
- return null;
- }
- return defineClass(name, data, 0, data.length);
- }
- /**
- * 读取字节流
- *
- * @param name 全类名
- * @return 字节码数组
- */
- private byte[] getBinaryData(String name) {
- InputStream is = null;
- byte[] result = null;
- ByteArrayOutputStream baos = null;
- try {
- if (name.contains(".")) {
- String[] split = name.split("\\.");
- name = split[split.length - 1];
- }
- String path = this.path + "\\" + name + ".class";
- File file = new File(path);
- if (!file.exists()) {
- return null;
- }
- is = new FileInputStream(file);
- baos = new ByteArrayOutputStream();
- byte[] buff = new byte[1024];
- int len = 0;
- while ((len = is.read(buff)) != -1) {
- baos.write(buff, 0, len);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (is != null) {
- is.close();
- }
- if (baos != null) {
- result = baos.toByteArray();
- baos.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return result;
- }
- }
3. 测试类的字节码文件
新建一个类 HelloWorld, 有一个公共方法 say, 注意包名和文件夹名
- package com.toly1994.classloader;
- public class HelloWorld {
- public void say() {
- System.out.println("HelloWorld");
- }
- }
4. 使用 LocalClassLoader
使用 LocalClassLoader 加载刚才的字节码文件, 通过反射调用 say 方法, 执行无误
这里要提醒一下: 使用 javac 编译时的 jdk 版本, 要和工程的 jdk 版本一致, 不然会报错
- LocalClassLoader loader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
- try {
- Class<?> clazz = loader.loadClass("com.toly1994.classloader.HelloWorld");;
- Constructor<?> constructor = clazz.getConstructor();
- Object obj = constructor.newInstance();
- Method say = clazz.getMethod("say");
- say.invoke(obj);//HelloWorld
- } catch (NoSuchMethodException | InvocationTargetException e) {
- e.printStackTrace();
- }
|-- 这里可以测试一下 obj 的类加载器
- System.out.println(obj.getClass().getClassLoader());
- //classloader.LocalClassLoader@6b71769e
这样无论. java 文件移到磁盘的哪个位置, 都可以的通过指定路径加载
五, 自定义类网络类加载器
将刚才的 class 文件放到服务器上: http://www.toly1994.com:8089/imgs/HelloWorld.class
然后访问路径来读取字节流, 进行类的加载
1. 自定义 NetClassLoader
核心也就是获取到流, 然后 findClass 中通过 defineClass 生成 Class 对象
- /**
- * 作者: 张风捷特烈
- * 时间: 2019/3/7/007:14:05
- * 邮箱: 1981462002@qq.com
- * 说明: 网络类加载器
- */
- public class NetClassLoader extends ClassLoader {
- private String urlPath;
- public NetClassLoader(String urlPath) {
- this.urlPath = urlPath;
- }
- @Override
- protected Class<?> findClass(String name) {
- byte[] data = getDataFromNet(urlPath);
- if (data == null) {
- return null;
- }
- return defineClass(name, data, 0, data.length);
- }
- private byte[] getDataFromNet(String urlPath) {
- byte[] result = null;
- InputStream is = null;
- ByteArrayOutputStream baos = null;
- try {
- URL url = new URL(urlPath);
- is = url.openStream();
- baos = new ByteArrayOutputStream();
- byte[] buff = new byte[1024];
- int len = 0;
- while ((len = is.read(buff)) != -1) {
- baos.write(buff, 0, len);
- }
- } catch (IOException e) {
- e.printStackTrace();
- } finally {
- try {
- if (is != null) {
- is.close();
- }
- if (baos != null) {
- result = baos.toByteArray();
- baos.close();
- }
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- return result;
- }
- }
2. 使用
使用上基本一致
- NetClassLoader loader = new NetClassLoader("http://www.toly1994.com:8089/imgs/HelloWorld.class");
- try {
- Class<?> clazz = loader.loadClass("com.toly1994.classloader.HelloWorld");
- Constructor<?> constructor = clazz.getConstructor();
- Object obj = constructor.newInstance();
- Method say = clazz.getMethod("say");
- say.invoke(obj);//HelloWorld
- } catch (NoSuchMethodException | InvocationTargetException e) {
- e.printStackTrace();
- }
|-- 这里可以测试一下 obj 的类加载器
- System.out.println(obj.getClass().getClassLoader());
- //classloader.NetClassLoader@66d2e7d9
3. 父委派机制测试
现在网络和本地都可以, 我们让本地的 loader 当做网络加载的父亲
- ---->[NetClassLoader# 添加构造]------------------------
- public NetClassLoader(ClassLoader parent, String urlPath) {
- super(parent);
- this.urlPath = urlPath;
- }
---->[测试类]-----------------------------
- LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
- // 这里讲 NetClassLoader 的干爹设置为 localLoader
- NetClassLoader netLoader = new NetClassLoader(localLoader, "http://www.toly1994.com:8089/imgs/HelloWorld.class");
- try {
- Class<?> clazz = netLoader.loadClass("com.toly1994.classloader.HelloWorld");
- Constructor<?> constructor = clazz.getConstructor();
- Object obj = constructor.newInstance();
- System.out.println(obj.getClass().getClassLoader());
- // 这里打印 classloader.LocalClassLoader@591f989e
- Method say = clazz.getMethod("say");
- say.invoke(obj);//HelloWorld
- } catch (NoSuchMethodException | InvocationTargetException e) {
- e.printStackTrace();
- }
|-- 可以看到, 老爹 LocalClassLoader 能加载, 作为孩子的 NetClassLoader 就没加载
|--- 现在将本地的[删了], 老爹 LocalClassLoader 加载不了, NetClassLoader 自己搞
- System.out.println(obj.getClass().getClassLoader());
- classloader.NetClassLoader@4de8b406
现在应该很明白父委派机制是怎么玩的了吧, 如果 NetClassLoader 也加载不了, 就崩了
六, class 对象的卸载
1. 一个类被 class 被能被 GC 回收 (即: 卸载) 的条件
[1]. 该类所有的实例都已经被 GC.
[2]. 加载该类的 ClassLoader 实例已经被 GC.
[3]. 该类的 java.lang.Class 对象没有在任何地方被引用.
2. 使用自定义加载器时 JVM 中的引用关系
- LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
- Class<?> clazz = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
- Constructor<?> constructor = clazz.getConstructor();
- Object obj = constructor.newInstance();
- System.out.println(obj.getClass().getClassLoader());
- Method say = clazz.getMethod("say");
- say.invoke(obj);//HelloWorld
|-- 使用上面的类加载器再加载一次 com.toly1994.classloader.HelloWorld 可见两个 class 对象一致
- System.out.println(clazz.hashCode());//1265210847
- Class<?> clazz2 = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
- System.out.println(clazz2.hashCode());//1265210847
2. 卸载
- LocalClassLoader localLoader = new LocalClassLoader("G:\\Out\\java\\com\\toly1994\\classloader");
- Class<?> clazz = localLoader.loadClass("com.toly1994.classloader.HelloWorld");
- Constructor<?> constructor = clazz.getConstructor();
- Object obj = constructor.newInstance();
- Method say = clazz.getMethod("say");
- say.invoke(obj);//HelloWorld
- // 清除引用
- obj = null; // 清除该类的实例
- localLoader = null; // 清楚该类的 ClassLoader 引用
- clazz = null; // 清除该 class 对象的引用
后记: 捷文规范
参考文章:
深入理解 Java 类加载器(ClassLoader)
Java --ClassLoader 创建, 加载 class, 卸载 class http://www.cnblogs.com/eoss/p/5992499.html
关于 Class 实例在堆中还是方法区中? https://www.cnblogs.com/xy-nb/p/6773051.html
1. 本文成长记录及勘误表
项目源码 | 日期 | 附录 |
---|---|---|
V0.1-- 无 | 2018-3-7 | 无 |
发布名: JVM 之类加载器 ClassLoader
捷文链接: https://juejin.im/post/5c7a9595f265da2db66df32c
2. 更多关于我
笔名 | 微信 | |
---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 |
我的 GitHub:https://github.com/toly1994328
我的简书: https://www.jianshu.com/u/e4e52c116681
我的简书: https://www.jianshu.com/u/e4e52c116681
个人网站: http://www.toly1994.com
3. 声明
1---- 本文由张风捷特烈原创, 转载请注明
2---- 欢迎广大编程爱好者共同交流
3---- 个人能力有限, 如有不正之处欢迎大家批评指证, 必定虚心改正
4---- 看到这里, 我在此感谢你的喜欢与支持
来源: https://juejin.im/post/5c80e272e51d451538309911