SLK的个人博客

  • 首页

  • 搜索
LRU Stack ThreadLocal Nacos RejectedExecutionHandler Executor RocketMQ ConcurrentHashMap CyclicBarrier Semaphore CountDownLatch canal Unsafe Atomic BlockingQueue AQS ReentrantLock Synchronized MESI Volatile JMM BufferPool Explain MySQL 常量池 Arthas JVM调优 三色标记 CMS ParNew 垃圾收集器 G1 Java Redis Android HTTPDNS DNS ioc 爬虫 seleniumhq 推荐引擎 Mahout IM Netty Vert.x HashMap 梯子 翻墙 V2ray Docker 搜索引擎 SpringBoot elasticsearch

Java基础 JDK类加载机制

发表于 2020-06-26 | 分类于 Java | 0 | 阅读次数 206

类加载运行全过程

当我们用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
# LRU # Stack # ThreadLocal # Nacos # RejectedExecutionHandler # Executor # RocketMQ # ConcurrentHashMap # CyclicBarrier # Semaphore # CountDownLatch # canal # Unsafe # Atomic # BlockingQueue # AQS # ReentrantLock # Synchronized # MESI # Volatile # JMM # BufferPool # Explain # MySQL # 常量池 # Arthas # JVM调优 # 三色标记 # CMS # ParNew # 垃圾收集器 # G1 # Java # Redis # Android # HTTPDNS # DNS # ioc # 爬虫 # seleniumhq # 推荐引擎 # Mahout # IM # Netty # Vert.x # HashMap # 梯子 # 翻墙 # V2ray # Docker # 搜索引擎 # SpringBoot # elasticsearch
Redis 缓存设计与性能优化
Java基础 JVM内存模型
  • 文章目录
  • 站点概览
宋龙宽

宋龙宽

87 日志
13 分类
53 标签
RSS
Github E-mail
Creative Commons
Links
  • 黑客派
  • Relyn
  • 张小妞的博客
  • ElasticSearch教程
© 2021 宋龙宽
由 Halo 强力驱动
|
主题 - NexT.Gemini v5.1.4