类加载器相关内容有很多,大概分以下几个关键点进行学习,
参考链接 https://www.bilibili.com/video/av47756459
目录
概览
类型(类,接口,枚举)的加载(从硬盘加载到内存),连接(字节码的校验(字节码可以被人为修改)、类之间关系的确定),初始化(静态变量的赋值)均是在程序运行期间完成的
JVM与程序的生命周期
以下几种情况JVM结束生命周期:
(1)执行System.exit()方法
(2)程序正常执行结束
(3)程序运行过程中遇到错误或异常而异常终止
(4)由于操作系统错误引起JVM进程终止
类加载器生命周期
加载
将类的.class 文件中的二进制数据读取到内存中,将其放入运行时数据区的方法区(1.8后有所改动)内,在内存中创建一个java.lang.Class 对象(规范并未明确说明Class对象放置区域,hotspot将其放置在方法区,不同于其他对象放置在堆中),用来封装类在方法区内的数据结构。
- 类的加载的最终产品是位于内存中的class对象
- class对象封装了类在方法区中的数据结构,并向java程序提供了访问方法区内数据结构的接口
加载.class 文件的方式:
— 从本地系统加载
— 通过网络下载
— 从zip等归档文件中加载
— 从专有数据库中加载
— 从java源文件动态编译为.class 文件(动态代理)
连接
(1)验证:确保加载类的字节码的正确性(2)准备:为类的静态变量分配内存,并将其初始化为默认值(3)解析:把类中的符号引用(间接引用)变为直接引用(使用指针指向内存位置)
初始化
为类的静态变量赋予正确的初始值(将静态变量的声明语句和静态代码块看做类的初始化语句)
使用:
卸载:从内存中销毁(OSGI)
主动使用与被动使用
在类加载,连接,初始化过程中,java 程序对类的使用分为两种:
(1)自动使用(7种)
— 创建类的实例(不是声明)
— 访问某个类或接口的静态变量或给静态变量赋值(子类调用父类的静态变量不会引起子类的主动使用)
— 调用类的静态方法(子类调用父类的静态方法不会引起子类的主动使用)
— 反射
— 初始化类的子类
— JVM启动时被标为启动类(包含main方法)的类(Java Test)
— 从JDK1.7开始的动态语言支持
(2)被动使用
除了主动使用中的7种方法,其余使用java类的方法都称为被动使用,均不会导致类的初始化(不影响加载,连接)。
所有java虚拟机实现必须在每个类或接口被java程序“首次主动使用”才初始化。
package test;
public class MyTest1 {
public static void main(String[] args) {
System.out.println(Child.string);
}
}
class Parent {
static String string = "hello";
static {
System.out.println("Parent static block");
}
}
class Child extends Parent {
static {
System.out.println("Child static block");
}
}
由于没有主动使用Child类,Child类没有初始化,但是进行了加载!
-XX:+TraceClassLoading 用于追踪类的加载信息并打印,添加至idea配置信息的VM options,即JVM参数(关于VM options,参考:https://blog.youkuaiyun.com/upgroup/article/details/81052047)。
常量
对于final属性的成员变量(常量),编译期常量会存入到调用此常量的方法所在类的常量池中。本质上,调用类并没有直接引用到定义常量的类。因此,并不会触发定义常量的类的初始化。即使删除定义常量的类的class文件也不影响使用,编译过后就完全不需要定义常量的类了。
若是 final 属性的非编译期常量(如 随机数)即 运行期常量,则不同于上述情况。
数组成员变量
建立对象数组,不会主动使用对象类。对于数组实例来说,类型是JVM运行时动态生成的,表示为[***,动态生成的类型其父类型就是Object。
反编译
将已编译的编程语言还原到未编译的状态。java中,就是将.class文件转换成.java文件。使用命令 javap 。(编译使用javac)
助记符
ldc, 表示将int,float 或 string 常量值从常量池推送至栈顶。
bipush表示将单字节常量(-128 ~ 127)推送至栈顶。
sipush 表示短整型常量推送至栈顶。
iconst_1 (m1 ~ 5)表示将int型常量(m1 ~ 5)推送至栈顶。
anewarray 创建一个引用类型的数组,并将其引用值压入栈顶。
newarray 创建一个指定原始类型的数组,并将其引用值压入栈顶。
类加载器准备阶段与初始化阶段
public class TestClass2 {
public static void main(String[] args) {
Test test = Test.getTest();
System.out.println(test.a);
System.out.println(test.b);
}
}
class Test {
static int a;
static Test test = new Test();
Test() {
a++;
b++;
System.out.println(a);
System.out.println(b);
}
static int b = 0;
static Test getTest() {
return test;
}
}
此例中,类加载器先在准备阶段顺序执行静态代码块,将成员变量赋默认值(a,b为0,test为null),然后再执行类的初始化阶段,a不改变;test执行new Test() ,将a,b值改为1;b初始化值改为0。
简单理解,第一次执行等号左边,第二次执行等号右边。
接口的初始化
由于接口的成员默认都是final的常量,会放置调用此常量的方法所在类的常量池中
不同于类,初始化接口,不会引起父接口的初始化 (不影响加载)。在初始化一个类时,也不会初始化它实现的接口。但,仍然需要加载。只有当程序首次使用特定接口的静态变量时才会初始化该接口。
public class TestInterface {
public static void main(String[] args) {
System.out.println(ChildInterface.b);
}
}
interface ParentInterface {
int a = 1 / 0;
}
interface ChildInterface extends ParentInterface {
int b = new Random().nextInt(10) + 1;
}
这段代码不会报错,即,不会引起ParentInterface 的初始化。若是将接口改为类则会报错。
(能在编译时初始化的,就不延迟到运行时解决)
类实例化
- 为新的对象分配内存
- 为实例变量赋默认值
- 为实例变量赋正确的初始值
- java编译器为它编译的每一个类都至少生成一个实例初始化方法,在class文件中,这个初始化方法称为<init>方法,针对源代码中的每一个构造方法,java编译器都生成一个<init>方法。
类加载器
JVM自带的类加载器
- 跟类加载器(Bootstrap) (听着这么熟悉?一个前端开发框架)
- 扩展类加载器(Extension)
- 系统(应用)类加载器(System)
自定义的类加载器
- java.lang.ClassLoader 的子类
- 定制类加载方式
类加载器并不需要该类“首次使用”才加载该类。JVM规范允许类加载器在预料某个类将被使用时,预先加载。若加载出现错误或缺失class文件,将在该类“首次主动使用”时才报告错误。若一直未使用,则不会报错。
双(父)亲委托机制
在双亲委托机制中,各个类加载器按照父子关系形成树形结构。除了启动类加载器(根类加载器)之外,所有类加载器有且只有一个父加载器。
每一个类加载器通过特定的目录进行加载。
首先不自己加载,而是通过自己的父亲加载,父亲又通过父亲的父亲进行加载直到启动类加载器。再自顶向下进行加载,看看到底谁能够加载。
由父加载器加载的类不能访问由子加载器所加载的类。
命名空间
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。
不同的命名空间中类的完整名字可以相同,但是,相同命名空间中不能出现两个相同的完整类名。
因此当实例化多个自己定义的类加载器时,不同的类加载器都会进行加载类。
当在一个类中创建引用对象时,对象所对应的类由加载包含该对象的类的加载器进行加载(同时遵循双亲委托机制)。
子加载器所加载的类能够访问父加载器所加载的类。但是,由父加载器加载的类不能访问由子加载器所加载的类。
类的卸载
当MySample类被加载、连接、初始化后,其生命周期就开始了。当代表类Class对象不再被引用,不可触及时,Class对象就会终结其生命周期,MySample类在方法区内的数据就会被卸载,从而结束其生命周期。
一个类何时结束其生命周期,取决于代表它的Class对象何时结束其生命周期。
由Java虚拟机自带的类加载器(根类、扩展、系统类加载器)所加载的类,在虚拟机的生命周期中始终不会被卸载。JVM会始终使用这些类加载器,这些类加载器会始终使用它们所加载的类的Class对象,这些Class对象始终是可以触及的。
由用户自定义的类加载器所加载的类可以被卸载。