1、类加载子系统作用以及类加载过程
-
类加载器子系统负责从文件系统或网络中加载class文件,class文件在文件开头有特性的文件标识(cafe babe: CAFEBABE)
-
-
ClassLoader只负责class文件的加载,至于它是否可以运行,则由ExecutionEngine决定
-
加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区还会存放运行时常量池信息,坑还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)
2、类加载器ClassLoader角色
- class file 存在于本地硬盘上,执行时,会将其加载到JVM当中,并按照该文件实例化出n个与其一致的实例
- class file 加载到JVM中,被称为元数据模板,放在方法区
- 在.class文件->JVM->最终成为元数据模板,此过程需要一个运输工具(类加载器 Class Loader)
3、类加载过程
1.Loading (加载)
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的的静态存储结果转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
plus:加载.class文件的方式
- 从本地系统直接加载
- 通过网络获取,典型场景:Web Applet
- 从zip压缩包中读取,成为日后jar、war格式的基础
- 运行时计算生成,使用最多的是:动态代理技术
- 有其他文件生成,典型场景:JSP应用
- 从专有数据库中提取.class文件,比较少见
- 从加密文件中获取,典型的用法:防class文件被反编译的保护措施
2.Linking (链接)
1)Verification(验证)
-
确保class文件的字节流中包含信息符合当前虚拟机要求,确保加载类的正确性,确保不会危害虚拟机自身安全
-
主要有四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
Verification (§4.10) ensures that the binary representation of a class or interface is structurally correct (§4.9). Verification may cause additional classes and interfaces to be loaded (§5.3) but need not cause them to be verified or prepared.
If the binary representation of a class or interface does not satisfy the static or structural constraints listed in §4.9, then a
VerifyError
must be thrown at the point in the program that caused the class or interface to be verified.If an attempt by the Java Virtual Machine to verify a class or interface fails because an error is thrown that is an instance of
LinkageError
(or a subclass), then subsequent attempts to verify the class or interface always fail with the same error that was thrown as a result of the initial verification attempt.
2)Preparation(准备)
-
为类变量分配内存并且设置该类变量的默认初始值,即零值
public class HelloApp { private static int a=1; //Prepare: a=0; ---> Initial:a=1; private final static int b=5; //Compiling: b=5; public static void main(String[] args){ System.out.println(a); } }
-
这里不包含用final修饰的static,因为final在编译时就会分配,准备阶段会显式初始化
-
这里不为实例变量分配初始化,类变量会分配在方法区中,而实例变量时会随着对象一起分配到Java堆中
3)Resolution(解析)
-
将常量池中的符号引用转换为直接引用的过程,如方法重写的本质
-
解析操作通常会在JVM执行完初始化后在执行
-
符号引用就是是一组描述引用目标的符号。符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件中。直接引用就是直接指向目标的指针、相对变异量或一个间接定位到目标的句柄
-
解析动作主要针对类和接口、字段、类方法、接口方法、方法类型等。对应常量池中的
CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
、CONSTANT_InterfaceMethodref_info
、CONSTANT_MethodType_info
等The
constant_pool
table (§4.4) in the binary representation of a class or interface is used to construct the run-time constant pool upon class or interface creation (§5.3). All references in the run-time constant pool are initially symbolic. The symbolic references in the run-time constant pool are derived from structures in the binary representation of the class or interface as follows:-
A symbolic reference to a class or interface is derived from a
CONSTANT_Class_info
structure (§4.4.1) in the binary representation of a class or interface. Such a reference gives the name of the class or interface in the form returned by theClass.getName
method, that is:- For a nonarray class or an interface, the name is the binary name (§4.2.1) of the class or interface.
- For an array class of n dimensions, the name begins with n occurrences of the ASCII “[” character followed by a representation of the element type:
Whenever this chapter refers to the name of a class or interface, it should be understood to be in the form returned by the
Class.getName
method. -
A symbolic reference to a field of a class or an interface is derived from a
CONSTANT_Fieldref_info
structure (§4.4.2) in the binary representation of a class or interface. Such a reference gives the name and descriptor of the field, as well as a symbolic reference to the class or interface in which the field is to be found. -
A symbolic reference to a method of a class is derived from a
CONSTANT_Methodref_info
structure (§4.4.2) in the binary representation of a class or interface. Such a reference gives the name and descriptor of the method, as well as a symbolic reference to the class in which the method is to be found. -
A symbolic reference to a method of an interface is derived from a
CONSTANT_InterfaceMethodref_info
structure (§4.4.2) in the binary representation of a class or interface. Such a reference gives the name and descriptor of the interface method, as well as a symbolic reference to the interface in which the method is to be found. -
A symbolic reference to a method handle is derived from a
CONSTANT_MethodHandle_info
structure (§4.4.8) in the binary representation of a class or interface. Such a reference gives a symbolic reference to a field of a class or interface, or a method of a class, or a method of an interface, depending on the kind of the method handle. -
A symbolic reference to a method type is derived from a
CONSTANT_MethodType_info
structure (§4.4.9) in the binary representation of a class or interface. Such a reference gives a method descriptor (§4.3.3). -
A symbolic reference to a call site specifier is derived from a
CONSTANT_InvokeDynamic_info
structure (§4.4.10) in the binary representation of a class or interface. Such a reference gives:- a symbolic reference to a method handle, which will serve as a bootstrap method for an invokedynamic instruction (§invokedynamic);
- a sequence of symbolic references (to classes, method types, and method handles), string literals, and run-time constant values which will serve as static arguments to a bootstrap method;
- a method name and method descriptor.
-
The Java Virtual Machine instructions anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, and putstatic make symbolic references to the run-time constant pool. Execution of any of these instructions requires resolution of its symbolic reference.
If an error occurs during resolution of a symbolic reference, then an instance of
IncompatibleClassChangeError
(or a subclass) must be thrown at a point in the program that (directly or indirectly) uses the symbolic reference.If an attempt by the Java Virtual Machine to resolve a symbolic reference fails because an error is thrown that is an instance of
LinkageError
(or a subclass), then subsequent attempts to resolve the reference always fail with the same error that was thrown as a result of the initial resolution attempt.
3.Initialization(初始化)
-
初始化阶段就是执行类构造器方法()的过程
-
此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
-
构造方法中指令安语句在原文件中出现的顺序执行
-
()不同于类的构造器(关联:构造器是虚拟机视角下的())
-
若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕
-
虚拟机必须保证一个类的()方法在多线程下被同步加锁(测试代码如下)
//虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁 public class DeadThreadTest { public static void main(String[] args) { Runnable r = ()->{ System.out.println(Thread.currentThread().getName() + "开始"); DeadThread dt = new DeadThread(); System.out.println(Thread.currentThread().getName() + "结束"); }; Thread t1 = new Thread(r,"线程1"); Thread t2 = new Thread(r,"线程2"); t1.start(); t2.start(); } } class DeadThread{ static{ if(true){ System.out.println(Thread.currentThread().getName() + "初始化当前类"); while(true){} } } }
4、类加载器的分类
-
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader,C/C++语言实现)和自定义类加载器(User-Defined ClassLoader,Java语言实现)。
-
从概念上讲,自定义类加载器一般指的是程序中有开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器
-
无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3个,如下所示
-
关于ClassLoader
ExtClassLoader
AppClassLoader
-
类加载器的测试代码
public class ClassLoaderTest { public static void main(String[] args) { //获取系统类加载器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2 //获取systemClassLoader上层:扩展类加载器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@1b6d3586 //获取extClassLoader上层:获取不到引导类加载器 ClassLoader bootStrapClassLoader = extClassLoader.getParent(); System.out.println(bootStrapClassLoader); //null //对于用户自定义类来说,获取ClassLoaderTest类的类加载器(默认使用系统类加载器来加载) ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2 //获取String类的类加载器(String类使用引导类加载器加载,Java的核心类库都是使用 //引导类加载器进行加载) ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); //null (String类使用引导类加载器加载) } }
1.虚拟机自带的加载器
1)启动类加载器(引导类加载器,BootStrap ClassLoader)
- 使用C/C++编写,嵌套在JVM内部;
- 用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类;
- 并不继承java.lang.ClassLoader类,没有父加载器;
- 加载扩展类和应用程序类加载器,并指定他们的父类加载器;
- 出于安全考虑,BootStrap启动类加载器只加载包名为java、javax、sun等开头的类;
2)扩展类加载器(ExtClassLoader)
- 使用Java编写,由sun.misc.Launcher$ExtClassLoader实现;
- 派生于ClassLoader类;
- 父类加载器为启动类加载器;
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载;
- 通过ClassLoader#getSystemClassLoader()#getParent()方法可以获取到该类加载器;
3)应用程序类加载器(系统类加载器,APPClassLoader)
- java编写,由sun.misc.Launcher$AppClassLoader实现;
- 派生于ClassLoader类;
- 父类加载器为扩展类加载器;
- 负责加载环境变量classpath或系统属性
java.class.path
指定路径下的类库; - 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的;
- 通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器;
4)用户自定义类加载器
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,但在必要时我们可以自定义类加载器,定制类的加载方式;
- 为什么要自定义类加载器?
- 隔离加载类
- 修改类加载方式
- 扩展加载源
- 防止源码泄露
- 实现步骤
- 开发人员通过继承抽象类
java.lang.ClassLoader
类的方式,实现自定义类加载器 - 在JDK1.2之前版本中,当自定义类加载器加载时,总会去继承
ClassLoader
类并重写loadClass()
方法,从而实现自定义加载类。但在1.2版本后已经不建议用户重写loadClass()方法,而是推荐把自定义加载类的逻辑写在findClass()方法中; - 在编写自定义类加载器时,若没有过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法以及其获取字节码流的方式,使自定义类加载器编写更加简洁。
- 开发人员通过继承抽象类
5)关于ClassLoader
-
ClassLoader类是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
5、双亲委派机制
- Java虚拟机对class文件采用的按需加载的方式,即当一个类需要使用时才会将它的class文件加载到内存中生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式;
1.工作原理
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是会把这个请求委托给父类的加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,一次递归,请求最终将到达顶层的启动类加载器;
- 如果父类加载器可以完成类加载任务,则成功返回,若无法完成加载任务,则子类才尝试自己完成该加载任务,此即双亲委派模式;
当需要加载API的实现类时,比如SPI的接口实现类jdbc.jar,此时因为引导类加载器无法加载,所以他为通过反向委托,让其子类加载器完成加载任务。
2.优势
-
避免类的重复加载
-
保护程序安全,防止核心API被随意篡改
- 自定义类:java.lang.String
- 自定义类:java.lang.ShkStart
package java.lang; public class ShkStart { public static void main(String[] args) { System.out.println("hello!"); } }
package com.renke.java; public class StringTest { public static void main(String[] args) { String str = new java.lang.String(); System.out.println("hello, renke!"); StringTest test = new StringTest(); System.out.println(test.getClass().getClassLoader()); ShkStart ss = new ShkStart(); /*Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang*/ System.out.println(ss.getClass().getClassLoader()); } }
package java.lang; public class String { //类的初始化第三个阶段,执行<clinit>()方法 static{ System.out.println("我是自定义String类的静态代码块"); } public static void main(String[] args) { System.out.println("hello string"); /* 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.application.Application 原因: 由于Java虚拟机的类加载采用的是双亲委派机制,导致当父类加载器的加载范围中存 在其子类加载器加载范围中相同全类名的类时,会优先使用父类加载器加载其范围中的类。 故,本类的全类名java.lang.String与引导类中的字符串类一致,导致使用的是字符串类, 而不是自定义的String类。(详见一、5、3.沙箱安全机制) 解决: 修改自定义String类的全类名,修改对应包名 */ } }
3.沙箱安全机制
- 自定义String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载JDK自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法就是因为加载的是rt,jar包中的String类。这样可以保证对java核心源代码的安全,此即沙箱安全机制;
4.其它
-
在JVM中表示两个class对象是否为同一个类存在两个必要条件:
-
类的完整类名必须一致,包括包名;
-
加载这个类的ClassLoader(值ClassLoader实例对象)必须相同;
(在JVM中,即使两个类对象(class对象)来源于同一个class文件,被同一个虚拟机所加载,只要它们的ClassLoader实例对象不同,那么这两个类对象)
-
-
JVM必须知道一个类型是由启动类加载器还是由用户类加载器加载的。若一个类型是由用户类加载器加载的,那么JVM则会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。当解析一个类型到另一个类型的引用时,JVM需要保证这两个类型的类加载器是相同的;
-
Java程序对类的使用方式分为:主动使用和被动使用(区别为是否导致类的初始化)
- 主动使用,分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如:Class.forName(“com.renke.Test”))
- 初始化一个类的子类
- Java虚拟机启动时被标明为启动类的类
- JDK 7 开始提供动态语言支持:
- java.lang.invoke.MethodHandle实例的解析结果
- REF_getStatic、REF_putStatic、REF_invokeStatic句柄对应的类没有初始化,则初始化
- 出了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化;
- 主动使用,分为七种情况: