一看你就懂,超详细java中的ClassLoader详解

Posted frank909

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一看你就懂,超详细java中的ClassLoader详解相关的知识,希望对你有一定的参考价值。

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

ClassLoader翻译过来就是类加载器,普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。本文的目的也是学习ClassLoader这种加载机制。

备注:本文篇幅比较长,但内容简单,大家不要恐慌,安静地耐心翻阅就是

Class文件的认识

我们都知道在Java中程序是运行在虚拟机中,我们平常用文本编辑器或者是IDE编写的程序都是.java格式的文件,这是最基础的源码,但这类文件是不能直接运行的。如我们编写一个简单的程序HelloWorld.java

public class HelloWorld

	public static void main(String[] args)
		System.out.println("Hello world!");
	

如图:

然后,我们需要在命令行中进行java文件的编译

javac HelloWorld.java


可以看到目录下生成了.class文件

我们再从命令行中执行命令:

java HelloWorld

上面是基本代码示例,是所有入门JAVA语言时都学过的东西,这里重新拿出来是想让大家将焦点回到class文件上,class文件是字节码格式文件,java虚拟机并不能直接识别我们平常编写的.java源文件,所以需要javac这个命令转换成.class文件。另外,如果用C或者PYTHON编写的程序正确转换成.class文件后,java虚拟机也是可以识别运行的。更多信息大家可以参考这篇

了解了.class文件后,我们再来思考下,我们平常在Eclipse中编写的java程序是如何运行的,也就是我们自己编写的各种类是如何被加载到jvm(java虚拟机)中去的。

你还记得java环境变量吗?

初学java的时候,最害怕的就是下载JDK后要配置环境变量了,关键是当时不理解,所以战战兢兢地照着书籍上或者是网络上的介绍进行操作。然后下次再弄的时候,又忘记了而且是必忘。当时,心里的想法很气愤的,想着是–这东西一点也不人性化,为什么非要自己配置环境变量呢?太不照顾菜鸟和新手了,很多菜鸟就是因为卡在环境变量的配置上,遭受了太多的挫败感。

因为我是在Windows下编程的,所以只讲Window平台上的环境变量,主要有3个:JAVA_HOMEPATHCLASSPATH

JAVA_HOME

指的是你JDK安装的位置,一般默认安装在C盘,如

C:\\Program Files\\Java\\jdk1.8.0_91

PATH

将程序路径包含在PATH当中后,在命令行窗口就可以直接键入它的名字了,而不再需要键入它的全路径,比如上面代码中我用的到javacjava两个命令。
一般的

PATH=%JAVA_HOME%\\bin;%JAVA_HOME%\\jre\\bin;%PATH%;

也就是在原来的PATH路径上添加JDK目录下的bin目录和jre目录的bin.

CLASSPATH

CLASSPATH=.;%JAVA_HOME%\\lib;%JAVA_HOME%\\lib\\tools.jar

一看就是指向jar包路径。
需要注意的是前面的.;.代表当前目录。

环境变量的设置与查看

设置可以右击我的电脑,然后点击属性,再点击高级,然后点击环境变量,具体不明白的自行查阅文档。

查看的话可以打开命令行窗口


echo %JAVA_HOME%

echo %PATH%

echo %CLASSPATH%

好了,扯远了,知道了环境变量,特别是CLASSPATH时,我们进入今天的主题Classloader.

JAVA类加载流程

Java语言系统自带有三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\\lib下的rt.jar、resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。我们可以打开我的电脑,在上面的目录下查看,看看这些jar包是不是存在于这个目录。
  • Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\\lib\\ext目录下的jar包和class文件。还可以加载-D java.ext.dirs选项指定的目录。
  • Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。

我们上面简单介绍了3个ClassLoader。说明了它们加载的路径。并且还提到了-Xbootclasspath-D java.ext.dirs这两个虚拟机参数选项。

加载顺序?

