闲谈JVM(五):浅谈ClassLoader

前言

在前几篇中,

闲谈JVM(四):浅谈CodeCache与JIT

闲谈JVM(三):浅析本地元空间参数配置

闲谈JVM(二):浅析新老生代参数配置

闲谈JVM(一):浅析JVM Heap参数配置

我们对JVM最主要区域的常用参数的配置进行了介绍了解,本篇,我们抛开JVM配置相关,来聊一聊JVM的类加载机制。

关于JVM类加载机制概念详细介绍,可以参考:

深入理解JVM虚拟机:(五)虚拟机类加载机制(上)

深入理解JVM虚拟机:(六)虚拟机类加载机制(下)

ClassLoader

JVM把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制。

ClassLoader体系

img

当启动一个JVM时,bootstrap 类加载器就会加载java的核心类,例如:rt.jar中的类。bootstrap 类加载器是其他类加载器的parent,它是唯一一个没有parent的类加载器。

接下来是extension 类加载器,它以bootstrap 类加载器作为parent,它用来从Java系统变量java.ext.dir中的jar包中加载类的。

第三个,也是最重要的一个就是开发者使用的system classpath 类加载器 。它是extension 类加载器 的child,它用来从Java系统变量java.class.path下面加载类,可以通过 -classpath 来指定这个位置。

注意类加载器的体系并不是“继承”体系,而是一个“委派”体系。大多数类加载器首先会到自己的parent中查找类或者资源,如果找不到,才会在自己的本地进行查找。事实上,类加载器被定义加载哪些在parent中无法加载到的类,这样在较高层级的类加载器上的类型能够被“赋值”为较低类加载器加载的类型。

类加载器的委托行为动机是为了避免相同的类被加载多次。回到1995年,Java的主要方向被放在Applet上,那时候网络带宽优先,所以程序中的类直到用时才会被加载。但是事实上,Java在服务器端展示了强劲的能力,但是服务器端要求类加载器能够反转委派原则,也就是先加载本地的类,如果加载不到,再到parent中加载。

ClassLoader在加载类时的概念区分

在我们日常开发中,一般来说,加载一个类可以使用两种方式:

1、Class.forName 常见于JDBC加载驱动

2、ClassLoader.loadClass 加载指定路径的Class

Class.forName和ClassLoader.loadClass都可以用来进行类型加载,而在Java进行类型加载的时刻,一般会有多个ClassLoader可以使用,并可以使用多种方式进行类型加载。

比如如下代码:

class A {
	public void m() {
		A.class.getClassLoader().loadClass(“B”);
	}
}

在A.class.getClassLoader().loadClass(“B”);代码执行B的加载过程时,一般会有三个概念上的ClassLoader提供使用。

1、CurrentClassLoader,称之为当前类加载器,简称CCL,在代码中对应的就是类型A的类加载器;

2、SpecificClassLoader,称之为指定类加载器,简称SCL,在代码中对应的是A.class.getClassLoader(),如果使用任意的ClassLoader进行加载,这个ClassLoader都可以称之为SCL;

3、ThreadContextClassLoader,称之为线程上下文类加载器,简称TCCL,每个线程都会拥有一个ClassLoader引用,而且可以通过Thread.currentThread().setContextClassLoader(ClassLoader classLoader)进行切换。

4、SCL和TCCL可以理解为在代码中使用ClassLoader的引用进行类加载,而CCL却无法获取到其引用,虽然在代码中CCL == A.class.getClassLoader() == SCL

5、CCL的加载过程是由JVM运行时来控制的,是无法通过Java编程来更改的。

ClassLoader常见问题

ClassLoader在Java语言中占据了核心地位,Java应用服务器,OSGi,以及大量的网络框架,它们大多数都用到了ClassLoader。如果在使用过程中出现了类加载错误,我们该如何解决,是非常的关键。

NoClassDefFoundError

NoClassDefFoundError是在开发JavaEE程序中常见的一种问题。该问题会随着你所使用的JavaEE中间件环境的复杂度以及应用本身的体量变得更加复杂,尤其是现在的JavaEE服务器具有大量的类加载器。

在JavaDoc中对NoClassDefFoundError的产生是由于JVM或者类加载器实例尝试加载类型的定义,但是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是能够被找到的,但是在执行时却没有找到。

这一刻IDE是没有出错提醒的,但是在运行时却出现了错误。

这类问题的排查,我们一般可以从Maven入手,检查POM中的依赖引用关系,亦或者可以通过JVM的类加载层面进行排查。

在JVM启动参数中,加入如下参数:

-verbose:class

加入该参数后,可以在JVM启动阶段,将加载的类信息全部打印出来,我们来看一下效果。

[root@VM_0_2_centos jmvtest]# java -verbose:class HelloWorld

