JAVA类加载机制

运行时数据区

方法区是JVM用来存储加载类的信息、常量、静态变量、编译后的代码等数据虚拟机规范中这是一个逻辑区域。具体实现根据不同来实现。如:oracle的HotSpot在java7中方法区放在永久代,java8放在元数据空间,并且通过GC机制对这个区域进行管理.

一个类加载后的信息最终是放在了方法区里面,但是这个过程究竟是由谁来做的呢?要想知道class是怎么加载的得从类的生命周期开始说起

类生命周期

类加载器

类加载器负责装入类,搜索网络、jar、zip、文件夹、二进制数据、内存等指定位置的类资源。一个java程序运行,最少有三个类加载器实例,负责不同类的加载.

核心类库加载器(Bootstrap Loader)

Bootstrap Loader 是C++实现,无对应java类.加载JRE_HOME/jre/lib目录或用户配置的目录.

JDK核心类库rt.jar.比如String、Object这些jdk提供的类就是由这个加载器去加载

扩展类库加载器(Extension class Loader)

ExtClassLoader的实例,加载JRE_HOME/jre/lib/ext目录,JDK扩展包或用户配置的目录.专门加载ext下面的扩展类库

应用程序加载器(application class Loader)

appClassLoaderr的实例,加载java.class.path指定的目录,用户应用程序class-path 或者java命令运行时参数-cp…。专门负责加载用户写的应用

演示

import com.sun.nio.zipfs.ZipInfo;

public class Demo {

    public static void main(String[] args) {
        // 核心类库加载器是C语言写的 所以是null
        System.out.println("核心类库加载器:" + Object.class.getClassLoader());
        // 扩展类加载器
        System.out.println("扩展类库加载器:" + ZipInfo.class.getClassLoader());
        // 应用程序加载器
        System.out.println("应用程序加载器:" + Demo.class.getClassLoader());
    }
}

通过这个演示,我们可以知道,java中的每一个类都是由一个专门的类加载器去加载的

JVM怎么知道类在哪里

结论

class信息存放在不同的位置如:桌面jar、项目bin目录、target目录等等,那么jvm是怎么知道去加载的呢?

查看jdk源代码:sun.misc.Launcher.AppClassLoader

JVM通过读取java.class.path的配置,指定去哪些地址加载类资源

验证过程

相关命令

1.jps命令查看本机java进程

2.jcmd命令查看运行时配置

准备程序

import java.io.IOException;

/**
 * @author pangbohuan
 * @date 2020/5/29 0029 19:30
 * @description
 */
public class Demo {

    public static void main(String[] args) throws IOException {
        System.out.println("Hello World!");
        System.in.read();
    }
}

启动程序,让他处于阻塞状态!哈哈,一切从Hello World开始然后又回到了Hello World!

开始验证

  • 1.打开CMD命令窗口

  • 2.输入jps命令查看本机java进程

    查看到我们的程序demo的id是2020

  • 3.输入jcmd 2020 help 命令查看运行时配置

    可以看到这里面有个参数是VM.system_properties查看系统环境变量

  • 4.输入 jcmd 2020 VM.system_properties 查看运行时环境变量

    这里面有很多配置信息,但是我们这一次找到java.class.path的信息看一下,里面包含了JDK存在的目录,maven的依赖jar包地址,编写的程序存放地址等这些信息。所以得出结论为什么我们如此轻松的跑起来java程序,是因为开发工具或是框架帮我们做了很多事情,配置了很多环境变量

类加载案例

自定义类加载器

需要测试加载的demo.java

package com.pbh.common.basics;

/**
 * @author pangbohuan
 * @date 2020/5/29 0029 19:30
 * @description
 */
public class Demo {

    public static void main(String[] args) {
        System.out.println();
    }

    static {
        System.out.println("静态方法执行");
    }

    public void test() {
        System.out.println("2222222222");
    }
}

准备一个类加载工作

package com.sydata.cscm.transfer.action;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.TimeUnit;

/**
 * @author pangbohuan
 * @date 2020/6/1 0001 15:25
 * @description 类加载测试
 */
public class LoadText {

    public static void main(String[] args) throws Exception {
        URL url = new URL("file:E:\\word\\pbh.Cloud.Parent\\common-basics\\target\\classes\\");
        URL[] urls = new URL[]{url};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        while (true) {
            Class<?> aClass = urlClassLoader.loadClass("com.pbh.common.basics.Demo");
            System.out.println("Demo使用的的类加载器:" + aClass.getClassLoader());
            Object object = aClass.newInstance();
            Object test = aClass.getMethod("test").invoke(object);
            System.out.println("调用Demo的test方法,返回值是" + test);

            //睡眠3秒执行一次
            TimeUnit.SECONDS.sleep(3);
        }
    }
}

从这个执行结果可以看到:加载外部类成功.静态代码块是在类第一次实例化的时候触发的.而且一次加载只会执行一次.加载已经成功了,那么卸载的动作是在什么时候做的呢?

一个类的卸载前提条件是加载这个类的加载器先被GC回收后才会卸载这个类

跟踪类的加载和卸载

将类加载器和类的实例置为空,跳出循环并通知一下GC

package com.sydata.cscm.transfer.action;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.TimeUnit;

/**
 * @author pangbohuan
 * @date 2020/6/1 0001 15:25
 * @description 类加载测试
 */
