01. 类加载子系统作用
- 从文件系统、网络中加载class文件,class文件开头有特定文件标识;
- ClassLoader只负责class文件加载,class文件是否运行由执行引擎决定;
- 加载的类的信息存放在方法区的内存空间,方法区还会存放运行时常量池信息、字符串字面量、数字常量等。
02. 类的加载过程
(1)加载
- 通过类全限定名获得定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构,从磁盘中读取到内存中;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
- 加载.class文件方式:本地、网络获取、zip包、运行时计算生成(如动态代理)等。
(2)链接
【1】验证
- 验证Class字节流是否符合虚拟机要求,保证被加载类的正确性。
【2】准备
- 为类变量(static修饰的)分配内存和设置该类变量的默认初始值,即零值;
- 不包含final修饰的static,final在编译时会分配,即常量在准备阶段显式初始化;
- 不为实例变量分配初始化,类变量会分配在方法区,实例变量会随着对象分配到Java堆。
【3】解析
将常量池中的符号引用转换成直接引用。事实上解析在初始化之后再执行。
符号引用:一组符号来描述引用的目标,符号引用就是就是表明指向一个唯一的类、字段、方法;
直接引用:直接指向目标的指针、相对偏移量、一个能间接定位到目标的句柄,使用直接引用时,引用的目标必定已经存在于虚拟机的内存中。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
我的理解:符号引用:所要用的类并没有加载,因此得用类全类名表明该类进行唯一标识,然后使用符号引用指向该类名,用文字形式描述引用关系;直接引用,该方法已经存在于内存中,根据直接引用的指针指向可以定位到该方法。
(3)初始化
- 执行类构造器方法
<clinit>()
; - 该方法不需定义,javac编译器自动收集类中所有类变量的赋值动作和静态代码块中语句合并而来,即字节码文件中就存在该方法;
- 构造器方法中指令按语句在源文件出现顺序执行;
<clinit>()
不同于类的构造器。(构造器是虚拟机视角下的<init>
);- 若该类有父类,JVM会保证子类
<clinit>()
执行前,执行完毕父类<clinit>()
; - 如果该类中没有静态变量和静态代码块,则字节码.class文件中不会有
<clinit>()
; - 虚拟机必须保证类的
<clinit>()
在多线程下被同步加锁,保证类加载一次。
总结:静态变量在准备阶段赋值为零,然后在初始化阶段根据类变量赋值动作和静态代码中的赋值动作的顺序执行赋值。
// 初始化阶段静态变量赋值测试
public class HelloApp {
private static int a = 1; // prepare阶段:a = 0 ---> initial: a = 1
private static int num = 1; // prepare阶段:num = 0
static {
num = 2; // 类构造器初始化<clinit>后num=2
number = 20;
}
private static int number = 10; // prepare阶段:number = 1 ---> initital: 20 --> 10
public static void main(String[] args) {
System.out.println(num);
System.out.println(number);
}
}
静态内部类初始化过程:
- 外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。
- 实例化外部类,调用外部类的静态方法、静态变量,则外部类必须先进行加载,但只加载一次。
- 直接调用静态内部类时,外部类不会加载。
public class ClinitTest {
static class Father {
public static int A = 1;
static {
A = 2;
}
}
static class Son extends Father{
public static int B = A;
}
public static void main(String[] args) {
// 执行到此处时,才先加载Father类,再加载Son类
System.out.println(Son.B);
}
}
03. 类加载器分类
- 引导类加载器(Bootstrap ClassLoader),非Java所写
- 自定义加载器,是将所有派生于抽象类ClassLoader的类加载器划分到自定义类加载器
(1)引导类加载器(启动类加载器)
- 使用C/C++实现,不能直接获取到
- 加载java核心库,加载包名为java、javax、sun等开头的类
- 不继承java.lang.ClassLoader,没有父加载器
- 加载扩展类、应用程序类加载器,并指定为他们的父类加载器
(2)扩展类加载器
- Java语言编写,派生于ClassLoader类,父类加载器为引导加载器(与继承无关)
- 加载jre/lib/ext子目录下的类库,用户创建的jar可添加到该目录下用该加载器加载
(3)应用程序类加载器(系统加载器)
- Java编写,派生于ClassLoader类,父类加载器为扩展类加载器
- 程序中默认类加载器,一般来说,Java应用的类由其加载
public class ClassLoaderTest {
public static void main(String[] args) {
// 获取系统类加载器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// 获取其上层扩展类加载器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@7d4991ad
// 获取BootstrapClassLoader
ClassLoader bootStrapClassLoader = extClassLoader.getParent();
System.out.println(bootStrapClassLoader); // 获取失败
// 对于用户自定义类来说,默认使用类加载器是系统类加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2
// String类引导使用类加载器加载 ---> Java核心类库使用引导类加载器加载
System.out.println(String.class.getClassLoader()); // null
}
}
(4)用户自定义类加载器
【1】使用情况
- 隔离加载类,避免类全限定名冲突;
- 修改类加载方式;
- 扩展加载源;
- 防止源码泄漏,如对字节码文件进行加密,在加载时解密。
【2】使用
- 继承ClassLoader类,自定义类加载逻辑写在findClass()方法中
- 可继承URLClassLoader类
04. ClassLoader类
- 抽象类
//获取ClassLoader的方法:
// 1. 获取当前类的ClassLoader
clazz.getClassLoader();
// 2. 获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();
// 3. 获取系统的ClassLoader
ClassLoader.getSystemClassLoader();
// 4. 获取调用者的ClassLoader
DriverManager.getCallerClassLoader();
05. 双亲委派机制
JVM对类的加载是按需加载,加载某个类的class时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,任务委派模式。
(1)工作原理
- 如果一个类加载器收到类的加载请求,并不会自己去加载,二是把请求委托给父类加载器去执行;
- 若父类加载器还有其父类加载器,则继续向上委托,依次递归,请求最终达到顶层引导加载器;
- 如果父类加载器可完成类加载任务,则成功返回,否则让子类加载器自己加载,向下委托。
举例:
- 自定义一个java.lang.String类,在该类中写main方法执行,会报错:由于双亲委派机制,实际上是引导类加载器加载的是核心API中的String类,该类并没有main方法,报错。
- 核心库的接口由引导类加载器加载,核心库接口的实现类由系统类加载器加载。
(2)优势
- 避免类的重复加载,委派过程中某类被某类加载器加载,就不会传给其他类加载器加载;
- 保护程序安全,防止核心API被随意篡改,如编写java.lang.String、java.lang.xx类报错。
(3)沙箱安全机制
将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。类比OS中的特权级别。在双亲委派机制中,外层恶意的类通过内置代码也无法获得权限访问到内部类,破坏代码就自然无法生效。
06. 其他
(1)对象所属类的判断
两个Class对象是否为同一个类的两个必要条件:
- 类的完整类名必须一致,包含包名;
- 加载该类的ClassLoader相同。
JVM必须知道一个类型由启动加载器加载还是由用户类加载器加载。若一个类型由用户类加载器加载,JVM将该类加载器的一个引用作为类型信息的一部分保存在方法区中。解析一个类型到另一个类型的引用时,JVM需要保证两类型的类加载器相同。
(2)类的主动使用和被动使用
- 主动使用:
- 创建类的实例:
- 访问某个类或接口的静态变量,或对该静态变量赋值;
- 调用类的静态方法;
- 反射(Class.forName("xx.xx"));
- 初始化一个类的子类;
- Java虚拟机启动时被标明为启动类的类;
- JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果等。
- 被动使用:
- 其他是类的被动使用,不会导致类的初始化。