Loading

JVM-类加载

Class Loader SubSystem

类型的加载、连接与初始化过程都是在程序运行期间完成的(类型等于class、interface)
类加载的产物是位于内存中Class对象
Class对象封装了类在方法区内的数据结构,并向Java程序员提供了访问方法区内的数据结构的接口

步骤

具体分三个步骤:加载、链接、初始化

  • 加载:查找并加载类的二进制数据到内存中

  • 连接:

    • 验证:确保被加载的类的正确性
    • 准备:为类的静态变量分配内存,并将其初始化为默认值
    • 解析:把类中的符号引用转换为直接引用
  • 初始化:对静态变量赋予正确的初始化值
    类加载过程-w538

类的加载

双亲委托机制

当Java程序请求加载器loader1加载Simple类时,loader1会首先委派其父加载器加载,如果父加载器能加载则由父加载器完成加载工作,否则才有loader1加载器本身加载Simple类

各个类加载器按照父子关系形成逻辑上的树形结构,除了根类加载器其他加载器有且仅有一个父加载器

如果有一个类加载器能够成功加载Test类,那么这个类加载器称为定义类加载器,能够返回Class对象引用的加载器都称为初始化类加载器

类加载器属于包含关系,子类包含父类引用

类加载器并不需要在类首次使用时才加载它,JVM规范中允许类加载器在预料到某个类将要被使用时就预先加载它,如果该类的.class文件缺失或者错误则必须在程序首次主动使用时才能抛出LinkageError错误,如果这个类一直没有被主动使用则一直不会报错。

