JVM篇2:[-加载器ClassLoader-]

2,973 阅读8分钟

写本篇的动因只是一段看起来很诡异的代码,让我感觉有必要认识一下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加载到内存里

虚拟机的内部体系结构.png


2.类加载的流程

关于Class实例在堆中还是方法区中?这里找了一篇文章,讲得挺深

class字节码的加载.png


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,所以也称应用类加载器

debug.png

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;
    }
}

Java类加载流程.png


3.三个JVM中的类加载器

JVM内置类加载器.png

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,注意包名和文件夹名

测试类.png

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文件放到服务器上:www.toly1994.com:8089/imgs/HelloW…
然后访问路径来读取字节流,进行类的加载

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中的引用关系

自定义加载器的引用关系.png

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
关于Class实例在堆中还是方法区中?


1.本文成长记录及勘误表
项目源码 日期 附录
V0.1--无 2018-3-7

发布名:JVM之类加载器ClassLoader
捷文链接:juejin.cn/post/684490…

2.更多关于我
笔名 QQ 微信
张风捷特烈 1981462002 zdl1994328

我的github:github.com/toly1994328
我的简书:www.jianshu.com/u/e4e52c116…
我的简书:www.jianshu.com/u/e4e52c116…
个人网站:www.toly1994.com

3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持

icon_wx_200.png