类加载机制(硬核详解)


之前学JAVA高并发的时候有一定的了解过类加载机制。
在这里插入图片描述
那么加载到方法区里面去的呢?

一、类生命周期

我们得从类生命周期开始说起,类的生命周期总共分七歩,接下来我们先了解下从加载到初始化。如下图
在这里插入图片描述

二、类加载器

在java中其实专门有一个东西负责装入类,它叫做类加载器

类加载器负责装入类,搜索网络、jar、zip文件夹、二进制数据、内存类等指定位置的类资源。

一个java程序运行,至少需要三个类加载器实例,负责加载不同的类的加载,那怎么去理解至少需要三个类加载器实例呢?

这个我觉得也很好理解,比如我们后端程序员不可能让我们去做行政,程序员鼓励师不能去敲代码,每个人的工作分工不同,要各司其职,这样的话才能更好的去维护,更好的去管理,在java中也是一样的。如下图

在这里插入图片描述

  1. Bootstrap loader核心类库加载器

    核心类库加载器是由C语言写的,什么叫核心类库呢?如我们的Object它就是一个类,它就是用C语言写的,为什么要用C语
    言写呢?因为java虚拟机最开始是由C语言实现的,先有了C然后再有了java,少了它就无法运行的类就叫核心类库,对应
    JRE_HOME/jre/lib目录

  2. Extension Class loader扩展类库加载器

    扩展类库加载器在新的版本中有一些调整,这个先放一边,毕竟也不是很大的调整,它是专门加载JRE_HOME/jre/lib/ext
    目录下的扩展类库的,什么叫扩展类库呢?就是说这个类可能在某些平台上没有的类,就是说不是缺它不可的类,比如
    我们谈恋爱,没有女朋友不行,但是发现真的少了女朋友照样过得很潇洒,就是这么一回事。

  3. Appliction class loader用户应用程序加载器

    应用程序加载器是为了加载我们开发人员写的代码,那它是怎么知道我们写的代码在哪些位置呢?有一个java.class.path
    参数,用户应用程序加载器通过加载这个参数,就能得到对应的目录

三、验证问题

1、 如何查看类对应的加载器?

我们可以通过JDK-API查看:java.lang.Class.getClassloader()返回类的类加载器;如果这个类是BootstrapLoader
加载的,那么这个方法在这种实现中就会返回null。
demo
/**
 * 查看类的加载器实例
 */
public class ClassLoaderView {
    public static void main(String[] args) throws Exception {
        // 加载核心类库的 BootStrap ClassLoader
        System.out.println(" 核心类库加载器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.String").getClassLoader());
        System.out.println(" 核心类库加载器:"
                + ClassLoaderView.class.getClassLoader().loadClass("java.lang.Object").getClassLoader());

        
        // 加载拓展库的 Extension ClassLoader
        System.out.println("拓展类库加载器:" + ClassLoaderView.class.getClassLoader()
                .loadClass("com.sun.nio.zipfs.ZipCoder").getClassLoader());
        // 加载应用程序的
        System.out.println("应用程序库加载器:" + ClassLoaderView.class.getClassLoader());


        // 双亲委派模型 Parents Delegation Model
        System.out.println("应用程序库加载器的父类:" + ClassLoaderView.class.getClassLoader().getParent());
        System.out.println(
                "应用程序库加载器的父类的父类:" + ClassLoaderView.class.getClassLoader().getParent().getParent());
    }
}

运行结果

核心类库加载器:null
核心类库加载器:null
拓展类库加载器:sun.misc.Launcher$ExtClassLoader@3e3abc88
应用程序库加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
应用程序库加载器的父类:sun.misc.Launcher$ExtClassLoader@3e3abc88
应用程序库加载器的父类的父类:null

2、JVM如何知道我们的类在何方?

查找AppClassLoader可以看到如下代码段,读取了java.classs.path参数,所以发现jvm还是比较傻的。

在这里插入图片描述

还可以利用jps、jcmd两个命令进行验证.
一、使用jmcd命令进行验证,运行如下代码
package classloader_demo1;

import java.io.IOException;

public class Main {

