从源码透彻理解JVM类加载机制

Posted 纵横千里,捭阖四方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从源码透彻理解JVM类加载机制相关的知识,希望对你有一定的参考价值。

本文我们深入分析一下类加载机制的原理,然后从源码角度看一下加载的过程。

目录

1 类加载器分类初探

2 三种类加载器介绍

3 从源码角度分析加载过程

3.1 创建扩展类加载器

3.2 构造应用类加载器


1 类加载器分类初探

JVM严格来讲支持两种类型的类加载器 ,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。BootstrapClassLoader是JVM里用C++实现的,而后者是使用Java语言来基于BootstrapClassLoader扩展来实现的。

而自定义类加载器可以进一步分为扩展加载器、系统加载器和用户自定义加载器三种。

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示

 上面的四类之间的关系是包含关系,不是上下层关系,也不是简单的父子类继承关系。这个怎么理解呢?我们后面章节看一下源码就明白了。我们这里先验证一下家在启动特征:

public static void main(String[] args) 
   //获取系统类加载器
   ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
   System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

   //获取其上层:扩展类加载器
   ClassLoader extClassLoader = systemClassLoader.getParent();
   System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6

   //获取其上层:获取不到引导类加载器
   ClassLoader bootstrapClassLoader = extClassLoader.getParent();
   System.out.println(bootstrapClassLoader);//null

上面代码中,我们可以获取系统类加载器地址,也可以获取其上层扩展类加载器地址,但是无法获得引导类地址。为什么获取不到呢?这是因为引导类加载器是JVM里用C++实现的,在Java代码里已经看不到了。

我们一般的业务类都是由系统加载器来加载的,例如:

//对于用户自定义类来说:默认使用系统类加载器进行加载
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

可以看到上面的地址与systemClassLoader的地址是一致的,两者是一个。

那jdk提供的核心类库使用的是什么加载器呢?我们可以测试一下:

ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null

此时获取不到,因为引导类加载器由 C/C++实现的,这简介说明String类使用引导类加载器进行加载的,由此也明确了 Java的核心类库都是使用引导类加载器进行加载的。

另外,我们上面两次执行代码,系统加载器的地址都是sun.misc.Launcher$AppClassLoader@18b4aac2,这也说明JVM此时只有一个系统类加载器,是全局唯一的。

2 三种类加载器介绍

接下来,我们就逐个介绍一下几种类加载器。

第一种:启动类加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  1. 这个类加载使用C/C++语言实现的,嵌套在JVM内部

  2. 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类

  3. 万物之祖,不继承其他类,没有父加载器

  4. 加载扩展类和应用程序类加载器,并作为他们的父类加载器

  5. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

第二种:扩展类加载器

扩展类加载器(Extension ClassLoader)

  1. Java语言编写,由sun.misc.Launcher$ExtClassLoader实现

  2. 派生于ClassLoader类

  3. 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

第三种:应用类加载器

应用程序类加载器(也称为系统类加载器,AppClassLoader),也就是用户自己写的类。

  1. Java语言编写,由sun.misc.LaunchersAppClassLoader实现

  2. 派生于ClassLoader类,父类加载器为扩展类加载器

  3. 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库

  4. 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载

  5. 通过classLoader.getSystemclassLoader()方法可以获取到该类加载器

我们可以通过下面的例子看一下加载器访问路径的情况:

public class ClassLoaderTest1 
    public static void main(String[] args) 
        System.out.println("**********启动类加载器**************");
        //获取BootstrapClassLoader能够加载的api的路径
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) 
            System.out.println(element.toExternalForm());
        
        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:引导类加载器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);
