jvm——类加载机制

类生命周期:

Created with Raphaël 2.2.0 开始 1、读取二进制内容 2、验证文件格式规范、语义分析、引用验证、字节码验证 3、分配内存、变量初始化,比如int设置为0,引用设置为null,布尔类型设置为false 4、解析类、接口、字段、方法 5、执行静态代码块,为静态变量赋值。 6、创建对象,开始使用 7、卸载 结束

其中1到5这几个步骤属于类加载,而其中能够干预的只有第一步,其他四步都是jvm自动的,程序员无法干预。

类加载器

    类加载器负责装入类,就像上面说的,实际上类加载器只能干预到第一步,他可以搜索网络、jar包、zip包、文件夹、二进制数据、内存指定位置,凡是可以存储的数据的地方,他都可以去加载。一个java运行是,至少有三个类加载实例,负责不同类型类的加载。

  • Bootstrap loader核心类库加载器:他是c或者c++实现的,没有对应的java类,他负责加载JRE_HOME/jre/lib目录或者用户配置的目录。由于没有对应的java类,所以打印这个加载器的类名是,输出为null

  • Extension Class Loader拓展类库加载器:ExtClassLoader的实例,他负责加载JRE_HOME/jre/lib/ext目录、jdk扩展包、用户配置的目录

  • application class loader用户应用程序加载器:AppClassLoader的实例,他负责加载java.class.path指定的目录,用户应用程序class-path 或者java命令运行时使用参数-cp。

public class ClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(String.class.getClassLoader());
        System.out.println(ZipPath.class.getClassLoader());
        System.out.println(ClassLoader.class.getClassLoader());
    }
}

null
sun.misc.Launcher$ExtClassLoader@694f9431
sun.misc.Launcher$AppClassLoader@18b4aac2

动态加载

    jvm是如何定一个唯一类的呢?需要同一个加载器+同一个类名。当一个加载器去加载一个类的时候,会判断这个类自己是否已经加载过,如果已经加载过,就不会重新加载了。
测试类:

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException, InterruptedException, NoSuchMethodException, InvocationTargetException {
    ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:C:\\Users\\xiaosong.yang\\Desktop\\")});
    while (true) {
        Class clazz = classLoader.loadClass("Test1");
        Object classLoaderTest = clazz.newInstance();
        clazz.getMethod("sayHello").invoke(classLoaderTest);
        Thread.sleep(1000);
    }
}

被加载的类

public class Test1 {

    public void sayHello() {
        System.out.println("xxxx");
    }

}

这上面的代码,我在我的桌面上建了一个Test.java的类,然后通过javac编译成Test.class。然后在IDEA中通过这段代码进行加载,创建了一个加载器,然后不断地重新加载这个类,如果我们在测试程序运行过程中,修改被加载的类的输出,从"xxxx"改成"haha",然后编译,会发现这个类测试类中的输出还是没有变。如果我们把测试类中的加载器每次都创建一个新的加载器,如下:

public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException, InterruptedException, NoSuchMethodException, InvocationTargetException {
    while (true) {
        ClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:C:\\Users\\xiaosong.yang\\Desktop\\")});
        Class clazz = classLoader.loadClass("Test1");
        Object classLoaderTest = clazz.newInstance();
        clazz.getMethod("sayHello").invoke(classLoaderTest);
        Thread.sleep(1000);
    }
}

如图,把classLoader的创建,放到while循环内部,这样每次加载Test1的时候,都是一个新的加载器,然后我们再重复上面的操作,把"xxxx"改成"haha",然后编译,这样我们会发现,输出会自动变成新的。正如我们最上面说的一个加载器只会加载一个类一次。

双亲委派模型

双亲委派模型
    双亲委派模型是java推荐,也是默认的一种类加载机制,如图所示,当你自己的加载器想加载一个类的时候,他会向上委托,让上层的加载器来加载,如果上次加载器找不到,再让下寻找,直到有找得到这个类的加载器来加载。比如我们上面所演示的动态加载的代码,如果我们将被加载类和测试类放在同一个项目中,那当UrlClassLoader加载器进行加载时,就会向上委托,到Bootstrap来加载,然后Bootstrap应该找不到这个类,再往下寻找,Extension加载器发现也找不到,然后再往下寻找,然后Application加载器发现可以加载,就会把Test类给加载了,然后我们循环去动态加载,其实是无效的。所以上面那个动态加载类,如果想要在一个应用中实现,仅仅像上面这么写是不行的,因为受到了双亲委派模型的限制,表面上看上去在不断地new新的加载器,实际上都不是你new的这个加载器加载的Test。
    双亲委派模型能起到公用和隔离这两个作用。比如一个tomcat容器中,放了两个应用,A应用和B应用有各自属于自己独有的类,和加载器,这个时候可以通过这个模型,在最下层进行隔离,让他们互相不受影响。同样对于一些公共的类模块,这个模型又可以保证他们互相公用,节省内存空间。

双亲委派模型的打破

    世事都不是绝对的,双亲委派模型也有他的缺点,所以在java历史上双亲委派模型被多次打破过。

远古时代的兼容

    双亲委派模型是java1.2才有的,而类加载器和抽象类java.lang.ClassLoader则在java1.0时代就已经存在了,所以当java1.2出现双亲委派模型时,为了向前兼容,该模型不得不做出妥协。所以第一种打破模型的方式就是自定义一个ClassLoader,重写其中的findClass方法即可,这样就会走自己的类加载代码,不会向上委派。


import java.io.*;

public class MyClassLoader extends ClassLoader {


    private String path;


    private String className;


    public MyClassLoader(String path, String className) {
        this.path = path;
        this.className = className;
    }



    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        String filePath = path + name + ".class";
        byte[] b = new byte[0];
        try {
            InputStream is = new FileInputStream(new File(filePath));
            OutputStream os = new ByteArrayOutputStream();
            b = new byte[1024];
            int length = 0;
            while ((length = is.read(b)) != -1) {
                os.write(b, 0, length);
            }
            return defineClass(name, ((ByteArrayOutputStream) os).toByteArray(), 0, ((ByteArrayOutputStream) os).toByteArray().length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


}

由上向下的调用

    双亲委派模型是自下而上的调用,但是也是存在自上而下的调用,比如JNDI、JDBC、JCE、JAXB等。为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置或者直接通过Thread.currentThread.getContextClassLoader来获取当前线程的加载器进行加载,而不是传递到底层去加载。如果创建线程时未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器(也就是Application加载器)。
    以JDBC为例,传统模式下我们利用jdbc链接数据库,都需要指明数据库的驱动包:

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

或者这样:

System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

但是在SPI机制下,我们不用再手动设置驱动包,

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:33061/xxx?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull", "root", "123456");

DriverManager中会有一个静态代码块执行ServiceLoader.load(Class<S> service, ClassLoader loader)这个方法。但是ServiceLoader是Bootstrap loader加载器加载的,根据双亲委派模型,他是无法进行加载由服务商提供的驱动包,所以这个时候就通过setContextClassLoader()的方法,偷偷把服务商提供的类加载进来。

热部署需求

    第三次双亲委派模型的打破是因为热部署(动态部署)的需求导致的。
OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
1)将java.*开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7)否则,类加载器失败。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值