java虚拟机系列:深入理解Java类加载机制

发布时间:2022-07-01 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了java虚拟机系列:深入理解Java类加载机制脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。

大纲

  • 前言
  • 类加载机制
  • 类加载器
    • 双亲委派机制
    • 为什么要使用双亲委派机制?
  • 分析ClassLoader
    • loadClass()
    • findClass()
      • defineClass(String name, byte[] b, int off, int len)
    • resolveClass(Class<?> c)
  • 自定义类加载器
    • 通过继承URLClassLoader来实现自定义类加载器
  • URLClassLoader
    • findClass()
  • launcher
    • getExtClassLoader()
    • createExtClassLoader()

前言

在我上一篇 类文件结构(java虚拟机系列:一文明解 .class 文件)博客中详细介绍了类文件(.class)如何为java语言的跨平台性发挥作用和它的内部结构,而类文件需要加载到虚拟机中,才能被运行,这篇文章将会详细解说虚拟机的类加载机制

类加载机制

所谓的类加载,就是把.class的二进制文件(不一定是文件,也可以通过网络二进制字节流)加载到内存中,形成一个可以直接使用的java类型(Class对象),虚拟机的类加载过程通常包括以下七个阶段

java虚拟机系列:深入理解Java类加载机制

  • 加载:在加载阶段,JVM通过一个类的全限定名称来获取二进制字节流,最后在内存里生成一个代表该类的Class对象。加载阶段是程序员最能掌控的一个阶段,因为并没有限定要通过何种途径来获取二进制流,所以我们可以通过自定义的类加载器,通过网络字节流传输等多种途径去获取二进制流。

  • 验证: 加载与连接是交叉进行的,比如某些验证字节码文件格式的操作。验证阶段主要是确保Class文件里面的信息符合规范,不会影响到虚拟机自身的安全

  • 准备: 这个阶段主要为类变量(静态变量)分配内存并初始化赋值。注意,这些类变量使用的内存在方法取,而方法区只是一个逻辑上的说法,在jdk7的方法区表现为永久代,而在jdk8使用了元空间的概念,所以类变量是随着Class对象放在堆里面。

    public static int a = 199;
    @H_590_126@//在准备阶段,赋给a的初始值是0而不是199
    //只有当存放在<clinIT>()的putstatic指令被执行后才会被赋值为199,而<clinit>类构造方法被执行是在初始化阶段才被执行
    
     public static final int b = 199;
    //而类变量b是由final修饰的,所以它的值199被存放于ConstantValue属性(被其对应的字段表引用)中,而ConstantValue会指向它对应的常量池之中的字面量,所以在准备阶段就直接对b赋值
    
  • 解析:解析阶段就是把符号引用转换为直接引用,符号就是用任意的字面量来描述引用的目标,而直接引用是可以直接指向目标的指针、相对偏移量或者是能间接定位到目标的句柄。直接引用直接对应着虚拟机的真实内存布局。《java虚拟机规范》中只说明在执行 checkcast、getfield、getstatic、instanceof、invokedynamic方法等17个用于操作符号引用的字节码之前,先对符号引用进行解析,所以虚拟机既可以在类加载阶段就对常量池里面的符号引用进行解析,也可以等到一个符号引用将要被使用之前就对其解析。

  • **初始化:**初始化阶段就是执行类构造器()的过程,而()是javac编译是自动生成的,它搜集了类中对类变量赋值的动作和static{}静态代码块的语句,如果一个普通的类没有静态变量赋值动作也没有静态代码块,()是不存在的,java虚拟机会保证在子类的()方法执行前先执行父类的()方法,而无需显式调用。

类加载器

​ 类加载器的任务是把二进制的字节流转换为内存中的Class对象。每个Class对象都包含着对它的类加载器的引用。对于任意一个类,都必须由它的类和加载它的类加载器共同确立在java虚拟机中的唯一性。

​ 在Java虚拟机的角度来说,类加载器分为两种,一种是虚拟机自身的启动类加载器,使用C++编写的,而另一种称为其它类加载器,使用java语言编写,都继承了ClassLoader类

  • Bootstrap Class Loader(引导类加载器),是虚拟机自身的加载器。这个加载器负责加载存放在<JAVA_HOME>lib目录或者被-Xbootclasspath参数所指定的路径存放jar,而且必须是符合的类库(如 rt.jar、tools.jar)
  • Extension Class Loader(扩展类加载器),由java语言编写,它负责加载<JAVA_HOME>libext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
  • Application Class Loader(应用程序类加载器),它负责加载用户路(ClassPath)上所有的类库

双亲委派机制

​ 双亲委派机制的其实就是当子类加载器收到了对加载类的请求的时候,它本身先不去加载,而是把加载的请求传递给它的父类加载器,因此所有的加载请求都会传递到最顶层的启动类加载器,然后父类加载器会尝试去加载该类,如果加载不了则再由子类去加载。

java虚拟机系列:深入理解Java类加载机制

为什么要使用双亲委派机制?

