类加载运行全过程

当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM。

package cn.pencilso.study.classloader;
public class TestRun {
    private static int int1 = 1;
    private static final int int2 = 2;
    private static User user = new User();

    public int add() {
        return int1 + int2;
    }

    public static void main(String[] args) throws ClassNotFoundException {
        TestRun testRun = new TestRun();
        int add = testRun.add();
        System.out.println("add-> "+add);
    }
}

通过Java命令执行代码的大体流程如下:

classloader (1)

其中classloader.loadClass的类加载过程有如下几步:

加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载

  • 加载 :读入class字节码文件,使用到类时才会加载,例如调用类的静态方法,new对象等。

  • 验证:校验字节码文件的正确性。

  • 准备:对静态变量分配内存,并赋予默认值,例如 int1的默认值是0,而user是null,至于int2则是2,因为int2 使用了final修饰词,所以int2是常量,不再是静态变量。

  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main() 方法)替换为指向数据所存内存的指针或句柄等 (直接引用) ,这是所谓的静态链接过程 (类加载期间完成)。

    动态链接是在程序运行期间完成的将符号引用替换为直接引用。

  • 初始化:对类的静态变量初始化为指定的值,int1赋值为1 ,user 则会加载User类,创建User实例后赋值给user, 然后执行静态代码块。

主类在运行过程中如果使用到其他类,会逐步加载这些类。

jar包或war包里的类不是一次性全部加载的,是使用到时才加载。

类加载器和双亲委派机制

Java中有多种类加载器

- 引导类加载器:负责加载jre/lib目录下的jvm的核心类库,rt.jar、charsets.jar 等。
- 扩展类加载器:负责加载jre/ext目录下的jvm扩展类库。
- 应用程序类加载器:负责加载ClassPath路径下的类,主要就是加载自己编写的那些类。
- 自定义加载器:负责加载指定目录下的类。

注意,引导类加载器是由C++所创建的,所以在Java代码中是获取不到引导类加载器的对象。

查看对应的几种类加载器

   public static void main(String[] args) {
        ClassLoader stringClassLoader = String.class.getClassLoader();
        ClassLoader deskeyFactoryClassloader = DESKeyFactory.class.getClassLoader();
        ClassLoader testJdkClassloader = TestJDKClassloader.class.getClassLoader();

        System.out.println("stringClassLoader-> "+stringClassLoader);
        System.out.println("fileClassloader-> "+deskeyFactoryClassloader);
        System.out.println("testJdkClassloader-> "+testJdkClassloader);
    }

-----------
stringClassLoader-> null
fileClassloader-> sun.misc.Launcher$ExtClassLoader@1b28cdfa
testJdkClassloader-> sun.misc.Launcher$AppClassLoader@18b4aac2
  
// String是Java核心类库,是由C++所编写的引导式加载器 bootstrapLoader,所以获取到的是null
// DESKeyFactory 是Java的扩展类库,所以获取到的是 ExtClassLoader
// testJdkClassloader 是自己编写的文件,所以类加载器是AppClassLoader

在创建Launcher时就创建好了ExtClassLoader、AppClassLoader两种加载器。

查看源码Launcher,分析ExtClassLoader、AppClassLoader是如何创建的。

 // 单例 Launcher
  private static Launcher launcher = new 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);
        }
      ......
    }
ExtClassLoader 创建过程
// 1、单例模式 ,在createExtClassLoader方法进行创建

        public static ExtClassLoader getExtClassLoader() throws IOException
        {
            if (instance == null) {
                synchronized(ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }
            return instance;
        }

// 2、可以看到new了一个ExtClassLoader,并且把扩展类库的路径传给了构造参数
        private static ExtClassLoader createExtClassLoader() throws IOException {
            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 {
                            final File[] dirs = getExtDirs();
                            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();
            }
        }

// 3、调用了supper方法,将类库路径传给了supper构造的参数中,并且将parent参数传递的是null,至此extClassLoader则创建出来了
        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
            SharedSecrets.getJavaNetAccess().
                getURLClassPath(this).initLookupCache(this);
        }