​
        System.out.println("***********扩展类加载器*************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) 
            System.out.println(path);
        
​
        //从上面的路径中随意选择一个类,来看看他的类加载器是什么:扩展类加载器
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@4dc63996
    

输出结果为:

**********启动类加载器**************
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/classes
null
***********扩展类加载器*************
/Users/liuqingchao/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
sun.misc.Launcher$ExtClassLoader@4dc63996

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。那为什么还需要自定义类加载器呢?

  1. 隔离加载类(比如说我假设现在Spring框架,和RocketMQ有包名路径完全一样的类,类名也一样,这个时候类就冲突了。不过一般的主流框架和中间件都会自定义类加载器,实现不同的框架,中间价之间是隔离的)

  2. 修改类加载的方式

  3. 扩展加载源(还可以考虑从数据库中加载类,路由器等等不同的地方)

  4. 防止源码泄漏(对字节码文件进行解密,自己用的时候通过自定义类加载器来对其进行解密)

通过上面的对比,可以看到几种加载器一个很重要的区别就是加载时访问的基础路径是不一样的,几种路径归纳起来就是:

boot_path  = SecuritySupport.getSystemProperty("sun.boot.class.path");
ext_path   = SecuritySupport.getSystemProperty("java.ext.dirs");
class_path = SecuritySupport.getSystemProperty("java.class.path");

3 从源码角度分析加载过程

我们写的类,例如:

public class Math 
    public static final int initData = 666;
    //一个方法对应一块栈帧内存区域
    public int compute()  
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    
​
    public static void main(String[] args) 
        Math math = new Math();
        math.compute();
    

其到底是如何加载的呢?本节我们就通过源码来更深入的探究一下。

类加载器初始化过程如下图所示:

 上面的部分有很多功能是通过C++实现的,我们难以阅读或者调试,要完全梳理清楚非常困难,我们接下来看几个关键步骤是如何做的。

3.1 创建扩展类加载器

创建的主要过程是:

  1. java启动时会首先创建JVM启动器sun.misc.Launcher实例。

  2. 在Launcher构造方法内部,其创建了两个类加载器,分别是 sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

  3. JVM使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

具体来说,JVM启动时通过一系列处理之后,最后调到了Launcher类,这里的Launcher就是亚当和夏娃,或者是女娲娘娘创造的第一个人,我们重点看该类的前面部分:

public Launcher() 
        Launcher.ExtClassLoader var1;
        try 
        //1:构造扩展类加载器,在构造的过程中将其父加载器设置为null
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
         catch (IOException var10) 
            throw new InternalError("Could not create extension class loader", var10);
        
​
        try 
        //2.构造应用类加载器,在构造的过程中将其父加载器设置为ExtClassLoader
        //Launcher的loader属性值是AppClassLoader,我们一般都是用这个类加载器来加载我们自己写的应用程序
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
         catch (IOException var9) 
            throw new InternalError("Could not create application class loader", var9);
            
....
    

上面已经注释了代码的执行位置处依次创建构造类加载器和应用类加载器。

我们本小节看一下getExtClassLoader()是如何构造扩展类的,下一小节看一下getAppClassLoader()如何构造应用类加载器的。

在`getExtClassLoader()中,实例初始化使用了经典的单例模式,保证一个JVM虚拟机内只有一个。

static class ExtClassLoader extends URLClassLoader 
    private static volatile Launcher.ExtClassLoader instance;
​
    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException 
        if (instance == null) 
            Class var0 = Launcher.ExtClassLoader.class;
            synchronized(Launcher.ExtClassLoader.class) 
                if (instance == null) 
                    instance = createExtClassLoader();
                
            
        
        return instance;
    

上面的核心工作是创建了一个拓展加载器实例。我们注意到ExtClassLoader 在构造时,没有传入参数,但实际其父类加载器是启动类加载器,是C++实现的。所以在java代码中只出现了BootClassPathHolder。而相关的实现我们不能再通过java代码看到了,我们就此打住。

那createExtClassLoader()里有做了什么呢?我们继续看:

private static Launcher.ExtClassLoader createExtClassLoader() throws IOException 
    try 
        return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() 
            public Launcher.ExtClassLoader run() throws IOException 
                File[] var1 = Launcher.ExtClassLoader.getExtDirs();
                int var2 = var1.length;
​
                for(int var3 = 0; var3 < var2; ++var3) 
                    MetaIndex.registerDirectory(var1[var3]);
                
                return new Launcher.ExtClassLoader(var1);
            
        );
     catch (PrivilegedActionException var1) 
        throw (IOException)var1.getException();
    

可以看到上面主要是在读文件目录,Launcher.ExtClassLoader.getExtDirs()很明显这里是读了扩展类加载器的目录,然后就是进一步遍历和处理,最后这些参数将作为创建扩展类加载器的构造方法的参数new Launcher.ExtClassLoader(var1)。在这个构造方法中还会进行权限等方面的验证,我们不再细看。

到这里,我们大致了解了构造扩展类加载器的基本过程,再深入看会有很多方法都是native的,也就是由C++来实现的,因此代码我们就看到这里。

3.2 构造应用类加载器

我们回到Launcher()方法,再看扩展类加载器:

try 
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
 catch (IOException var9) 
    throw new InternalError("Could not create application class loader", var9);

可以看到这里是将扩展类加载器的信息作为参数来创建一个载入器,这也是我们前面说的不同级别的加载器之间是继承又不是继承的关系。

我们继续看getAppClassLoader()的实现:

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException 
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() 
        public Launcher.AppClassLoader run() 
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        
    );

可以看到这里就是从系统的环境变量中找到java.class.path指向的类路径,用new Launcher.AppClassLoader(var1x, var0)来创建加载器对象。

具体来说,AppClassLoader的getAppClassLoader()方法,将ExtClassLoader对象 var0传递给超类ClassLoader,并标记ExtClassLoader 扩展类加载器是AppClassLoader应用程序加载器的父加载器。

我们再看一下其实现:

AppClassLoader(URL[] var1, ClassLoader var2) 
    super(var1, var2, Launcher.factory);
    this.ucp.initLookupCache(this);

在这里调用的super()方法会进一步将另外两个重要的加载器一起给创建出来,不过呢,我们理解到这一步就够了。

以上是关于从源码透彻理解JVM类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

从JDK源码级别彻底剖析JVM类加载机制

一:从JDK源码级别彻底剖析JVM类加载机制

02-从JDK源码级别彻底剖析JVM类加载机制

03-从JDK源码级别彻底剖析JVM类加载机制

JAVA类加载机制详解

别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析JVM篇二