有一个显而易见的好处就是java中的类和它的类加载器一起形成了具备优先级层次的关系。比如遵循双亲委派机制,可以确保在任意的加载器的环境中Object都是同一个类,因为它最终是由引导类加载器去加载的,而如果不遵循的话,用户可以自定义一个 java.lang.Object类在classpath下加载,会造成Object类不唯一,造成应用程序的混乱。

分析ClassLoader

loadClass()

我们可以通过以下这种方式去使一个类进行加载,并获取到它的Class对象

public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader SystemClassLoader = ClassLoader.getSystemClassLoader();
        Class<?> aClass = systemClassLoader.loadClass("com.test.JVM.JVMTest");
    }

然后我们再来看看loadClass()方法,因为系统类加载器(也就是应用程序类加载器)继承了ClassLoader,所以也继承了ClassLoader的loadClass()方法

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
//下面这段简短的代码遵循了双亲委派机制
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) {
                    //委托给父类的loadClass()方法
                    c = parent.loadClass(name, false);
                } else {
                    //如果parent为null,则证明它的父类加载器
                    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还是null,则证明父类没有去加载,所以当前的类加载器就去加载
                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;
    }
}

findClass()

我们在loadClass()方法看到,如果父类加载不了,那么当前的类加载器就调用该findClass()方法去加载。官方建议自定义的类加载器都去重写这一方法而不是loadClass(),因为loadClass()方法是用来实现双亲委派机制的,而如果重写它可能会破坏双亲委派机制

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

defineClass(String name, byte[] b, int off, int len)

这个方法通常用在findClass()方法里,用于传入对应类的全限定名称加上一个字节数组和它的起始位置和长度,就能得到一个Class对象。如果直接调用此方法,得到的Class对象是未经过解析的,因为它没有调用resolveClass(),要具体等到一个符号被引用之前才去解析它。

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassForMATError
{
    return defineClass(name, b, off, len, null);
}

resolveClass(Class<?> c)

类加载器可能(也只是可能,因为上面有说到解析阶段只要保证符号引用被使用前执行就行,可能在类加载阶段就解析,也可能到用的时候才解析)会使用此方法来连接特定的类(解析阶段,把符号引用变成虚拟机内存中的直接引用)。

自定义类加载器

当我们想获取D盘或者网络中的.class文件并把它们加载到内存中或者需要Class的解密加密器时,可以自定义类加载器来完成,通常,我们需要继承ClassLoader并重写findClass()方法即可,如下图

public class MyClassLoader extends ClassLoader{
    private File classPathFile;

    public MyClassLoader(){
        //为了方便,这里就直接用了classpath的路径,也可以换成其它盘的路径
        String path = MyClassLoader.class.getResource("").getPath();
        this.classPathFile = new File(path);
    }


