文档章节

3,jvm 类加载机制

天呀鲁哇
 天呀鲁哇
发布于 2017/07/25 14:04
字数 2375
阅读 1
收藏 0
点赞 0
评论 0

一般说到java的类加载机制,都要说到“双亲委派模型”(其实个人很不理解为什么叫“双亲”,其实英文叫“parent”)。使用这种机制,可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。JVM根据 类名+包名+ClassLoader实例ID 来判定两个类是否相同,是否已经加载过(所以这里可以略微扩展下,可以通过创建不同的classloader实例来实现类的热部署)。 
有个图很形象(来源见参考资料)。

ClassLoader

注意:如果想形象地看到java的类加载顺序,可以在运行java的时候加个启动参数,即java –verbose

下面结合上图来详细介绍下java的类加载过程。

  • BootStrapClassLoader。它是最顶层的类加载器,是由C++编写而成, 已经内嵌到JVM中了。在JVM启动时会初始化该ClassLoader,它主要用来读取Java的核心类库JRE/lib/rt.jar中所有的class文件,这个jar文件中包含了java规范定义的所有接口及实现。
  • ExtensionClassLoader。它是用来读取Java的一些扩展类库,如读取JRE/lib/ext/*.jar中的包等(这里要注意,有些版本的是没有ext这个目录的)。
  • AppClassLoader。它是用来读取CLASSPATH下指定的所有jar包或目录的类文件,一般情况下这个就是程序中默认的类加载器。
  • CustomClassLoader。它是用户自定义编写的,它用来读取指定类文件 。基于自定义的ClassLoader可用于加载非Classpath中(如从网络上下载的jar或二进制)的jar及目录、还可以在加载前对class文件优一些动作,如解密、编码等。

很多资料和文章里说,ExtClassLoader的父类加载器是BootStrapClassLoader,其实这里省掉了一句话,容易造成很多新手(比如我)的迷惑。严格来说,ExtClassLoader的父类加载器是null,只不过在默认的ClassLoader 的 loadClass 方法中,当parent为null时,是交给BootStrapClassLoader来处理的,而且ExtClassLoader 没有重写默认的loadClass方法,所以,ExtClassLoader也会调用BootStrapLoader类加载器来加载,这就导致“BootStrapClassLoader具备了ExtClassLoader父类加载器的功能”。看一下下面的代码就很容易理解上面这一大段话了。

/**
 * 查看父类加载器
 */
private static void test1() {
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
    System.out.println("系统类装载器:" + appClassLoader);
    ClassLoader extensionClassLoader = appClassLoader.getParent();
    System.out.println("系统类装载器的父类加载器——扩展类加载器:" + extensionClassLoader);
    ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
    System.out.println("扩展类加载器的父类加载器——引导类加载器:" + bootstrapClassLoader);
}
  •  

可以看出ExtensionClassLoaderparentnull

三个重要的方法

查看classloader的源码可以发现三个重要的方法:

  • loadClass。classloader加载类的入口,此方法负责加载指定名字的类,ClassLoader的实现方法为先从已经加载的类中寻找,如没有则继续从父ClassLoader中寻找,如仍然没找到,则从BootstrapClassLoader中寻找,最后再调用findClass方法来寻找,如要改变类的加载顺序,则可覆盖此方法,如加载顺序相同,则可通过覆盖findClass来做特殊的处理,例如解密、固定路径寻找等,当通过整个寻找类的过程仍然未获取到Class对象时,则抛出ClassNotFoundException。如类需要resolve,则调用resolveClass进行链接。
  • findClass。此方法直接抛出ClassNotFoundException,因此需要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
  • defineClass。此方法负责将二进制的字节码转换为Class对象,这个方法对于自定义加载类而言非常重要,如二进制的字节码的格式不符合JVM Class文件的格式,抛出ClassFormatError;如需要生成的类名和二进制字节码中的不同,则抛出NoClassDefFoundError;如需要加载的class是受保护的、采用不同签名的或类名是以java.开头的,则抛出SecurityException;如需加载的class在此ClassLoader中已加载,则抛出LinkageError。