AppClassLoader 创建过程
// 1、获取classpath下的类路径,并且new了一个AppClassLoader,将路径与扩展库加载器传递到构造参数。
        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);
                }
            });
        }

// 2、AppClassLoader构造方法,将类路径数组与扩展库类加载器传到了supper
        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
            ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
            ucp.initLookupCache(this);
        }
// 3、最后跟踪supper构造方法,将parent 保存在了一个成员变量里面。而对于AppClassLoader来说
//		这个parent就是扩展库的ClassLoader
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        ....
    }
类加载器初始化过程

类运行加载过程中会创建JVM启动器实例sun.misc.Launcher 并且使用了单例设计模式。

在Launcher构造方法内部,创建了两个类加载器,分别是:

- ExtClassLoader 扩展类加载器
- AppClassLoader 应用加载器 并且其parent是ExtClassLoader扩展类加载器
- JVM默认使用Launcher的getClassLoader方法返回的类加载器实例,加载应用程序,而这个getClassLoader方法返回的就是AppClassLoader应用加载器。
双亲委派机制流程

每个类加载器都会维护一个已加载过的类数组,当需要加载某一个类时,类加载器执行顺序是:自定义加载器(如果有的话)-> 应用类加载器 -> 扩展类加载器 -> 引导类加载器

假设现在需要加载一个User.class类,会从自定义加载器开始,先检查自身已加载类列表是否存在,如果存在则直接返回,如果不存在,调用parent类加载器进行加载,如果parent已加载列表也没有,则调用parent的parent类加载器,如此层层向上委托,直到没有parent为止。

如果最顶级的parent在它自己的已加载列表中找不到,则在它的类加载路径寻找文件,如果还找不到,则层层往下退回加载请求,每一层都执行同样的操作,只要有一层类加载器找到了,就会放到该加载器的已加载列表,并且返回该类的信息,如果所有的类加载器都没有找到,则抛出ClassNotFoundException。

classloader-shuangqin

双亲委派机制源码实现

源码在ClassLoader.loadCLass

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 第一步,检查已经加载过的class,如果已加载过了,则直接return了。
        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) {
   
                // 如果所有的parent都加载不到的话,则去自己的类路径寻找文件
                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;
    }
}
为什么要设计双亲委派机制
  • 沙箱安全机制 : 自己写的核心内库不会被加载,这样可以防止核心API库被随意篡改。
  • 避免类的重复加载:当父加载器已经加载了该类时,就没有必要子CLassLoader再加载一次,保证被加载类的唯一性

尝试自定义String类如下,替换Java的核心类库String ,运行错误,提示找不到这个main方法。

原因String是Java核心类库,由引导类加载器最先加载的。在执行下列代码时,会委托给父加载器进行加载String,最后委托到引导类加载器的时候,会从引导类加载器返回回来,而引导类加载器所加载的String,是Java的核心类库,它的匹配规则是依据包名+类名的匹配规则来匹配。

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("---------My String ----------");
    }
}
运行结果
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
全盘负责委托机制

“全盘负责” 是指当一个ClassLoader装在一个类时,除非显示的使用另外一个CLassLoader,该类所依赖及引用的类也由这个CLassLoader载入。

自定义类加载器

自定义类加载器,加载Class文件

实现一个简单的类加载器,只需要继承CLassLoader,并重写findClass 方法即可。

先准备一个示例类 : User

package cn.pencilso.study.classloader;

public class User {
    
    public void sout(){
        System.out.println("-------this is User Class --------");
    }
}

对代码进行编译,编译之后class文件会在项目的target 目录下。

准备文件夹,将User这个类的class文件复制出来。

我把class文件放在了 "/Users/pencilso/Desktop/classloader/" + "cn/pencilso/study/classloader"

需要注意的是,后面的 "cn/pencilso/study/classloader" 对应的是这个类的包名。

pencilso@MacBook-Pro ~ % ls ~/Desktop/classloader/cn/pencilso/study/classloader
User.class

简单的类加载器实现

package cn.pencilso.study.classloader;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Method;

public class MyClassLoaderTest {
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("/Users/pencilso/Desktop/classloader/");
        Class<?> userCLass = myClassLoader.loadClass("cn.pencilso.study.classloader.User");
        Object userInstance = userCLass.newInstance();
        Method soutMethod = userCLass.getDeclaredMethod("sout", null);
        soutMethod.invoke(userInstance, null);