有两种类加载器

  • Java虚拟机自带的加载器
    • 根类加载器(Bootstrap)
      • 负责加载虚拟机核心类库,$JAVA_HOME中jre/lib/rt.jar里所有class文件如java.lang.*,由c++实现不是ClassLoader子类
    • 扩展类加载器(Extension)
      • 负责加载Java平台中扩展功能jar包,包$JAVA_HOME中jre/lib/*.jar或者-Djava.ext.dirs指定目录下jar
    • 系统(应用)类加载器(System)
      • 负载加载classpath中指定jar包以目录下class文件
  • 用户自定义加载器
    • java.lang.ClassLoader的子类
    • 用户可以定制类的加载方式

类加载器-w227
ps:获取一个数组的类加载会获得一个和数组中元素类加载器相同的类加载器,基础数据类型数组没有类加载器

内建于JVM中的启动类加载器会加载java.lang.ClassLoader以及其他的Java平台类,当JVM启动时,一块特殊的机器码会运行,它会加载扩展类加载器和系统类加载器,这块机器码叫做启动类加载器(Bootstrap)。

启动类加载器并不是Java类(C++实现),而其他的加载器都是Java类,启动类加载器是特定于平台的机器指令,它负责开启整个加载过程。
所有类加载器(除了启动类加载器)都被实现为Java类。不过,总归要有一个组件加载第一个Java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯Java类加载器就是启动类加载器的指责。
启动类加载器还会负责加载提供JRE正常运行所需要的基本组件,这包括java.util与java.lang包中的类等等。

定义类加载器:真正加载类的加载器

好处

  1. 可以确保Java核心库的类型安全:所有的Java应用都至少会引用Java.lang.Object类,也就是说在运行期,Java.lang.Object这个类会被加载到Java虚拟机中;如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间发挥着作用)。借助于双亲委托机制,Java核心类库中类的加载工作都是由启动类加载器来统一完成,从而确保类Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。
  2. 可以确保Java核心类库所提供的类不会被自定义的类所替代。
  3. 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。

上下文线程类加载器(打破双亲委托模型)

  • 当前类加载器(Current ClassLoader)

每个类都会使用自己的类加载器(即加载自身的类加载器)来加载器其他类(指所依赖的类)如果Class X引用了Class Y,那么Class X的类加载器就会去加载Class Y(前提是Class Y尚未被加载)

  • 线程上下文类加载器(Context ClassLoader)

如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承其父线程的上下文类加载器。
Java应用运行时的初始线程的上下文类加载器是应用类加载器。在线程中运行的代码可以通过该类加载器来加载类与资源。

  • 线程上下文类加载器的重要性:

SPI(Service Provider Interface)

父ClassLoader可以使用当前线Thread.currentThread().getContextClassLoader()所指定的ClassLoader加载的类。这就改变了父ClassLoader不能使用子ClassLoader或者是其他没有直接父子关系的ClassLoader加载类的情况,即改变了双亲委托模型。

线程上下文加载器就是当前线程的Current ClassLoader。

在双亲委托模型下,类加载是由上至下的,即下层的类加载器会委托上层进行加载。但是对于SPI来说,有些接口是Java核心库所提供的,而Java核心库是由启动类加载器来加载的,而这些接口的实现却来自于不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

  • 线程上下文类加载器一般使用模式(获取 - 使用 - 还原)

获得ClassLoader的途径

  • 获取当前类的ClassLoader
    • class.getClassLoader();
  • 获取当前线程上下文的ClassLoader
    • Thread.currentThread().getContextClassLoader();
  • 获得系统的ClassLoader
    • ClassLoader.getSystemClassLoader();
  • 获取调用者的ClassLoader
    • DriverManager.getCallerClassLoader();

加载.class文件的方式

  • 从本地系统直接加载
  • 通过网络下载.class文件
  • 从zip、jar等归档文件中加载.class文件
  • 从专有数据库提取.class文件
  • 从Java源文件动态编译为.class文件

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该类加载器以及所有父加载器所加载的类组成。
  • 在同一个命名空间,不会出现完整名字相同的两个类。
  • 在不同的命名空间中,可能会出现类的完整名字相同的两个类。

同一个命名空间的内是相互可见的;
子加载器加载的类能够访问父加载器所加载的类;
父加载器加载的类不能够访问子加载器所加载的类;
如果两个加载器之间没有直接或者间接的父子关系,各自加载的类相互不可见。

类的初始化

步骤

  • 假如这个类没有被加载和连接,则先进行加载和连接
  • 假如这个类存在直接父类且未初始化则先初始化其直接父类
  • 假如类中存在初始化语句,就依次执行初始化语句

类的使用

Java程序对类的使用方式分为两种:

  • 主动使用
  • 被动使用

所有的Java虚拟机实现必须在每个类或者接口被Java程序“首次主动使用”时才初始化他们

主动使用(初始化时机)七种情况:

  1. 创建类的实例
  2. 访问某个类或者接口的静态变量(getstatic),或者对该静态变量赋值(putstatic)
  3. 调用类的静态方法(invokestatic)
  4. 反射(如Class.forName("com.test.className"))
  5. 初始化一个类的子类
  6. Java虚拟机启动时被标明为启动类的类
  7. JDK1.7开始的对动态语言的支持

除了上述7种情况都属于被动使用,都不会导致类的“初始化”。

类的加载指将类的.class文件中二进制数据读入到内存中,将其放在运行时的数据区的方法区内,然后在内存中创建了一个java.lang.Class对象用来封装类在方法区的数据结构。

常量在编译阶段会存入到调用这个常量的方法所在类的常量池中,本质上,调用类并没有直接引用到定义常量的类,因为并不会触发定义常量的类的初始化
但是当一个常量的值无法在编译期间确定时,那么其值就不会放到调用类的常量池中,这时在程序运行时,会导致主动使用这个常量所在的类,显然会导致这个类被初始化

对于静态字段,只有直接定义了该字段的类才会被初始化

初始化规则

  • 当一个类在初始化时,要求其父类全部都已经初始化完毕
  • 当初始化一个类的时,并不会先初始化它所实现的接口
  • 当一个接口在初始化时,并不要求其父接口都完成初始化,只有在真正使用到父接口的时候(如引用接口中所定义的常量时)才会初始化

初始化阶段会按照声明变量顺序从上到下进行

类的卸载

由Java虚拟机自带的类加载所加载的类,在虚拟机生命周期内,始终不会被卸载。虚拟机本身始终会引用这些类加载,而这些类加载器则始终会引用它们所加载类的Class对象。

由用户自定义的类加载器加载的类是可以被卸载的

JVM参数

规则

-XX:+{option} 表示开启option选项

-XX:-{option} 表示关闭option选项

-XX:{option}={value} 表示将option选项的值设置为value

具体参数

-XX:+TraceClassLoading,用于追踪类的加载信息并打印出来

助记符

ldc---表示将int、float、String类型常量值从常量池推送至栈顶

bipush---表示将单字节(-128~127)的常量值推送至栈顶

sipush---表示将一个短整型常量值(-32768~32767)推送至栈顶

iconst_1---表示将int类型1推送至栈顶(ICONST - Push value between -1, ..., 5)

anewarray---表示创建一个引用类型的(如类、接口、数组)数组,并将其引用值压入栈顶

newarray---表示创建一个指定的基础数据类型(int、float、char等)的数组,并将其引用值压入栈顶

posted @ 2021-03-08 21:48  Vic呼叫89  阅读(43)  评论(0)    收藏  举报