类初始化,类加载,类加载器
- 1. 类加载
- 1.1. 类的加载
- 1.2. 类的链接
- 1.2.1. 验证
- 1.2.2. 准备
- 1.2.3. 解析
- 2. 类加载器
- 2.1. 类加载器分为四种:前三种为虚拟机自带的加载器。
- 2.2. 类加载有三种方式:
- 2.3. **JVM类加载机制**
- 2.4. 双亲委派机制
- 3. 类的初始化
- 3.1. 类的主动引用会发生类的初始化
- 3.2. 类的被动引用不会发生类的初始化
- 3.3. 类缓存
1. 类加载
1.1. 类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Class
对象,用来封装类在方法区内的数据结构。
-
类的加载的最终产品是位于堆区中的
Class
对象,Class
对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。 -
在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取其定义的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆中生成一个代表这个类的
java.lang.Class
对象,作为对方法区中这些数据的访问入口。
-
加载.class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
1.2. 类的链接
- 将java类的二进制代码合并到JVM的运行状态中的过程
1.2.1. 验证
- 验证:确保加载的类信息符合JVM规范,没有安全方面的问题
- 验证阶段大致会完成4个阶段的检验动作:
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以
0xCAFEBABE
开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。 - 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了
java.lang.Object
之外。 - 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证:确保解析动作能正确执行。
- 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以
1.2.2. 准备
- 准备:正式为类变量(static)分配内存并设置类变量默认初始值的阶段,这些内存都将在方法区中进行分配
该阶段有以下几点需要注意:
- 1、这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 2、这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
- 3、如果类字段的字段属性表中存在
ConstantValue
属性,即同时被final和static修饰,那么在准备阶段变量value就会被初始化为ConstValue属性所指定的值。
假设一个类变量的定义为: public static int value=3
;那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的 public static
指令是在程序编译后,存放于类构造器 <clinit>()
方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行
- 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
- 对于引用数据类型reference来说,如数组引用、对象引用等,如果没有对其进行显式地赋值而直接使用,系统都会为其赋予默认的零值,即null。
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
1.2.3. 解析
- 解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
2. 类加载器
2.1. 类加载器分为四种:前三种为虚拟机自带的加载器。
- 启动类加载器(Bootstrap)C++
- 负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类
- 扩展类加载器(Extension)Java
- 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/ext/ *.jar或-Djava.ext.dirs指定目录下的jar包的一些jar包
- 应用程序类加载器(AppClassLoader)Java
- 也叫系统类加载器,负责加载classpath中指定的jar包及目录中class
- 用户自定义加载器 Java.lang.ClassLoader的子类,用户可以定制类的加载方式
2.2. 类加载有三种方式:
- 1、命令行启动应用时候由JVM初始化加载
- 2、通过Class.forName()方法动态加载
- 3、通过ClassLoader.loadClass()方法动态加载
2.3. JVM类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
- 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效
2.4. 双亲委派机制
- 双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
- 1、当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 2、当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 4、若ExtClassLoader也加载失败,则会使用AppClassLoader来加载
- 5、如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException
- 好处:
-
防止内存中出现多份同样的字节码(安全性角度)
- 比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object对象。
-
保证Java程序安全稳定运行
-
3. 类的初始化
在Java中对类变量进行初始值设定有两种方式:①声明类变量时指定初始值;②使用静态代码块为类变量指定初始值。
- 特点
- 执行类构造器
<clinit>()
方法的过程。- 类构造器
<clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)
- 类构造器
- 当初始化一个类时,如果其父类还没有进行初始化,则先触发其父类的初始化
- 虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确加锁和同步
- 执行类构造器
3.1. 类的主动引用会发生类的初始化
- 当虚拟机启动,会先初始化main方法所在的类
- new一个类的对象
- 调用类的静态成员(除了final常量)和静态方法
- 使用Java.lang.reflect包的方法对类进行反射调用
- 初始化一个类时,其父类如果没有初始化,则会先初始化它的父类
3.2. 类的被动引用不会发生类的初始化
- 访问一个静态域时,只有真正声明这个域的类才会被初始化。如:通过子类引用父类的静态变量,不会导致子类初始化
- 通过数组定义类引用,不会触发此类的初始化
- 引用常量不会触发此类的初始化(常量在链接阶段就存入调用类的常量池中了)
3.3. 类缓存
标准的javaSE类加载器可以按要求查找类,一旦某个类 被加载到类加载器中,它将维持加载(缓存)一段时间。但是JVM垃圾回收机制可以回收这些Class对象