前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM篇2:[-加载器ClassLoader-]

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

作者头像
张风捷特烈
发布2024-02-11 08:32:24
1300
发布2024-02-11 08:32:24
举报
文章被收录于专栏:Android知识点总结

写本篇的动因只是一段看起来很诡异的代码,让我感觉有必要认识一下ClassLoader

代码语言:javascript
复制
----[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
虚拟机的内部体系结构.png
2.类加载的流程

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

class字节码的加载.png
class字节码的加载.png

2.1:加载
代码语言:javascript
复制
将字节码(二进制流)载入方法区
堆内存中生成java.lang.Class对象,作为方法区中该类各种数据的操作入口

|-- .class文件主要来源--------------------
    -– 磁盘中直接加载
    -– 网络加载.class文件
    -– 从zip ,jar 等文件中加载.class 文件
    -– 从专有数据库中提取.class文件
    -– 将Java源文件动态编译为.class文件

2.2:连接 - 验证

验证加载进来的字节流信息是否符合虚拟机规范

代码语言:javascript
复制
[1].文件格式验证: 字节流是否符合class文件格式规范
[2].元数据验证: 是否符合java的语言语法的规范
[3].字节码验证:方法体进行校验分析,保证运行时没危害出现
[4].符号引用验证 :常量池中的各种符号引用信息进行匹配性校验

2.3:连接 - 准备

为类静态变量分配内存并设置为[对应类型的初始值]

代码语言:javascript
复制
----[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 : 初始化

按顺序查找静态变量以及静态代码块对用户自定义类变量的赋值,

代码语言:javascript
复制
//现在count=0,调用后new Counter()时count++,变为1
private static Counter sCounter = new Counter();
public static int count = 10;// 此时count赋值为10 

二、类被初始化的时机
1.类被初始化的时机代码测试
代码语言:javascript
复制
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对初始化的影响
代码语言:javascript
复制
|-- 访问编译期静态常量[不会]触发初始化
|-- 访问运行期静态常量[会]触发初始化

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.初始化的其他小点
代码语言:javascript
复制
|-- 类初始化时并不会初始化它的接口
|-- 子接口初始化不会初始化父接口
|-- 声明类变量时不会初始化
|-- 子类再调用父类的静态方法或属性时,子类不会被初始化

Shape shape;//声明类变量,不会初始化
String color = Rect.color;//只初始化Shape
Rect.draw();//只初始化Shape

三、关于类加载器
1.系统类加载器(应用类加载器)

通过ClassLoader.getSystemClassLoader()可以获取系统类类加载器 debug一下,可以看到系统类加载器:类名为AppClassLoader,所以也称应用类加载器

debug.png
debug.png
代码语言:javascript
复制
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关系 认了干爹之后,有事先让干爹来摆平,干爹摆不平,再自己来,都摆不平,就崩了呗。

代码语言:javascript
复制
---->[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
Java类加载流程.png

3.三个JVM中的类加载器
JVM内置类加载器.png
JVM内置类加载器.png
代码语言:javascript
复制
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.自定义类加载器的干爹
代码语言:javascript
复制
---->[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
代码语言:javascript
复制
/**
 * 作者:张风捷特烈
 * 时间: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
测试类.png
代码语言:javascript
复制
package com.toly1994.classloader;
public class HelloWorld {
    public void say() {
        System.out.println("HelloWorld");
    }
}

4.使用LocalClassLoader

使用LocalClassLoader加载刚才的字节码文件,通过反射调用say方法,执行无误 这里要提醒一下:使用javac编译时的jdk版本,要和工程的jdk版本一致,不然会报错

代码语言:javascript
复制
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对象

代码语言:javascript
复制
/**
 * 作者:张风捷特烈
 * 时间: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.使用

使用上基本一致

代码语言:javascript
复制
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当做网络加载的父亲

代码语言:javascript
复制
---->[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回收(即:卸载)的条件
代码语言:javascript
复制
[1].该类所有的实例都已经被GC。
[2].加载该类的ClassLoader实例已经被GC。
[3].该类的java.lang.Class对象没有在任何地方被引用。

2.使用自定义加载器时JVM中的引用关系
自定义加载器的引用关系.png
自定义加载器的引用关系.png
代码语言:javascript
复制
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.卸载
代码语言:javascript
复制
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.更多关于我

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

3.声明

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-02-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Java类加载流程
    • 1.Java虚拟机结构
      • 2.类加载的流程
        • 2.1:加载
        • 2.2:连接 - 验证
        • 2.3:连接 - 准备
        • 2.4:连接 - 解析
        • 2.5 : 初始化
    • 二、类被初始化的时机
      • 1.类被初始化的时机代码测试
        • 2.final对初始化的影响
          • 3.初始化的其他小点
          • 三、关于类加载器
            • 1.系统类加载器(应用类加载器)
              • 2.父委托机制(或双亲委托机制)
                • 3.三个JVM中的类加载器
                • 四、自定义类本地磁盘类加载器
                  • 1.自定义类加载器的干爹
                    • 2.自定义LocalClassLoader
                      • 3.测试类的字节码文件
                        • 4.使用LocalClassLoader
                        • 五、自定义类网络类加载器
                          • 1.自定义NetClassLoader
                            • 2.使用
                              • 3.父委派机制测试
                              • 六、class对象的卸载
                                • 1.一个类被class被能被GC回收(即:卸载)的条件
                                  • 2.使用自定义加载器时JVM中的引用关系
                                    • 2.卸载
                                    • 后记:捷文规范
                                      • 参考文章:
                                        • 1.本文成长记录及勘误表
                                          • 2.更多关于我
                                            • 3.声明
                                            相关产品与服务
                                            腾讯云服务器利旧
                                            云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                                            领券
                                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档