JVM基础之类加载器详解

目录

  • 开始
    • 谈谈你对Java的理解
    • Java 和 C++的区别
    • Java平台无关性是如何实现的?
    • 为什么JVM不直接将源码解析成机器码?
    • JVM的主要组成部分
  • 前置知识-反射
    • 谈谈什么是反射?
    • 用过反射吗?
  • ClassLoader
    • ClassLoader的种类
    • 自定义ClassLoader的实现
    • 类加载器的双亲委派机制
      • 源码分析
    • 为什么要使用双亲委派机制去加载类
    • 类的加载方式
    • 类的装载过程
    • loadClass和forName的区别

开始

本文我整理了一些JVM类加载器相关的面试高频知识点,方便同学们复习。

在详细了解JVM知识点之前,我们先引入以下基础的经典面试题:

谈谈你对Java的理解

  • 平台无关性,一次编译到处运行。

  • GC垃圾回收机制:Java无需像C++一样手动管理内存。

  • Java的语言特性:泛型、反射、lambda表达式等。

  • 面向对象:包括封装、继承、多态。

  • Java自身类库如:集合、并发库、网络库以及IO流。

  • 异常处理。

Java 和 C++的区别

  • Java不提供指针直接访问内存,程序的内存更加安全。
  • Java的类是单继承,而C++的类是多继承(Java的接口是可以多继承的)。
  • Java有自动垃圾回收机制,无需手动释放内存。
  • Java可实现平台无关性,一次编译,不同系统平台都能运行。

Java平台无关性是如何实现的?

Java源码首先被编译成字节码,再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要重新编译,Java虚拟机在执行字节码的时候会将字节码转换成具体平台上的机器指令。所以Java也是一种编译与解释并存的语言。它即具备编译型语言的特征,也具备解释型语言的特征。

引用维基百科的介绍:

为了改善编译语言的效率而发展出的即时编译技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成字节码。到执行期时,再将字节码直译,之后执行。JavaLLVM是这种技术的代表产物。

为什么JVM不直接将源码解析成机器码?

  • 准备工作:每次执行前都需要进行各种检查。
  • 兼容性:这样做使得其他语言也能够按照JVM的标准解析成字节码。

JVM的主要组成部分

  • Class Loader:依据特定格式,加载class文件到内存。
  • Execution Engine:对命令进行解析。
  • Native Interface:融合不同开发语言的原生库为Java所用。
  • Runtime Data Area:JVM内存空间结构模型。

前置知识-反射

谈谈什么是反射?

Java反射机制是在运行状态中,对于任意一个类,都能知道这个类的所有属性和方法;对于任意一个对象,都能调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

用过反射吗?

Java反射的三种方法:

  • 调用对象的getClass()方法。
  • Class.forName("xxx.xxx.xxx")。
  • 类名.class。

常用方法:

  • newInstance():创建对象实例。
  • getMethod():可以获取所有public的方法(包含继承和实现的方法)。
  • getDeclaredMethod():获取指定对象的方法(不包含继承和实现方法)。
  • setAccessible():传入true/false,当设为true时可访问私有方法。
  • Class.getField():获取类中字段属性。
  • Invoke():可以用于执行指定的方法。

ClassLoader

ClassLoader在Java中有着非常重要的作用,它主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class的二进制数据流。它是Java的核心组件,所有的Class都是由ClassLoader进行加载的。ClassLoader负责将Class文件里的二进制数据流装载进系统,然后交由Java虚拟机进行连接,初始化等操作。

ClassLoader的种类

  • BootStrapClassLoader:它由C++编写,加载核心库java.*。
  • ExtClassLoader:Java编写,加载扩展库Javax.*。
  • AppClassLoader:Java编写,加载程序所在目录。
  • 自定义ClassLoader:Java编写,定制化加载。

自定义ClassLoader的实现

自定义ClassLoader的实现有两个关键函数:

  • findClass()
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
  • defineClass()
    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
    {
        return defineClass(name, b, off, len, null);
    }

类加载器的双亲委派机制

双亲委派机制

在加载一个类时,会通过递归的方法,自底向上,首先看自定义类加载器中是否已经加载过相关类,若没有加载过,则调用父类加载器App ClassLoader的loadClass()方法看是否加载过,若还是没有就继续调用父类加载器Extension ClassLoader看是否加载过,直到检查到BootStrap ClassLoader。若都没加载过,就考虑自己是否具备加载的条件,如果不能加载,就自顶向下交给子类加载器进行加载。若还是无法加载,则抛出ClassNotFoundExcepiton异常。

源码分析

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // 类加载是线程安全的
        synchronized (getClassLoadingLock(name)) {
            // 首先,检查类是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 递归调用检查父类加载器
                        c = parent.loadClass(name, false);
                    } else {
                        // 查找Bootstrap ClassLoader中是否加载了有关类
                        // 其中调用了findBootstrapClass 为native方法
                        // 因为Bootstrap类加载器是c++中实现的
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    //如果依然没找到,那么就调用自定义类加载器的findClass进行查找
                    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;
        }
    }

为什么要使用双亲委派机制去加载类

答:使用双亲委派机制能够避免多份同样的字节码重复加载。双亲委派机制能够保证同一份字节码文件只会被加载一次,并且可以保证一些核心的类不会被篡改替换,保证安全性。

类的加载方式

  • 隐式加载:new,new后的对象实例无需再调用newInstance()方法来获取对象实例,new支持带参数的构造器生成对象实例。
  • 显式加载:loadClassforName。显式加载得到class对象后都需要调用newInstance()方法来获取对象实例。

类的装载过程

  1. 加载

    • 通过ClassLoader加载class文件字节码,生成Class对象。
  2. 链接

    • 校验:检查加载的class的正确性和安全性。
    • 准备:为类变量分配存储空间并设置类变量的初始值。
    • 解析:JVM将常量池内的符号引用转换为直接引用。
  3. 初始化

    • 执行类变量赋值和静态代码块。

loadClass和forName的区别

  • Class.forName()得到的class是已经初始化完成的。
  • ClassLoader.loadClass()得到的class是还没有链接的。

Class.forName()代码实现:

    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        // 第二个参数被设为true,这个类将被初始化
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

我们在实际场景中两种方法都会使用到。

例如在使用jdbc的mysql驱动时,我们需要使用Class.forName()方法来指定Driver。

Class.forName("com.mysql.jdbc.Driver");

而在Spring IoC中,要读取一些Bean的配置文件时,如果以classpath方式来加载,则需要使用ClassLoader.loadClass()方式进行加载。这与Spring IoC容器的懒加载方式有关。Spring IoC为了加快初始化速度,大量使用懒加载方式来加速。而ClassLoader.loadClass()方法则可以对类进行延迟初始化,从而提升Spring的启动速度。

JavaJVM
2025 © Yeliheng的技术小站 版权所有