public class LoadText {

    public static void main(String[] args) throws Exception {

        URL url = new URL("file:E:\\word\\pbh.Cloud.Parent\\common-basics\\target\\classes\\");
        URL[] urls = new URL[]{url};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);

        while (true) {
            if(urlClassLoader==null){
                break;
            }
            System.out.println("开始加载com.pbh.common.basics.Demo类了");
            Class<?> aClass = urlClassLoader.loadClass("com.pbh.common.basics.Demo");
            System.out.println("Demo使用的的类加载器:" + aClass.getClassLoader());
            Object object = aClass.newInstance();
            Object test = aClass.getMethod("test").invoke(object);
            System.out.println("调用Demo的test方法,返回值是" + test);

            //睡眠3秒执行一次
            TimeUnit.SECONDS.sleep(3);

            object = null;
            urlClassLoader = null;
        }

        System.out.println("开始卸载com.pbh.common.basics.Demo类了");
        System.gc();
        TimeUnit.SECONDS.sleep(10);
    }
}

idea 设置一下vm参数 -verbose:class

然后点击运行,运行结果如下

可以看到jvm加载了很多类进来,其中Demo类加载是由urlClassLoader.loadClass(“com.pbh.common.basics.Demo”)触发的

可以看到当我们将类加载器和类的实例置空后,手动调用GC会卸载Demo类.手动调用GC并不一定会触发,只是给了GC一个参考建议.

小结

既然说一个class不能被同一个类加载器重复加载,那么用两个不同的加载器行不行?我们将URLClassLoader放入循环中,每次执行都实例化一个新的类加载器进行测试一下

package com.sydata.cscm.transfer.action;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.TimeUnit;

/**
 * @author pangbohuan
 * @date 2020/6/1 0001 15:25
 * @description 类加载测试
 */
public class LoadText {

    public static void main(String[] args) throws Exception {

        URL url = new URL("file:E:\\word\\pbh.Cloud.Parent\\common-basics\\target\\classes\\");
        URL[] urls = new URL[]{url};

        while (true) {
            URLClassLoader urlClassLoader = new URLClassLoader(urls);

            System.out.println("开始加载com.pbh.common.basics.Demo类了");
            Class<?> aClass = urlClassLoader.loadClass("com.pbh.common.basics.Demo");
            System.out.println("Demo使用的的类加载器:" + aClass.getClassLoader());
            Object object = aClass.newInstance();
            Object test = aClass.getMethod("test").invoke(object);
            System.out.println("调用Demo的test方法,返回值是" + test);

            //睡眠3秒执行一次
            TimeUnit.SECONDS.sleep(3);
        }
    }
}

可以看到,Demo类每次循环都被重新加载了,因为每次加载他的加载器都是不同的实例,如果是同一个类加载器实例加载同一个类,那么这个类就不会被重新加载.这也是很多框架实现的热加载功能

可以总结出来:一个类是否会被重复加载取决于类加载器实例ID+类的全名

双亲委派模型

概念

什么是双亲委派模型,这是一种对类加载器的逻辑定义方式.没有明确的指定父子关系

这是为了避免重复加载和保证沙箱安全的一种手段,由下到上逐级往上委托,由上到下逐级查找

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

这种设计也挺好理解,如果不走这个模式,假设开发者自己也定义一个java.lang.String的类,这时候java到底该使用哪个String呢?所以官方的说法是为了保证沙箱安全

案例

在之前类加载案例-小结中说到一个类是否会被重复加载取决于类加载器实例ID+类的全名,现在有了双亲委派的概念以后我们再测试一下,是否真的就像我们所说的一样,子类不会自己尝试去加载而是先交由它的父类加载

声明一个父类加载器加载urls让它可以搜素到子类加载器加载类的地方,然后实例化子类加载器时传入父类加载器定义一个逻辑关系,如下

package com.sydata.cscm.transfer.action;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.TimeUnit;

/**
 * @author pangbohuan
 * @date 2020/6/1 0001 15:25
 * @description 类加载测试
 */
public class LoadText {

    public static void main(String[] args) throws Exception {

        URL url = new URL("file:E:\\word\\pbh.Cloud.Parent\\common-basics\\target\\classes\\");
        URL[] urls = new URL[]{url};
        // 定义一个父类加载器
        URLClassLoader parentClassLoader = new URLClassLoader(urls);
        System.out.println("父类加载器:" + parentClassLoader);
        while (true) {
            //子类加载器实例化时声明一下所属的父类加载器
            URLClassLoader urlClassLoader = new URLClassLoader(urls, parentClassLoader);
            Class<?> aClass = urlClassLoader.loadClass("com.pbh.common.basics.Demo");
            System.out.println("Demo使用的的类加载器:" + aClass.getClassLoader());
            Object object = aClass.newInstance();
            Object test = aClass.getMethod("test").invoke(object);
            System.out.println("调用Demo的test方法,返回值是" + test);

            //睡眠3秒执行一次
            TimeUnit.SECONDS.sleep(3);
        }
    }
}

从输出结果可以看到

Demo使用的类加载器从始至终都是父类加载器的地址,也就证明了子类不会尝试自己去加载

Demo类的静态方法执行只调用了一次,也就证明Demo只被加载了一次

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值