JVM之类加载器ClassLoader

栏目: Java · 发布时间: 7年前

内容简介:写本篇的动因只是一段看起来很诡异的代码,让我感觉有必要认识一下ClassLoader上一篇讲了Java虚拟机,关于类加载器一笔带过,本篇详细讲一下java文件通过javac可以编译成.class文件,类加载器就是将.calss加载到内存里

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

JVM之类加载器ClassLoader

2.类加载的流程

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

JVM之类加载器ClassLoader

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

JVM之类加载器ClassLoader
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;
    }
}

复制代码
JVM之类加载器ClassLoader

3.三个JVM中的类加载器

JVM之类加载器ClassLoader
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,注意包名和文件夹名

JVM之类加载器ClassLoader
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中的引用关系

JVM之类加载器ClassLoader
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对象的引用
复制代码

后记:捷文规范

参考文章:

1.本文成长记录及勘误表

项目源码 日期 附录
V0.1--无 2018-3-7

发布名: JVM之类加载器ClassLoader
捷文链接:https://juejin.im/post/5c7a9595f265da2db66df32c

2.更多关于我

笔名 QQ 微信
张风捷特烈 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----看到这里,我在此感谢你的喜欢与支持

JVM之类加载器ClassLoader

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

GWT in Action

GWT in Action

Robert Hanson、Adam Tacy / Manning Publications / 2007-06-05 / USD 49.99

This book will show Java developers how to use the Google Web Toolkit (GWT) to rapidly create rich web-based applications using their existing skills. It will cover the full development cycle, from ......一起来看看 《GWT in Action》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具