    public static void main(String[] args) throws IOException {
        System.out.println("Hello World!");
        System.in.read();//这段代码会阻塞
    }
}

打开命名窗口
在这里插入图片描述
然后运行jcmd help命令
在这里插入图片描述
找到对应main函数的ID运行:jcmd 17389 help看看支持哪些命令
在这里插入图片描述

可以看到有非常多的命令,可以看到有很多命令,GC.heap_dump可以查看堆信息、VM.flags可以看到jvm的参数配置等等…,这次我们就先看看jvm系统参数的配置,运行jcmd 17389 VM.system_propertites命令
在这里插入图片描述

如下图可以看到自己类的信息有这么多,那么为什么有这么多呢,其实是idea开发工具帮我们做了了很多的配置,有了这些配置信息jvm就能找到我们程序编译之后的目录,进而运行我们的代码。
在这里插入图片描述

3、类会不会重复加载?

那么类会不会重复加载呢?答案肯定是否定的。

类不会重复加载

类的唯一性:同一个加载器,类名一样,代表是同一个类。
识别方式:Classloader Insance id + PackgeName + ClassName
验证方式:使用类加载器,对同一个class类的不同版本,进行多次加载,检查是否会加载到最新的代码。


①、新建HelloService类加载测试类
/** 类加载测试类 */
public class HelloService {

    public static String value = getValue();

    static {
        System.out.println("静态代码块运行");
    }

    private static String getValue() {
        System.out.println("静态方法被运行");
        return "netease";
    }

    public void test() {
        System.out.println("hello.." + value);
    }
}

②、新建LoaderTest类
package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 进行加载
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找类的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            if(loader == null){
                break;
            }
            // 问题:静态块什么时候触发?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            //反射创建对象
            Object newInstance = clazz.newInstance();
            //调用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);

            Thread.sleep(3000L);
            System.out.println();

        }
    }
}


运行结果

HelloService所使用的类加载器:java.net.URLClassLoader@1c20c684
静态方法被运行
静态代码块运行
hello..netease
调用getValue获得的返回值为:null

HelloService所使用的类加载器:java.net.URLClassLoader@1218025c
静态方法被运行
静态代码块运行
hello..netease
调用getValue获得的返回值为:null

代码是一直循环创建类加载器创建对象的,接下来我们修改HelloService中的代码,加了几个1
在这里插入图片描述
重新编译

HelloService所使用的类加载器:java.net.URLClassLoader@1218025c
静态方法被运行
静态代码块运行
hello..netease
调用getValue获得的返回值为:null

运行结果还是一样,进一步的证明了类不会重复加载

4、类如何卸载?

类被卸载需要满足两个条件
①:该Class所有的实例都已经被GC销毁;
②:加载该类的所有ClassLoader实例都已经被GC;
验证方式:jvm启动参数中增加-verbose:class参数,输出加载和卸载的日志信息,同手手动的触发GC的操作

下面开始进行验证:

首先给我们运行的类加上-verbose:class参数,加上字后就能看到日志信息了

在这里插入图片描述
修改LoaderTest代码

package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 进行加载
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找类的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

        while (true) {
            //  通过URLClassLoader创建一个新的类加载器
            if (loader == null) {
                break;
            }
            // 问题:静态块什么时候触发?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            //反射创建对象
            Object newInstance = clazz.newInstance();
            //调用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);

            Thread.sleep(3000L);
            System.out.println();

            //对象置为空方便GC直接回收
            newInstance = null;
            //类加载器也置为空
            loader = null;
        }

        //手动触发GC,此方法不一定有用,但能让jvm更主动的去做一次GC
        System.gc();
        Thread.sleep(10000L);
    }
}

可以看到信息已经打印出来了,说明Class所有的实例和该类的所有ClassLoader实例都已经被GC;
在这里插入图片描述

5、双亲委派模型是什么?