我们看到了系统的3个类加载器,但我们可能不知道具体哪个先行呢?
我可以先告诉你答案

  1. Bootstrap CLassloder
  2. Extention ClassLoader
  3. AppClassLoader

为了更好的理解,我们可以查看源码。
sun.misc.Launcher,它是一个java虚拟机的入口应用。

public class Launcher 
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() 
        return launcher;
    

    private ClassLoader loader;

    public Launcher() 
        // Create the extension class loader
        ClassLoader extcl;
        try 
            extcl = ExtClassLoader.getExtClassLoader();
         catch (IOException e) 
            throw new InternalError(
                "Could not create extension class loader", e);
        

        // Now create the class loader to use to launch the application
        try 
            loader = AppClassLoader.getAppClassLoader(extcl);
         catch (IOException e) 
            throw new InternalError(
                "Could not create application class loader", e);
        

        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解
        Thread.currentThread().setContextClassLoader(loader);
    

    /*
     * Returns the class loader used to launch the main application.
     */
    public ClassLoader getClassLoader() 
        return loader;
    
    /*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader 

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader 

源码有精简,我们可以得到相关的信息。

  1. Launcher初始化了ExtClassLoader和AppClassLoader。
  2. Launcher中并没有看见BootstrapClassLoader,但通过System.getProperty("sun.boot.class.path")得到了字符串bootClassPath,这个应该就是BootstrapClassLoader加载的jar包路径。

我们可以先代码测试一下sun.boot.class.path是什么内容。

System.out.println(System.getProperty("sun.boot.class.path"));

得到的结果是:

C:\\Program Files\\Java\\jre1.8.0_91\\lib\\resources.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\lib\\rt.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\lib\\sunrsasign.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\lib\\jsse.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\lib\\jce.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\lib\\charsets.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\lib\\jfr.jar;
C:\\Program Files\\Java\\jre1.8.0_91\\classes

可以看到,这些全是JRE目录下的jar包或者是class文件。

ExtClassLoader源码

如果你有足够的好奇心,你应该会对它的源码感兴趣

/*
     * The class loader used for loading installed extensions.
     */
    static class ExtClassLoader extends URLClassLoader 

        static 
            ClassLoader.registerAsParallelCapable();
        

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        
            final File[] dirs = getExtDirs();

            try 
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() 
                        public ExtClassLoader run() throws IOException 
                            int len = dirs.length;
                            for (int i = 0; i < len; i++) 
                                MetaIndex.registerDirectory(dirs[i]);
                            
                            return new ExtClassLoader(dirs);
                        
                    );
             catch (java.security.PrivilegedActionException e) 
                throw (IOException) e.getException();
            
        

        private static File[] getExtDirs() 
            String s = System.getProperty("java.ext.dirs");
            File[] dirs;
            if (s != null) 
                StringTokenizer st =
                    new StringTokenizer(s, File.pathSeparator);
                int count = st.countTokens();
                dirs = new File[count];
                for (int i = 0; i < count; i++) 
                    dirs[i] = new File(st.nextToken());
                
             else 
                dirs = new File[0];
            
            return dirs;
        
 
......
    

我们先前的内容有说过,可以指定-D java.ext.dirs参数来添加和改变ExtClassLoader的加载路径。这里我们通过可以编写测试代码。

System.out.println(System.getProperty("java.ext.dirs"));

结果如下:

C:\\Program Files\\Java\\jre1.8.0_91\\lib\\ext;C:\\Windows\\Sun\\Java\\lib\\ext

AppClassLoader源码

/**
     * The class loader used for loading from java.class.path.
     * runs in a restricted security context.
     */
    static class AppClassLoader extends URLClassLoader 


        public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

     
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() 
                    public AppClassLoader run() 
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                
            );
        

        ......
    

可以看到AppClassLoader加载的就是java.class.path下的路径。我们同样打印它的值。

System.out.println(System.getProperty("java.class.path"));

结果:

D:\\workspace\\ClassLoaderDemo\\bin

这个路径其实就是当前java工程目录bin,里面存放的是编译生成的class文件。

好了,自此我们已经知道了BootstrapClassLoader、ExtClassLoader、AppClassLoader实际是查阅相应的环境属性sun.boot.class.pathjava.ext.dirsjava.class.path来加载资源文件的。

接下来我们探讨它们的加载顺序,我们先用Eclipse建立一个java工程。

然后创建一个Test.java文件。

public class Test

然后,编写一个ClassLoaderTest.java文件。


public class ClassLoaderTest 

	public static void main(String[] args) 
		// TODO Auto-generated method stub
	
		ClassLoader cl = Test.class.getClassLoader();
		
		System.out.println("ClassLoader is:"+cl.toString());
		
	


我们获取到了Test.class文件的类加载器,然后打印出来。结果是:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93  

也就是说明Test.class文件是由AppClassLoader加载的。

这个Test类是我们自己编写的,那么int.class或者是String.class的加载是由谁完成的呢?
我们可以在代码中尝试

public class ClassLoaderTest 

	public static void main(String[] args) 
		// TODO Auto-generated method stub
	
		ClassLoader cl = Test.class.getClassLoader();
		
		System.out.println("ClassLoader is:"+cl.toString());
		
		cl = int.class.getClassLoader();
		
		System.out.println("ClassLoader is:"+cl.toString());
		
	


运行一下,却报错了

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" java.lang.NullPointerException
	at ClassLoaderTest.main(ClassLoaderTest.java:15)

提示的是空指针,意思是int.class这类基础类没有类加载器加载?

当然不是!
int.class是由Bootstrap ClassLoader加载的。要想弄明白这些,我们首先得知道一个前提。

每个类加载器都有一个父加载器

每个类加载器都有一个父加载器,比如加载Test.class是由AppClassLoader完成,那么AppClassLoader也有一个父加载器,怎么样获取呢?很简单,通过getParent方法。比如代码可以这样编写:

ClassLoader cl = Test.class.getClassLoader();
		
System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\\'s parent is:"+cl.getParent().toString());

运行结果如下:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742

这个说明,AppClassLoader的父加载器是ExtClassLoader。那么ExtClassLoader的父加载器又是谁呢?

System.out.println("ClassLoader is:"+cl.toString());
System.out.println("ClassLoader\\'s parent is:"+cl.getParent().toString());
System.out.println("ClassLoader\\'s grand father is:"+cl.getParent().getParent().toString());

运行如果:

ClassLoader is:sun.misc.Launcher$AppClassLoader@73d16e93
Exception in thread "main" ClassLoader's parent is:sun.misc.Launcher$ExtClassLoader@15db9742
java.lang.NullPointerException
	at ClassLoaderTest.main(ClassLoaderTest.java:13)

又是一个空指针异常,这表明ExtClassLoader也没有父加载器。那么,为什么标题又是每一个加载器都有一个父加载器呢?这不矛盾吗?为了解释这一点,我们还需要看下面的一个基础前提。

父加载器不是父类

我们先前已经粘贴了ExtClassLoader和AppClassLoader的代码。

static class ExtClassLoader extends URLClassLoader 
static class AppClassLoader extends URLClassLoader 

可以看见ExtClassLoader和AppClassLoader同样继承自URLClassLoader,但上面一小节代码中,为什么调用AppClassLoader的getParent()代码会得到ExtClassLoader的实例呢?先从URLClassLoader说起,这个类又是什么?
先上一张类的继承关系图

URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。

public abstract class ClassLoader 

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// The class loader for the system
    // @GuardedBy("ClassLoader.class")
private static ClassLoader scl;

private ClassLoader(Void unused, ClassLoader parent) 
    this.parent = parent;
    ...

protected ClassLoader(ClassLoader parent) 
    this(checkCreateClassLoader(), parent);