[Opened /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Object from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.io.Serializable from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Comparable from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.CharSequence from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.String from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.reflect.Type from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Class from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Cloneable from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ClassLoader from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.System from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Throwable from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Error from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ThreadDeath from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Exception from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.RuntimeException from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.SecurityManager from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.security.ProtectionDomain from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.security.AccessControlContext from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.security.SecureClassLoader from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ReflectiveOperationException from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ClassNotFoundException from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.LinkageError from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.NoClassDefFoundError from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ClassCastException from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ArrayStoreException from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.VirtualMachineError from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.OutOfMemoryError from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.StackOverflowError from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.IllegalMonitorStateException from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.Reference from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.SoftReference from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.WeakReference from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.FinalReference from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.PhantomReference from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded sun.misc.Cleaner from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.Finalizer from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ref.ReferenceQueue from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Runnable from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Thread from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.Thread$UncaughtExceptionHandler from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.ThreadGroup from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.util.Map from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.util.Dictionary from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.util.Hashtable from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.util.Properties from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.reflect.AccessibleObject from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]
[Loaded java.lang.reflect.Member from /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.252.b09-2.el7_8.x86_64/jre/lib/rt.jar]

NoSuchMethodError

在另一个场景中,我们可能遇到了另一个错误,也就是NoSuchMethodError。

NoSuchMethodError代表这个类型确实存在,但是一个不正确的版本被加载了。为了解决这个问题我们同样可以可以使用 -verbose:class 来判断该JVM加载的到底是哪个版本。

往往出现这个报错是在Spring框架中,由于Spring框架版本的错综复杂,互相引用依赖,经常会出现Spring A包的X版本引用了Spring B包的Y版本,导致找不到方法,因此报错。

ClassCastException

NoClassDefFoundErrorNoSuchMethodError是两个在 JavaEE 环境中经常出现的问题,这些问题需要我们了解问题的本质,才能够被从容的处理。

下面我们看一下ClassCastException,在一个类加载器的情况下,一般出现这种错误都会是在转型操作时,比如:

A a = (A) method();,很容易判断出来method()方法返回的类型不是类型A,但是在 JavaEE 多个类加载器的环境下就会出现一些难以定位的情况。

我们来看一下这个场景:

public class ClassLoaderClassCastException {

    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader myClassLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        ClassLoaderClassCastException classLoaderClassCastException = new ClassLoaderClassCastException();
        System.out.println(classLoaderClassCastException.getClass().getClassLoader().toString());
        ClassLoaderClassCastException object1 = (ClassLoaderClassCastException) myClassLoader.loadClass("com.xuangy.classloader.ClassLoaderClassCastException").newInstance();
    }
}

执行结果:

sun.misc.Launcher$AppClassLoader@b4aac2
Exception in thread "main" java.lang.ClassCastException: com.xuangy.classloader.ClassLoaderClassCastException cannot be cast to com.xuangy.classloader.ClassLoaderClassCastException
    at com.xuangy.classloader.ClassLoaderClassCastException.main(ClassLoaderClassCastException.java:35)

在上面的例子中,我们自行实现了一个ClassLoader,去加载**ClassLoaderClassCastException.java,然后将其强转成ClassLoaderClassCastException,**但是却抛出了java.lang.ClassCastException,明明是两个一样的Class,为何会抛出强转失败的异常呢?

这里就会引出一个问题,在JVM中,如何确定一个类型实例?答:全类名吗?

不是,是类加载器加上全类名。在JVM中,类型被定义在一个叫 SystemDictionary 的数据结构中,该数据结构接受类加载器和全类名作为参数,返回类型实例。

JVM ClassLoader

类型加载时,需要传入类加载器和需要加载的全类名,如果在 SystemDictionary 中能够命中一条记录,则返回 class 列上对应的类型实例引用,如果无法命中记录,则会调用loader.loadClass(name);进行类型加载。
这里不会更加深入的介绍 SystemDictionary 如何进行类型加载的过程,而是需要指出 JVM中确定一个类型的坐标是通过类加载器和全类名做到的。

再回头看上面的示例,ClassLoaderClassCastException是由AppClassLoader进行加载的,而我们进行强转的ClassLoaderClassCastException是由我们自己定义的ClassLoader进行加载的,这两个ClassLoaderClassCastException虽然是同一个Class,但是在JVM中,它们其实是两个不同的类型,因此无法进行相互强转。

在传统的双亲委派模型下,这种 ClassCastException 是不会发生的,因为它的加载顺序杜绝了出现这种问题的可能,而在 JavaEE 环境下,每个资源模块(比如一个war包)都优先使用自身的资源,正因为突破了双亲委派模型, 奇怪的问题就发生了。

结语

本篇,我们对JVM的ClassLoader实战部分进行了了解,在日常业务开发中,可能与ClassLoader打交道的地方并不多见,但是对于中间件领域开发,ClassLoader却是至关重要的,理解ClassLoader机制也是非常重要的,本篇只是对常见的问题进行了介绍,更多关于ClassLoader的内容,还望您自行深入理解。



更多精彩文章, 请关注我的个人公众号:老宣说
让我们一起共同学习成长!
感谢您的支持!
老宣说

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值