首先要了解什么是双亲委派模型?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T2sCcYz6-1585758473101)(https://note.youdao.com/src/930E982EF7264D36B222FD7ABE89B52E)]

为了避免重复加载,由下到上逐级委托,由上倒下逐级查找

首先不会自己去尝试加载类,而是把这个请求,委派给父加载器去完成;每一个层次的加载器都是如此,因此所有的类加载请求都会传给上层的启动类加载。
只有当父加载器反馈自己无法完成该加载的请求时(该加载器的搜索范围中没有找到对应的类),子加载器才会尝试自己去加载。

==注:类加载器之间不存在父类子类的关系,“双亲”是翻译,可以理解为逻辑定义的上下级关系。==

下面用代码具体去体现

首先看一个问题,如果我把类加载器的创建放到while循环中去,每次都会创建新的类加载器,那么能不能实现类的动态加载呢?

package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 进行加载
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找类的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        while (true) {
            //每次都新建一个类加载器
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl});

            //  通过URLClassLoader创建一个新的类加载器
            if (loader == null) {
                break;
            }
            // 问题:静态块什么时候触发?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            //反射创建对象
            Object newInstance = clazz.newInstance();
            //调用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);

            Thread.sleep(3000L);
            System.out.println();

            //对象置为空方便GC直接回收
            newInstance = null;
            //类加载器也置为空
            loader = null;
        }

        //手动触发GC,此方法不一定有用,但能让jvm更主动的去做一次GC
        System.gc();
        Thread.sleep(10000L);
    }
}

HelloService所使用的类加载器:java.net.URLClassLoader@119d7047
静态方法被运行
静态代码块运行
hello..4444netease
调用getValue获得的返回值为:null

[Loaded HelloService from file:/ideaWorkspace/]
HelloService所使用的类加载器:java.net.URLClassLoader@3b07d329
静态方法被运行
静态代码块运行
hello..5555netease
调用getValue获得的返回值为:null

可以看到编译之后,类确实被重新加载了,eclipse/idea的热部署和jsp页面就是通过不同的类加载器实现的。

敲重点!!!下面思考一个问题,如果我新创建一个parentLoader,然后传入while循环中的类加载器,还能不能实现动态加载?

package classloader_demo1;

import java.net.URL;
import java.net.URLClassLoader;

/**
 * 指定class 进行加载
 */
public class LoaderTest {
    public static void main(String[] args) throws Exception {
        //指定jvm查找类的位置
        URL classUrl = new URL("file:///ideaWorkspace/");

        URLClassLoader parentLoader = new URLClassLoader(new URL[]{classUrl});
        while (true) {
            //每次都新建一个类加载器
            URLClassLoader loader = new URLClassLoader(new URL[]{classUrl},parentLoader);

            //  通过URLClassLoader创建一个新的类加载器
            if (loader == null) {
                break;
            }
            // 问题:静态块什么时候触发?
            Class clazz = loader.loadClass("HelloService");
            System.out.println("HelloService所使用的类加载器:" + clazz.getClassLoader());

            //反射创建对象
            Object newInstance = clazz.newInstance();
            //调用test方法
            Object value = clazz.getMethod("test").invoke(newInstance);
            System.out.println("调用getValue获得的返回值为:" + value);

            Thread.sleep(3000L);
            System.out.println();

            //对象置为空方便GC直接回收
            newInstance = null;
            //类加载器也置为空
            loader = null;
        }

        //手动触发GC,此方法不一定有用,但能让jvm更主动的去做一次GC
        System.gc();
        Thread.sleep(10000L);
    }
}

答案是否定的,其实这里传入parentLoader就是使用了委派双亲模式,虽然每次都新创建了一个类加载器,但是lodaer不会自己去查找类,而是会把查找交给parentLoader去查找,所以每次使用的还是parentLoader类加载器,也就不会动态的加载HelloService类了。
在这里插入图片描述

如果我把parentLoader删掉,其实他指定的父级加载器就会是ExtClassLoader/AppclassLoader,希望大家能够理解,有问题的话可以随时私信我进行沟通。

四、总结:

今天我们学习了整个类加载器的相关知识,学习了类的生命周期、加载顺序,学会了
1、如何查看类加载器
2、JVM如何知道我们的类在何方?
3、类会不会重复加载?
4、类如何卸载条件是什么?
5、双亲委派模型是什么?
最后希望大家能够多多交流,一起学习一起进步!
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值