什么是类加载
类的加载是指将类的.class二进制数据读入内存,放在运行数据区的方法
类的生命周期
这5个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析的阶段则不一定急的话。
这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交互地混合进行的。
- 加载
加载阶段虚拟机需要完成一下三件事情
- 通过一个类的全限定名来获取二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构
- 在Java堆中生成代表这个累的java.lang.class对象,作为方法区数据的访问入口
相对于其他的阶段,加载阶段是可控性最强的,开发人员可以自定义自己的类加载器
- 验证
验证是链接的第一阶段,验证阶段非常重要,但不是必须。
如果所引入的类反复验证,可采用 -Xverify:none 来关闭大部分验证,以缩短类加载时间
- 准备
为静态变量分配内存,初始化默认值。
注意事项
-
该阶段进行内存分配的仅包括类变量,而不包括实例变量,实例变量会在对象实例化时候,随对象一块分配在Java堆中
-
之类设置的初始值通常是数据类型默认的零值(0,0L,null,false....)
类变量的定义为
public static int v = 3;变量 初始值是0,而不是3。
把 v赋值3 是在程序编译后,存放于类的构造器方法中,初始化阶段才会执行类变量的赋值
-
如果类字段属性存在Constant,即被final和static同时修饰,那么在准备阶段就会被初始化属性所指定的值
类变量定义为
public static final int v = 3;编译时,javac将会为v赋值为3
- 初始化
初始化为类的静态变量赋予正确的初始值
JVM初始化步骤
- 假设类还没被加载和链接,则程序先加载并链接该类
- 假设类的直接父类还没被树池化,则先初始化父类
- 假设类中有初始化语句,则系统一次执行这些初始化代码
类的初始化时机
只有当对类的主动使用的时候才会导致类的初始化
类的主动使用包括以下六种
- 创建类的实例,也就是new的方式
- 访问某个类或接口的静态变量或者对改静态变量赋值
- 调用类的静态方法
- 反射
- 初始化某个类,其父类也会被初始化
- Java虚拟机启动时,被标明为启动类的类
- 结束生命周期
以下情况,Java虚拟机将结束生命周期
-
执行System.exit()方法
-
程序 正常执行 / 错误异常 / 系统异常 结束
类加载器
类加载器的层次关系图
注意:这里的父类加载器并不是通过继承的关系来实现的,而是采用组合实现的。
站在Java开发人员的角度,类加载器可划分为以下三类
- 启动类加载器[Bootstrap ClassLoader],负责加载。位于JDK\jre\lib目录下,属于JVM虚拟机的一部分。启动类加载器无法被java程序直接引用
- 拓展类加载器[Extension ClassLoader],加载器由**sun.misc.Launcher$ExtClassLoader **实现,开发者可以直接使用拓展类加载器
- 引用程序加载器[Application ClassLoader],加载器由sun.misc.Launcher$AppClassLoader 实现,负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,一般情况下,这个就是程序中默认的类加载器
Java类加载机制
全盘负责 当一个类加载器加载某个Class时,该Class所 依赖和引用 的其他Class也由该类加载器负责载入,除非显示使用另外一个类加载器
父亲委托 先让父类加载器试图加载该类,当父类加载器无法加载时,尝试从自己的类路径中加载
缓存机制 缓存机制保证所有加载过的Class都会被缓存,当需要某个Class的时候,类加载器先从缓存中寻找该Class,缓存不存在,系统才会读取对应的二进制数据转换成对应的Calss对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM
类的加载
有以下三种方式
- 命令行启动,有JVM初始化加载
- 通过Class.forName()方法动态加载 [引用程序加载器]
- 通过Class.loadCalss()方法动态加载 [引用程序加载器]
示例代码
package com.ys.classLoader;
public class LoadClassTest {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = LoadClassTest.class.getClassLoader();
System.out.println(classLoader);
// 使用ClassLoader.loadClass()来加载类,不会执行初始化代码
//classLoader.loadClass("com.ys.classLoader.Test1");
//使用Class.forName()来加载类,会默认执行初始化代码
//Class.forName("com.ys.classLoader.Test1");
//使用Class.forName()来加载类,并指定类加载器,不会执行初始化代码
Class.forName("com.ys.classLoader.Test1", false, classLoader);
}
}
Class.forName() 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
ClassLoader.loadClass() 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
补充知识
上面提到newInstance才会去执行,那么 new 关键字 和 newInstance() 方法的区别?
newInstance() 必须保证这个类已经加载并且已经连接
new 关键字 这个类可以没有被加载,不需要该类在classpath中设定,但可能需要通过classlaoder来加载。
双亲委派模型
如果一个类加载起收到类加载的情况,首先不会做自己去尝试加载这个类,而是委托给父加载器去完成,依次向上,最终所有请求都被传递到顶层类加载器。只有父加载器找不到所需要的类时,子加载器才会去加载该类。
传递方式查看类加载器的层次关系图
双亲委派模型的意义
- 系统类防止内存中出现多分相同的字节码
- 保证Java程序稳定运行
自定义加载器的应用
-
热部署
JVM有缓存机制,导致修改类,必须重启JVM虚拟机,才会加载新的类。那如何通过我们自己写的类加载器来重新加载改动的类
通过上面的双亲委派机制得知,如果不打破双亲委派机制的话,自定义类加载器就不会工作。
具体代码请见: 详解Java的类加载机制
-
网络传输的Class加密
具体请见: 详解Java的类加载机制