protected ClassLoader() 
    this(checkCreateClassLoader(), getSystemClassLoader());

public final ClassLoader getParent() 
    if (parent == null)
        return null;
    return parent;

public static ClassLoader getSystemClassLoader() 
    initSystemClassLoader();
    if (scl == null) 
        return null;
    
    return scl;


private static synchronized void initSystemClassLoader() 
    if (!sclSet) 
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) 
            Throwable oops = null;
            //通过Launcher获取ClassLoader
            scl = l.getClassLoader();
            try 
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
             catch (PrivilegedActionException pae) 
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) 
                    oops = oops.getCause();
                
            
            if (oops != null) 
                if (oops instanceof Error) 
                    throw (Error) oops;
                 else 
                    // wrap the exception
                    throw new Error(oops);
                
            
        
        sclSet = true;
    


我们可以看到getParent()实际上返回的就是一个ClassLoader对象parent,parent的赋值是在ClassLoader对象的构造方法中,它有两个情况:

  1. 由外部类创建ClassLoader时直接指定一个ClassLoader为parent。
  2. getSystemClassLoader()方法生成,也就是在sun.misc.Laucher通过getClassLoader()获取,也就是AppClassLoader。直白的说,一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。

我们主要研究的是ExtClassLoader与AppClassLoader的parent的来源,正好它们与Launcher类有关,我们上面已经粘贴过Launcher的部分代码。

public class Launcher 
    private static URLStreamHandlerFactory factory = new Factory();
    private static Launcher launcher = new Launcher();
    private static String bootClassPath =
        System.getProperty("sun.boot.class.path");

    public static Launcher getLauncher() 
        return launcher;
    

    private ClassLoader loader;

    public Launcher() 
        // Create the extension class loader
        ClassLoader extcl;
        try 
            extcl = ExtClassLoader.getExtClassLoader();
         catch (IOException e) 
            throw new InternalError(
                "Could not create extension class loader", e);
        

        // Now create the class loader to use to launch the application
        try 
	    //将ExtClassLoader对象实例传递进去
            loader = AppClassLoader.getAppClassLoader(extcl);
         catch (IOException e) 
            throw new InternalError(
                "Could not create application class loader", e);
        

public ClassLoader getClassLoader() 
        return loader;
    
static class ExtClassLoader extends URLClassLoader 

        /**
         * create an ExtClassLoader. The ExtClassLoader is created
         * within a context that limits which files it can read
         */
        public static ExtClassLoader getExtClassLoader() throws IOException
        
            final File[] dirs = getExtDirs();

            try 
                // Prior implementations of this doPrivileged() block supplied
                // aa synthesized ACC via a call to the private method
                // ExtClassLoader.getContext().

                return AccessController.doPrivileged(
                    new PrivilegedExceptionAction<ExtClassLoader>() 
                        public ExtClassLoader run() throws IOException 
                            //ExtClassLoader在这里创建
                            return new ExtClassLoader(dirs);
                        
                    );
             catch (java.security.PrivilegedActionException e) 
                throw (IOException) e.getException();
            
        


        /*
         * Creates a new ExtClassLoader for the specified directories.
         */
        public ExtClassLoader(File[] dirs) throws IOException 
            super(getExtURLs(dirs), null, factory);
           
        
        
 

我们需要注意的是

ClassLoader extcl;
        
extcl <

以上是关于一看你就懂,超详细java中的ClassLoader详解的主要内容,如果未能解决你的问题,请参考以下文章

一看你就懂,超详细 java 中的 ClassLoader 详解

在java语言中Object如何成为超类?看完你就懂了

根据jdk1.8源码整理而得,java集合体系(继承实现关系)图解,超清晰,一看就懂,方便记忆

Django-超简单的表单数据验证,一看就懂

❤️Java图文深入解析 继承多态接口(超详细,小白一看就会)❤️

报错注入是什么?一看你就明白了。报错注入原理+步骤+实战案例