jvm 类加载

类加载器

在Java代码中,类型的加载连接初始化过程都是在程序运行期间完成

加载:

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

将二进制文件加载到数据区的方法区

并在内存中创建一个java.lang.Class对象,用来封装类在方法区内的数据结构

class文件通过 本地系统、网络下载、归档文件中、专有数据库中、源文件动态编译 获得

连接:

  • 验证: 加载类的正确性
  • 准备:为类的静态变量分配内存,初始化为默认值
  • 解析:把类中的符号引用转换为直接引用

初始化:

为类的静态变量赋予正确的初始值

初始化是存在顺序的, 从上到下初始化。

编译器为它编译的每个类都至少生成一个实例初始化方法,java的class文件中,这个实例初始化方法被称为<.init>

类的卸载

由JVM自带的类加载器所加载的类(根类、扩展类、系统类), 在VM的生命周期中,★始终不会被卸载

JVM始终会引用这些自带加载器, 这些加载器始终引用他们所加载的类的Class对象。这些Class不会被卸载

★★★(用户自定义的类加载器,加载的类, 是能被卸载的)

类的使用

类或者是接口被Java虚拟机首次主动使用初始化

主动使用

  • 创建类的实例

    定义类的数组,只是开辟空间(运行期创建新类型[L),并没有初始化(会加载类)

  • 访问某个类或接口的静态变量,或者对该静态变量赋值

    对于静态字段,只有直接定义了该字段的类(不管引用)才会被初始化

    但是会加载定义类到虚拟机中

    定义为final设置常量,在编译阶段就赋值到常量池(助记符)中,并不会加载类

    如果常量的值并非编译器确定的,不会放到常量池中,那么在运行期间就会主动使用

  • 调用类的静态方法

  • 反射

  • 子类被初始化

    子类的初始化会先调用父类的初始化

    初始化一个类时,不会初始化它所实现的接口

    初始化一个接口时,并不会初始化它的父接口

  • 标记为启动类(含有main方法的)

  • JDK7的动态语言支持,解析句柄初始化

被动使用

除了主动其他都是被动, 不会主动的初始化类

类加载器

JVM规范可以预先加载将要使用的类(可能没有使用)。

如果在预先加载的过程中存在class文件丢失,加载器必须在程序首次主动使用该类时才报告错误(LinkageError), 如果类没有被主动使用,那么类加载器将不会报告错误

1
2
3
4
clazz.getClassLoader(); // 当前类的ClassLoader
Thread.currnetThread().gerContextClassLoader(); // 线程上下文的ClassLoader
ClassLoader.getSystemClassLoader(); // 系统ClassLoder
DriverManager.getCallerClassLoader(); // 调用者ClassLoder

数组的类型是在运行时动态生成的,使用的类加载器是元素的类加载器, 原生类型没类加载器

  • JVM自带加载器

    • 根类加载器(Bootstrap)

      加载lang包的核心类库,从sub.boot.class.path所指定的目录中加载类库,依赖底层属于JVM实现

      由JVM启动启动类加载器

    • 扩展类加载器(Extension)

      父加载器为根加载器,从java.ext.dirs、jre\lib\ext指定的目录加载类库

      纯Java实现,是ClassLoader的子类

    • 系统(应用)类加载器(System)

      应用类加载器,父类为扩展加载器

      classpath或者java.class.path指定的目录中加载类

      用户定义类加载器的父加载器

      通过修改启动参数 java.system.class.loader来修改系统加载器(上加载器还是VM系统类加载器).

  • 用户自定义加载器

    • Java.lang.ClassLoader的子类
    • 用户自定义
      1
      2
      3
      4
      5
      loadClass 
      |
      findClass
      |
      defineClass 使用父类定义类

当前类加载器

每个类都会使用自己的类加载器(加载自身的加载器)去加载其它类(依赖的类)

线程上下文类加载器

如果线程没有通过setContextClassLoader设置类加载器的话, 线程将继承其父线程的上下文类加载器

Java应用运行时初始线程的上下文类加载器是System(App)系统加载器

父ClassLoader可以使用当前线程currentThread的上下文getContextLoader所指定的classloader来加载类

这样可以解决不同命名空间下的可见性(摆脱双亲委托模型)

双亲委托机制

可以确保Java核心库的类型安全(核心类都是由启动类来完成)
不同的类加载加载的类相互隔离在一些框架中存在应用场景

  1. 自底向上检查类是否已经加载
  2. 自顶向下尝试加载类

子加载器有父加载器的实例(装饰模式)

双亲委托机制

每个加载器实例有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
​ ★ 子加载器所加载的类可以访问父加载器所加载的类
​ ★ 父加载器无法加载由子加载器加载过的类
​ ★ 同一个命名空间内的类是相互可见

被某个加载器加载的类, 如果类内部需要加载其它类, 它将使用该类的加载器类作为其加载器

破坏双亲委派机制

  1. 为了向前兼容ClassLoader

    loadClass中实现双亲委派机制

    加入findClass,重写findClass实现自己的加载方式

    双亲委派机制源码

  2. 实现SPI

    例如 JNDI、JDBC、JCE、JAXB、JBI等实现SPI

    通过Thread上下文加载器实现SPI

    父加载器通过使用上下文中存储的子类加载器去加载类

    JDBC getConnection使用ThreadContextClassLoader

    检查是否是相同的类

  3. 实现热替换(HotSwap)、热部署(HotDeployment)

线程上下文加载器

线程上下文加载器就是为了破坏Java双亲上下文加载委托机制

高层提供统一的接口让底层实现,为了能让高层能实例化底层的类,需要通过线程上下文加载器来帮助高层的ClassLoader找到并加载该类

// 常量在编译阶段就会存入到这个常量的方法所在类的常量池中,

// 本质上调用常量没有直接引用到定义常量到类,并不触发常量的初始化

助记符

每个助记符在java底层都有相应的实现类

ldc

将int,float,string类型的常量从常量池中推送到栈顶

bipush

将单字节(-128~127)的常量值推送到栈顶

sipush

将短整形常量(-32768~32767)推送到栈顶

iconst_{N}

{N}: m1,0<=N<=5

将int(-1~5)类型推送到栈顶

anewarray

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

newarray

创建一个指定的原始类型(如int、float、char等)的数组,并将其压入栈顶

invokevirtual

执行虚方法, 进行多态查找流程

 上一篇