        System.out.println("classLoader:" + userCLass.getClassLoader().getClass());
    }


    @AllArgsConstructor
    @Data
    public static class MyClassLoader extends ClassLoader {
        /**
         * class 文件所在的文件夹
         */
        private String folder;

        /**
         * 从指定目录中寻找class文件
         *
         * @param name
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class<?> findClass(final String name) throws ClassNotFoundException {
            try {
                //将包名中的 "." 替换为文件路径分隔符
                String classPath = name.replace(".", File.separator);
                FileInputStream fileInputStream = new FileInputStream(folder + classPath + ".class");
                int len = fileInputStream.available();
                byte[] classByte = new byte[len];
                fileInputStream.read(classByte);
                fileInputStream.close();
                //解释class文件
                return defineClass(name, classByte, 0, classByte.length);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }

    }
}

运行后,结果如下,可以看到已经调用到了User对象里面的"sout"方法,输出了 "this is User Class" 。

但是classLoader为什么还是AppClassLoader?

这是因为双亲委托机制的结果,自定义ClassLoader加载器也会向上层级委托。

-------this is User Class --------
classLoader:class sun.misc.Launcher$AppClassLoader

源码分析:

我们的自定义类加载器是继承自CLassLoader,那么在初始化时会执行CLassLoader的构造方法,如下:

		// 1、ClassLoader的无参构造方法,调用了getSystemClassLoader,并且传入了二参构造    
		protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }

		// 2、getSystemClassLoader方法是获取一个CLassLoader对象,其初始化方法是 initSystemClassLoader
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }

		// 3、初始化SystemClassLoader
 		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;
              	// 调用了getClassLoader方法,而这个getClassLoader方法获取到的就是appClassLoader对象
                scl = l.getClassLoader();
              ......
     }
          
    // 4、查看二参的ClassLoader构造方法,可以得知,它最后将AppClassLoader对象存在了parent字段中。
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        ....
    }

通过上面源码分析,我们自定义的类加载器,加载User.class,执行双亲委派机制时,委托到了AppClassLoader,后又委托到ExtClassLoader,后又委托到 BootstrapLoader ,而 BootstrapLoader 是肯定找不到这个类的,ExtClassLoader也找不到,委托退到AppClassLoader的时候找到了。所以上面的代码运行输出的ClassLoader是 AppClassLoader。

即使我们的User.class文件在项目的target目录下,和我们自己创建的外部文件夹都有一份,但是只要有一个类加载器加载到了文件,其他的类加载器则不再会加载。而AppClassLoader的层级比自定义ClassLoader的层级要高 。

那怎样让它用自定义加载器加载User.class ?

接下来,把源码中的User.java 删除,编译、重新运行,这次的结果是自定义的CLassLoader了,因为这时候target目录下已经没有了User.class ,所以AppClassLoader找不到这个类,委托会退到自定义CLassLoader,而自定义CLassLoader会去我们自定义的文件夹下面去找这个类文件。

-------this is User Class --------
classLoader:class cn.pencilso.study.classloader.MyClassLoaderTest$MyClassLoader
打破双亲委派机制

我们得知在加载类文件时,需要经过双亲委派机制,层级向上委托,层级向下退回。

那么如何指定让自定义加载器直接加载,而不向上委托?

之前提到过双亲委派机制的源码实现在 ClassLoader.loadCLass 中,那么自定义类加载器继承后,在MyClassLoader类中,重写loadClass方法即可。

@Override
        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();
                    long t1 = System.nanoTime();
                    if (name.equals("cn.pencilso.study.classloader.User")) {
                        //当加载指定的类时,不委托给父加载器,自行加载。
                        c = findClass(name);
                    } else {
                        //默认委托给父加载器
                        c = getParent().loadClass(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;
            }
        }

接下来把源码中的User.java 文件添加回来,再次执行,最后输出的是我们自定义的类加载器了。

-------this is User Class --------
classLoader:class cn.pencilso.study.classloader.MyClassLoaderTest$MyClassLoader