好,可能有人看了上面会疑惑,为什么上面说自定义classloader是需要重写findClass而不是loadClass或者其他方法。这里建议查看源码。

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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }
  •  

可以看到,JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的判断方法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。

看完这一大串可能有人还是不理解,ok,那我们现在就动手写一个自己的ClassLoader,尽量包含上面三个方法。

自定义ClassLoader

先定义一个Person接口。

public interface Person {
    public void say();
}
  •  

再定一个高富帅类实现这个接口

public class HighRichHandsome implements Person {

    @Override
    public void say() {
        System.out.println("I don't care whether you are rich or not");
    }

}
  •  

好的,开胃菜结束,主角来了,MyClassLoader。

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class MyClassLoader extends ClassLoader{
    /* 
     * 覆盖了父类的findClass,实现自定义的classloader
     */
    @Override
    public Class<?> findClass(String name) {
        byte[] bt = loadClassData(name);
        return defineClass(name, bt, 0, bt.length);
    }

    private byte[] loadClassData(String className) {
        InputStream is = getClass().getClassLoader().getResourceAsStream(
                className.replace(".", "/") + ".class");
        ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
        int len = 0;
        try {
            while ((len = is.read()) != -1) {
                byteSt.write(len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return byteSt.toByteArray();
    }
}
  •  

代码很简单,不解释了,最后在测试类LoaderTest里写个测试方法。

/**
 * 父类classloader
 * @throws Exception
 */
private static void test2() throws Exception{
    MyClassLoader loader = new MyClassLoader();
    Class<?> c = loader.loadClass("com.alibaba.classload.HighRichHandsome");
    System.out.println("Loaded by :" + c.getClassLoader());

    Person p = (Person) c.newInstance();
    p.say();

    HighRichHandsome man = (HighRichHandsome) c.newInstance();
    man.say();    
}
  •  

main方法中调用这个方法即可。LoaderTest默认构造函数会设置AppClassLoaderparent, 测试时执行test2()方法会发现HighRichHandsome类是委托AppClassLoader加载的,所以AppClassLoader可以访问到,不会出错。

但是我们再想一下,如果我们直接加载,不委托给父类加载,会出现什么情况?

/**
 * 自己的classloader加载
 * @throws Exception
 */
private static void test3() throws Exception{
    MyClassLoader loader = new MyClassLoader();
    Class<?> c = loader.findClass("com.alibaba.classload.HighRichHandsome");
    System.out.println("Loaded by :" + c.getClassLoader());

    Person p = (Person) c.newInstance();
    p.say();

    //注释下面两行则不报错
    HighRichHandsome man = (HighRichHandsome) c.newInstance();
    man.say();    
}
  •  

可以看到,悲剧的报错了。根据class loader命名空间规则,每个class loader都有自己唯一的命名空间,每个class loader 只能访问自己命名空间中的类,如果一个类是委托parent加载的,那么加载后,这个类就类似共享的,parent和child都可以访问到这个类,因为parent是不会委托child加载类的,所以child加载的类parent访问不到。简单来说,即子加载器的命名空间包含了parent加载的所有类,反过来则不成立。 本例中LoaderTest类是AppClassLoader加载的,所以其看不见由MyClassLoader加载的HighRichHandsome类,但Person接口是可以访问的,所以赋给Person类型不会出错。

一些小的知识点

1.Class.forName()

相信大家都写过连接数据库的例子,基本上就是加载驱动,建立连接,创建请求,写prepareStatement,关闭连接之类的。在这里,有一段代码:

public DbTest() {
    try {
        Class.forName("com.mysql.jdbc.Driver");// 加载驱动
        conn = DriverManager.getConnection(url, "root", "");// 建立连接
        stm = conn.createStatement(); // 创建请求
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  •  

我相信大家一开始的时候肯定都有些疑惑,就是Class.forName(“com.MySQL.jdbc.Driver”),为什么加载驱动是Class.forName,而不是ClassLoader的loadClass?为什么这么写就可以加载驱动了呢?

其实Class.forName()是显示加载类,作用是要求JVM查找并加载指定的类,也就是说JVM会执行该类的静态代码段。查看com.mysql.jdbc.Driver源码可以发现里面有个静态代码块,在加载后,类里面的静态代码块就执行(主要目的是注册驱动,把自己注册进去),所以主要目的就是为了触发这个静态方法。

2.Web容器加载机制

其实web容器的加载机制这一点我说不好,因为没看过诸如tomcat之类的源码,但是根据自己使用感觉以及查了相关资料,可以简单介绍一下。一般服务器端都要求类加载器能够反转委派原则,也就是先加载本地的类,如果加载不到,再到parent中加载。这个得细看,我这里只知道这个概念,所以就不说了。JavaEE规范推荐每个模块的类加载器先加载本类加载的内容,如果加载不到才回到parent类加载器中尝试加载。

3.重复加载与回收

一个class可以被不同的class loader重复加载,但同一个class只能被同一个class loader加载一次。见下列代码:

/**
 * 对象只加载一次,返回true
 */
private static void test4() {
    ClassLoader c1 = LoaderTest.class.getClassLoader();
    LoaderTest loadtest = new LoaderTest();
    ClassLoader c2 = loadtest.getClass().getClassLoader();
    System.out.println("c1.equals(c2):"+c1.equals(c2));
}
  •  

类的回收。说到最开始的why,其实java的回收机制我前面有篇博客说的比较详细,这里就插一句废话吧,当某个classloader加载的所有类实例化的所有对象都被回收了,则该classloader会被回收。

本文转载自:http://blog.csdn.net/tonytfjing/article/details/47212291

共有 人打赏支持
天呀鲁哇
粉丝 8
博文 82
码字总数 42007
作品 0
长宁
程序员
深入理解JVM学习笔记(一、总览)

1、JVM历史 2、JVM内存结构 3、JVM垃圾回收机制 4、JVM性能监控工具 5、JVM性能调优案例时间 6、JVM类文件结构 7、JVM类加载机制 8、JVM字节码执行引擎 9、JVM虚拟机编译及其运行时优化 10、...

jintaohahahaha ⋅ 05/28 ⋅ 0

结合JVM源码谈Java类加载器

一、前言 之前文章 Java 类加载器揭秘 从Java层面讲解了Java类加载器的原理,这里我们结合JVM源码在稍微深入讲解下。 二、Java类加载器的委托机制 Java 类加载器使用的是委托机制,也就是一个...

阿里加多 ⋅ 04/29 ⋅ 0

两道面试题,带你解析Java类加载机制

文章首发于【博客园-陈树义】,点击跳转到原文《两道面试题,带你解析Java类加载机制》 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: 请写出最后的输出字符...

陈树义 ⋅ 06/12 ⋅ 0

两道面试题带你解析 Java 类加载机制

在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题: class Grandpa{ } class Father extends Grandpa{ }class Son extends Father{ }public class Initialization...

⋅ 06/13 ⋅ 0

Java和Android ClassLoder对比以及Class加载过程

Java的ClassLoder的父子关系如下:Bootstrap--ExtClassClassLoader---AppClassLoader。 JVM启动时先运行启动类加载器Bottstrap,主要加载Java核心API;然后加载扩展类加载器ExtClassLoder,该...

JasmineBen ⋅ 05/23 ⋅ 0

有一到五年开发经验的JAVA程序员需要掌握的知识与技能!

JAVA是一种平台,也是一种程序设计语言,如何学好程序设计不仅仅适用于JAVA,对C++等其他程序设计语言也一样管用。有编程高手认为,JAVA也好C也好没什么分别,拿来就用。为什么他们能达到如此...

java高级架构牛人 ⋅ 06/02 ⋅ 0

面试中关于Java虚拟机(jvm)的问题看这篇就够了

最近看书的过程中整理了一些面试题,面试题以及答案都在我的文章中有所提到,希望你能在以问题为导向的过程中掌握虚拟机的核心知识。面试毕竟是面试,核心知识我们还是要掌握的,加油~~~ 下面...

snailclimb ⋅ 05/12 ⋅ 0

作为一个java程序员这些技能你都知道吗?

一、Java特点 1、 面向对象 尽管受到其前辈的影响,但Java没被设计成兼容其他语言源代码的程序。这允许Java开发组自由地从零开始。这样做的一个结果是,Java语言可以更直接、更易用、更实际的...

java高级架构牛人 ⋅ 05/23 ⋅ 0

Java 面试知识点解析(三)——JVM篇

前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大部...

我没有三颗心脏 ⋅ 05/16 ⋅ 0

类加载器ClassLoader-1

一, 类加载器深入剖析 1,Java虚拟机与程序的生命周期 在如下几种情况下,Java虚拟机将结束生命周期: –执行了System.exit()方法 –程序正常执行结束 –程序在执行过程中遇到了异常或错误而...

康熙兄弟 ⋅ 06/02 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

645. Set Mismatch - LeetCode

Question 645. Set Mismatch Solution 思路: 遍历每个数字,然后将其应该出现的位置上的数字变为其相反数,这样如果我们再变为其相反数之前已经成负数了,说明该数字是重复数,将其将入结果r...

yysue ⋅ 20分钟前 ⋅ 0

Confluence 6 从生产环境中恢复一个测试实例

请参考 Restoring a Test Instance from Production 页面中的内容获得更多完整的说明。 很多 Confluence 的管理员将会使用生产实例运行完整数据和服务的 Confluence 服务器,同时还会设置一个...

honeymose ⋅ 25分钟前 ⋅ 0

Python这么强?红包杀手、消息撤回也可以无视,手机App辅助!

论述 标题也许有点不好理解,其实就是一款利用Python实现的可以监控微信APP内的红包与消息撤回的助手。不得不说,这确实是一款大家钟意的神器。 消息撤回是一件很让人恶心的事,毕竟人都是有...

Python燕大侠 ⋅ 36分钟前 ⋅ 0

压缩打包介绍、gzip压缩工具、bzip2压缩工具、xz压缩工具

压缩打包介绍 压缩的好处不仅能节省磁盘空间而且在传输的时候节省传输时间和网络带宽 windows系统下文件带有 .rar .zip .7z 后缀的就是压缩文件 linux系统下则是 .zip, .gz, .bz2, .xz, ...

黄昏残影 ⋅ 41分钟前 ⋅ 0

观察者模式

1.利用java原生类进行操作 package observer;import java.util.Observable;import java.util.Observer;/** * @author shadow * @Date 2016年8月12日下午7:29:31 * @Fun 观察目标 **/......

Cobbage ⋅ 43分钟前 ⋅ 0

Ubuntu打印服务器配置

参考:https://blog.csdn.net/gsls200808/article/details/50950586 https://blog.csdn.net/jiay2/article/details/80252369 https://wiki.gentoo.org/wiki/HPLIP 由于媳妇儿要大量打印资料,......

大熊猫 ⋅ 49分钟前 ⋅ 0

面试的角度诠释Java工程师(二)

原文出处: locality 续言: 相信每一位简书的作者,都会有我这样的思考:怎么写好一篇文章?或者怎么写好一篇技术类的文章?我就先说说我的感悟吧,写文章其实和写程序是一样的。为什么我会...

颖伙虫 ⋅ 52分钟前 ⋅ 0

github中SSH的Key

https://help.github.com/articles/connecting-to-github-with-ssh/ https://help.github.com/articles/testing-your-ssh-connection/ https://help.github.com/articles/adding-a-new-ssh-k......

whoisliang ⋅ 52分钟前 ⋅ 0

only_full_group_by

我的mysql是在CentOS7.1下面的5.7.17 在 /etc/my.cnf 文件里加上如下: sql_mode='NO_ENGINE_SUBSTITUTION' 然后,重启Mysql服务 systemctl restart mysqld...

SunHacker ⋅ 今天 ⋅ 0

实际项目(SpringBoot项目)中集成Druid

参考网页 https://blog.csdn.net/liuchuanhong1/article/details/55050131 https://blog.csdn.net/CoffeeAndIce/article/details/78707819 https://www.pocketdigi.com/20170530/1577.html 为......

karma123 ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部