JVM之再谈类加载

15 篇文章 0 订阅

总体上说,虚拟机把描述类的数据,从字节码文件加载到内存,然后进行数据校验、转换解析、初始化,最终形成可以被寻积极使用的Java类型,这就是虚拟机的类加载机制。

与C/C++在编译时,需要连接不同,Java语言里,类型的加载、连接和初始化都是在程序运行期间完成。虽然增加了性能开销,但有更高灵活性。

Java的动态扩展特性,依赖运行时的动态加载和动态连接

例如,面向接口编程,可以在运行时再指定实际的实现类;用户可以通过自定义类加载器,在运行时,从网络或其他地方加载一个二进制流,作为程序代码的一部分。

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,其生命周期,如下图

类的生命周期

加载、验证、准备、初始化、卸载,这5阶段的顺序是确定的。

解析阶段不一定,某些情况下,可以在初始化之后再开始。这是为了支持Java语言的运行时绑定

虚拟机规范严格规定,5种情况,必须立即类初始化:

  • 遇到new,getstatic,putstatic,invokestatic字节码指令,若类未曾初始化,则先初始化

  • 使用java.lang.reflect包的方法,反射调用类,若类未曾初始化,先初始化

  • 初始化一个类时,若父类未曾初始化过,先初始化父类
  • 虚拟机启动时,用户需要指定一个要执行的朱磊,虚拟机会先初始化主类
  • 使用JDK 1.7动态语言支持时,若java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且句柄对应的类未曾初始化过,则先初始化

加载(Loading)

加载是类加载(Class Loading)过程的一个阶段。

加载完成3件事情

  • 通过类的全限定名来获取定义此类的二进制字节流
  • 将这个二进制字节流的静态存储结构转化为方法区运行时数据结构
  • 在内存生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

HotSpot虚拟机的java.lang.Class对象,放在方法区Method Area,不是堆Heap

连接(Linking)

加载阶段与连接阶段的部分内容,交叉进行,加载阶段未完成,连接阶段可能已经开始

验证(Verification)

连接阶段的第一步,目的是为了确保class文件的字节流中包含的信息,符合当前虚拟机的要求,而且,不会危害虚拟机自身安全

大致完成4个阶段的检验

  • 文件格式验证
    • 是否符合字节码文件格式的规范,并能被当前版本的VM处理
    • 基于二进制流进行,后面的验证基于方法区的存储结构,不再直接操作字节流
    • 验证通过后才会将二进制流存入JVM内存的方法区
  • 元数据验证
    • 对字节码描述的信息进行语义分析,保证其符合规范
  • 字节码验证
    • 通过数据流和控制流分析,确定程序语义是合法、符合逻辑的
    • 第二阶段,对元数据的数据类型做完校验后,这个阶段将校验分析类的方法体
  • 符号引用验证
    • 发生在虚拟机将要把符号引用转化为直接引用的时候,即解析阶段中发生
    • 对类自身以外的信息进行匹配性校验
    • 目的是保证解析动作能正常执行

准备(Preparation)

正式为类变量分配内存,并设置类变量初始值的阶段,这些变量所使用的内存,都将在方法区中分配

即static修饰的变量,初始值为各个类型的0值

如果是final修饰的类变量,会直接生成ConstantValue属性。在准备阶段,虚拟机会根据ConstantValue为变量赋值

解析(Resolution)

虚拟机将常量池内的符号引用,转换为直接引用的过程

符号引用(Symbolic References)
:class文件结构规范定义了的引用

直接引用(Direct References)
直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 与虚拟机的内存布局相关

参看:JVM之类文件结构

分为:

  • 类或接口解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

初始化(Initialization)

类加载过程的最后一步,这一阶段,才真正开始执行类中定义的Java代码(或者说字节码)

准备阶段,变量已经被清零,有了初始值;初始化阶段,则根据我们定义的Java代码逻辑去初始化类变量和其他资源

从另外的角度表达,初始化阶段,是执行类构造器的<clinit>方法的过程

  • 虚拟机会保证一个类的方法在多线程环境中被正确的加锁、同步;如果多个线程同时去初始化一个类,那么只有会有一个线程执行方法,其他线程阻塞

类加载器

作用
类加载器实现 类的加载动作,同时用于确定一个类。
确定类的唯一性
对于任意一个类,都需要由加载它的 类加载器和这个 类本身一同确立其在Java虚拟机中的 唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就不相等

类加载器

从虚拟机的角度来讲,只存在2种不同的类加载器

  • 启动类加载器(Bootstrap ClassLoader)
    • C++实现(HotSpot而言),是虚拟机自身一部分
  • 所有其他加载器
    • Java实现,独立于虚拟机外
    • 全继承自java.lang.ClassLoader

从程序员角度讲

  • 启动类加载器(Bootstrap ClassLoader)
    • 负责将存放在\lib目录(或者-Xbootclasspath参数指定的路径)中的类库,加载到虚拟机中。其无法被Java程序直接引用。
  • 扩展类加载器(Extention ClassLoader)
    • sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录(或者java.ext.dirs系统变量指定的路径)中的所有类库,开发者可以直接使用。
  • 应用程序类加载器(Application ClassLoader)
    • 由sun.misc.Launcher$APPClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库。
    • 由getSystemClassLoader()方法返回,所以又叫系统类加载器

类加载机制

双亲委派机制(Parents Delegation Model)

双亲委派机制

  • 除了顶层的Bootstrap ClassLoader外,其他加载器都应当有自己的父类加载器
  • 加载器之间的父子关系,通过组合关系复用

双亲委派工作过程

  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。

  • 每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中

  • 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。

代码实现集中在java.lang.ClassLoader的loadClass()方法中

//JDK 1.8 中的loadClass()方法
 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) {
                        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) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

为什么要使用双亲委派模型,组织类加载器之间的关系?

Java类随着它的类加载器一起具备了一种带优先级的层次关系。比如java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各个类加载器环境中,都是同一个类。

如果没有使用双亲委派模型,让各个类加载器自己去加载,那么Java类型体系中最基础的行为也得不到保障,应用程序会变得一片混乱。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值