    @override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classname = MyClassLoader.class.getPackage().getName()+"."+ name;
        if (classPathFile != null){
            File classFile = new File(classPathFile, name.replaceAll("\.", "/") + ".class");
            if (classFile.exists()){
                FileinputStream in = null;
                ByteArrayOutputStream out = null;
                try {
                    in = new FileInputStream(classFile);
                    out = new ByteArrayOutputStream();
                    byte[] buff = new byte[1024];
                    int len;
                    while ((len = in.read(buff)) != -1){
                        out.write(buff,0,len);
                    }
                    //通过defineClass()加载出了Class对象
                    return defineClass(className,out.toByteArray(),0,out.size());
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                }finally {
                    if (null != in) {
                        try {
                            in.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if (out != null){
                        try {
                            out.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return null;
    }
}

​ 自定义的类加载器的父加载器是AppClassLoader(如下图),也就是应用程序(系统类)加载器,所以我们自定义的类加载器如果像上面一样没有重写load()方法的话也是会遵循双亲委派机制的

System.out.println(new MyClassLoader().getParent());

[外链图片转存失败,站可能有防盗链机制,建议将图片保存下来直接上传(img-Lv0sukXR-1635834649825)(java虚拟机系列:深入理解Java类加载机制.assets/image-20211102095820220.png)]

通过继承URLClassLoader来实现自定义类加载器

如果通过去继承ClassLoader来创造自定义的类加载器的话,需要重写findClass()方法中去定位资源的代码或者获取字节流的代码,较为烦杂。所以如果不是有什么复杂的需求,可以通过继承URLClassLoader来降低编码量

下面自定义了一个SelfClassLoader然后继承了URLClassLoader

public class SelfClassLoader extends URLClassLoader {

    public SelfClassLoader(URL[] urls) {
        super(urls);
    }
}

通过下面的方法加载D盘下的类

public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //指定了在哪个文件下
    File file = new File("D://");
    SelfClassLoader selfClassLoader = new SelfClassLoader(new URL[]{file.toURI().toURL()});
    //这里需要指定类的全限定名称
    Class<?> aClass = selfClassLoader.loadClass("com.test.Test");
    Object o = aClass.getConstructor().newInstance();
    System.out.println(o);
}

java虚拟机系列:深入理解Java类加载机制

URLClassLoader

先看看它的继承关系

java虚拟机系列:深入理解Java类加载机制

首先我们看看自定义的类加载器继承了URLClassLoader后要怎么用

//像这样,我们继承了URLClassLoader并且写了三个构造方法,并且分别执行了父类的三个构造方法
public class SelfClassLoader extends URLClassLoader {
    public SelfClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public SelfClassLoader(URL[] urls) {
        super(urls);
    }

    public SelfClassLoader(URL[] urls, ClassLoader parent, URLStreAMHandlerFactory factory) {
        super(urls, parent, factory);
    }
}

然后我们罗列以下URLClassLoader的三个构造方法

public URLClassLoader(URL[] urls, ClassLoader parent)
public URLClassLoader(URL[] urls)
public URLClassLoader(URL[] urls, ClassLoader parent,URLStreamHandlerFactory factory)
  • urls:指明了要加载的class文件的路径,因为是一个数组,所以可以包含多个路径
  • parent:指定了该类加载器的父类加载器,AppClassLoader也是通过这个去指定父类加载器的,稍后会说
  • factory:用来指定URLClassPath,这个URLClassPath是用来定位资源的

findClass()

因为URLClassLoader继承了ClassLoader,而它没有重写ClassLoader的loadClass()方法,所以还是遵循双亲委派机制,所以我们重点看一下findClass()方法,因为它是用来加载出Class对象的方法,如下图

protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    //把全限定名称转为路径
                    String path = name.replace('.', '/').concat(".class");
                    //通过URLClassPath定位到资源
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            //通过重载的defineClass方法来获得Class实例
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

至于重载的defineClass源码就不去分析了,如果大家有兴趣可以去看看

Launcher类

Launcher属于oracle的闭源代码,所以只能通过idea反编译出来,存在了奇怪的变量名。Launcher里封装了扩展类加载器和应用程序加载器,所以Launcher实现对以上两个加载器的加载。下面我们来简单了解以下它。首先来看看Launcher的构造方法

public Launcher() {
    Launcher.ExtClassLoader VAR1;
    try {
        //获取了扩展类加载器
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        //获取了应用程序加载器,注意这里把扩展类加载器的引用传入了
        //可以推测这里是把AppClassLoader的父类加载器设定为ExtClassLoader
        //所以子父类加载器不是通过继承来决定的,而是通过聚合的方式
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    //设置线程上下文加载器为AppClassLoader
    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    if (var2 != null) {
        SecurityManager var3 = null;
        if (!"".equals(var2) && !"default".equals(var2)) {
            try {
                var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
            } catch (IllegalAccessException var5) {
            } catch (InstantiationException var6) {
            } catch (ClassNotFoundException var7) {
            } catch (ClassCastException var8) {
            }
        } else {
            var3 = new SecurityManager();
        }

        if (var3 == null) {
            throw new InternalError("Could not create SecurityManager: " + var2);
        }

        System.setSecurityManager(var3);
    }

}

我们再来看看被封装的AppClassLoader

static class AppClassLoader extends URLClassLoader {
	......
}

我们发现它继承了URLClassLoader,其实ExtClassLoader也继承了URLClassLoader,如下继承图

java虚拟机系列:深入理解Java类加载机制

所以就很好理解了,AppClassLoader是通过URLClassLoader的构造方法传入parent参数来确定他父类构造器是扩展类加载器的。我们再来看看扩展类或者应用程序类加载器是如何向URLClassLoader注册它们要加载的包的路径的

getExtClassLoader()

我们先从扩展类加载器是如何获取的开始,如下代码

static class ExtClassLoader extends URLClassLoader {
    private static volatile Launcher.ExtClassLoader instance;

    //使用了单例模式,确保扩展类加载器只能有一个实例
    //这里具体使用了双重检查加synchronized,确保了只能有一个线程对扩展类加载器实例化
    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;
    }
    ......省略其它代码
}

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]);
                }
			   //在这里调用了ExtClassLoader构造方法
                //这里传入的var1参数是后来传给它父类(URLClassLoader)构造方法的urls参数(文件路径)
                return new Launcher.ExtClassLoader(var1);
            }
        });
    } catch (PrivilegedActionException var1) {
        throw (IOException)var1.getException();
    }
}

我们再点进getExtDirs()看看它是如何获取ExtClassLoader要加载类的文件目录

private static File[] getExtDirs() {
    String var0 = System.getProperty("java.ext.dirs");
    ...省略代码
}

然后我们发现它是通过System.getProperty()方法获取的,我们测试一下该方法

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

得到结果

@H_293_3042@

这个路径正是扩展类加载器允许加载的类的所在路径。然后回到createExtClassLoader(),点进Launcher.ExtClassLoader(var1)方法

public ExtClassLoader(File[] var1) throws IOException {
    //调用了扩展类加载器父类构造器(URLClassLoader)方法,指定了要加载的Class的路径
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
    SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

脚本宝典总结

以上是脚本宝典为你收集整理的java虚拟机系列:深入理解Java类加载机制全部内容,希望文章能够帮你解决java虚拟机系列:深入理解Java类加载机制所遇到的问题。

如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。