类加载器系统
- 类加载器负责从文件系统或者网络中加载 class 文件,class 文件在文件开头有特定的文件标识信息。(CAFFE BABY)
- classloader 只负责class 文件的加载,至于它是否可以运行,则由 execution engine(执行引擎)来决定的。
- 加载的字节码信息存放于一块称为方法区的内存空间
类加载的流程
- 在调用 HelloLoader 类的方法之前,会去加载 HelloLoader 类。
- 如果字节码文件没有语法错误,那么就会被顺利加载。
- 然后就进行链接操作
- 然后进行类的初始化
- 然后执行所调用的方法
加载
- 通过类的全限定类名获取定义此类的二进制文件流
- 将字节流的物理结构转换为方法区的运行时数据结构
- 在内存中生成一个代表这个类的Class 对象,作为方法区这个类的各种数据的入口
获取 class 文件的途径:
-
从本地系统中直接加载
-
通过网络获取,典型场景: Web Applet
-
从zip压缩包中读取, 成为日后jar、war格式的基础
-
运行时计算生成,使用最多的是:动态代理技术
-
由其他文件生成,典型场景: JSP应用
-
从专有数据库中提取.class文件,比较少见.
-
从加密文件中获取,典型的防Class文件被反编译的保护措施
链接
- 验证(Verify) :
- 目的在子确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。
- 主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
- 准备(Prepare) :
- 为类变量(也就是静态变量)分配内存并且设置该类变量的默认初始值,即零值。这里先设置为零值,在后面的初始化阶段才会设置为类中指定的值。
- 这里不包含用final修饰的static,因为final在编译的时候就会分配了,准备阶段会显式初始化;
- 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
- 解析(Resolve) :
- 将常量池内的符号引用转换为直接引用的过程。
- 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。
- 符号引用就是一组符号来描述所引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
- 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、 CONSTANT Fieldref info、 CONSTANT Methodref info等
初始化
- 初始化阶段就是执行类构造器方法 ()的过程。这个方法是固定的。clinit 也就是 class init。
- 此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。
- 构造器方法中指令按语句在源文件中出现的顺序执行。也就是说static 代码块中的赋值语句,可能会被后面的声明语句给覆盖
- ()不同于类的构造器。 (关联:构造器是虚拟机视角下的())
- 若该类具有父类,JVM会保证子类的()执行前,父类的()已经执行完毕。
- 虚拟机必须保证-一个类的()方法在多线程下被同步加锁。因为一个类只需要被加载一次就可以在方法区创建Class 对象。因此,在执行 clinit 方法的时候是会被加锁的。也就是说static 代码块中的逻辑是加锁的。
public class Main {
// 下面的类变量会在链接阶段分配内存 赋零值
// 然后再初始化阶段 按照顺序按照显示的赋值 进行修改
// 在链接阶段分配内存并且赋值为 0
private static int a=10;
static {
a=20;
// 这里能够访问的
// 因为在链接阶段 b 已经分配了内存等于 0
// 所以在初始化的时候可以进行设置
b=50;
System.out.println(a);
System.out.println(b);
// 这里无法调用b 因为 static 执行的时候 还未完成初始化
// Illegal forward reference 非法的前置引用
}
// 在链接阶段分配内存并且赋值为 0
private static int b=30;
public static void main(String[] args) {
// 然后再初始化中 按照上面类变量的顺序
// a:10->20
// b: 50->30
System.out.println(a); //20
System.out.println(b); //30
}
}
类加载器的分类
-
Bootstrap ClassLoader 启动类加载器,也叫做引导类加载器
- 这个类加载使用C/C++语言实现的,嵌套在JVM内部。也就是 jvm 的一部分。
- 它用来加载Java的核心库(JAVA HOME/jre/lib/rt.jar.
resources. jar或sun. boot. class. path路径下的内容) ,用于提供
JVM自身需要的类 - 并不继承自java. lang.ClassLoader,没有父加载器。
- 加载ExtClassLoader 和 AppClassLoader (扩展类和应用程序类加载器),并指定为他们的父类加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、 sun等开头的类
-
Extension ClassLoader 扩展类加载器
- Java语言编写,由sun. misc. Launcher $ExtClassLoader实现。
- 派生于ClassLoader类
- 上一级加载器为Bootstrap ClassLoader
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安
装目录的jre/lib/ext子目录(扩展目录)下加载类库。如果用户创
建的JAR放在此目录下,也会自动由扩展类加载器加载。
-
AppClassLoader 应用类加载器
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 派生于ClassLoader类
- 上层加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性java. class.path指
定路径下的类库 - 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由
它来完成加载 - 通过ClassLoader.getSystemClassLoader ()方法可以获取到该类加载器
双亲委派机制
基本概念
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是
把这个请求委托给父类的加载器去执行; - 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归
请求最终将到达项层的启动类加载器 - 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器
无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模
式。
举例:
// 这里自定义了一个 String 类,也放在了 java.lang 包中
package java.lang;
public class String {
// 这里的 static 代码块会在这个 String 类加载的初始化阶段执行
static {
System.out.println("自定义的 string 类的静态代码块执行了");
}
}
public class _双亲委派 {
public static void main(String[] args) {
java.lang.String str=new java.lang.String();
System.out.println("main 方法执行了");
}
}
// 最终的执行结果 并没有执行自定义 String 的 static 代码块
上述代码的分析:
在 main 方法的第一行, AppClassLoader 想要加载 String 类,然后向上委托,传递到 BootStrapClassLoader,BootStrapClassLoader发现加载的是 java.lang.String 那么就会直接进行加载,加载的就是 java 核心库中的 String,而不是让 AppClassLoader 去加载用户自定义的 String。
如果第一行加载的是一个用户自定义的类,那么就一路传递到 BootStrap 发现无法加载,那么就交给Extension ClassLoader 去加载,Extension ClassLoader 发现这个类也不在自己加载的路径中,那么就再返回给 AppClassLoader。AppClassLoader 就会在classpath中进行加载。
两个例子
package java.lang;
public class String {
// 这里的 static 代码块会在这个 String 类加载的初始化阶段执行
static {
System.out.println("自定义的 string 类的静态代码块执行了");
}
public static void main(String[] args) {
System.out.println("自定义String类的 main 方法执行了");
}
}
// 执行结果如下:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
上述结果,就是因为在执行 main 方法之前,AppClassLoader需要加载一个 java.lang.String 的类,然后它向上委托,传递到 BootStrapClassLoader 发现BootStrapClassLoader可以加载,于是加载的是核心库中的 String 类。核心库中的 String 类中是没有 main 方法的,所以报错。
package java.lang;
public class MyLang {
public static void main(String[] args) {
System.out.println("MyLang 类的 main 方法执行了");
}
}
// 执行结果
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
在加载 MyLang 类的时候,传递到了 BootStrap ClassLoader 类加载器,这个类加载器发现当前加载的是 java.lang 包,可是核心库中没有所对应的类MyLang,因此不会加载 MyLang 类,直接报错。
沙箱安全机制
保护核心类的加载全部都是使用 BootStrap ClassLoader 来进行加载,保证核心类的安全和 BootStrap ClassLoader 的自身安全。
双亲委派的优势
- 避免类的重复加载,一旦上层类加载器加载了一个类,那么下层就不会加载了
- 保护程序的安全,放置核心类库被篡改
- 自定义:java.lang.String 不会加载自己写的 String
- 自定义:java.lang.MyLang 拒绝加载 MyLang 因为不允许用户使用 java.lang 包名
其他知识
JVM 中如何判定两个 class 对象是否是同一对象
- 类的全限定类名完全一致
- 加载这个类的ClassLoader 也完全一致
也就是说,即使两个 class对象来源于同一个字节码文件,被同一个虚拟机加载,但是只要加载它们的 ClassLoader实例对象不一致,那么两个类对象也是不一致的。
类的主动使用和被动使用
Java程序对类的使用方式分为:主动使用和被动使用。
- 主动使用,又分为七种情况:
- 创建类的实例
- 访问某个类或接口的静态变量,或者对该静态变量赋值
- 调用类的静态方法
- 反射(比如: Class. forName (“com. atguigu. Test”) )
- 初始化一个类的子类
- Java虛拟机启动时被标明为启动类的类
- JDK 7开始提供的动态语言支持:
java. lang. invoke . MethodHandle实例的解析结果
REF getStatic、 REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
- 除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,
都不会导致类的初始化。
回顾