概述
在Class文件中描述的各类信息,最终都需要加载到虚拟机中之后才能被运行和使用。
JVM类加载机制
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制
动态加载
Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的,
- 缺点
这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销 - 好处
为Java应用提供了极高的扩展性和灵活性
例子
编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类,用户可以通过Java预置的或自定义类加载器,让某个本地的应用程序在运行时从网络或其他地方上加载一个二进制流作为其程序代码的一部分
类加载的时机
整个生命周期将会经理加载(Loading)、验证(Verification)、准备(Perparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)共7个阶段。
验证、准备、解析三个部分统称为连接(Linking)
固定不变的步骤:除了解析之外
类的加载、验证、准备、初始化、卸载五个阶段的顺序是确定的,而解析不一定,解析可以在初始化阶段之后
类初始化种类
- 遇到new、getstatic、putstatic、invokestatic 这4个字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这4条指令的Java代码有
1.new实例化对象
2.读取或设置一个类型的静态字段(被final、已在编译器把结果放入常量池的静态字段除外)的时候
3.调用一个类型的静态方法的时候 - 使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有初始化,则需初始化
- 初始化类的时候,如果其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类
- 使用JDK7新加入的动态语言支持时,
- 当一个接口中顶一个JDK8新加入的默认方法(default关键字修饰的接口方法)时,如果有这个接口得实现类发生了初始化,那该接口要在其之前被初始化
有且只有这6中场景的行为称为对一个类型进行主动引用。
除此之外,所有引用类型的方法都不会触发初始化,称为被动引用
被动引用举例
/**
* @Author jtao
* @Date 2021/1/26 22:28
* @Description
*/
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
/**
* @Author jtao
* @Date 2021/1/26 22:29
* @Description
*/
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init");
}
}
/**
* @Author jtao
* @Date 2021/1/26 22:36
* @Description
*/
public class NotInitialization {
//通过子类引用弗雷德静态字段,不会导致子类初始化
@Test
public void NotInitialization() {
System.out.println(SuperClass.value);
}
//通过数组定义来引用来,不会触发此类的初始化
@Test
public void NotInitialization1() {
SuperClass[] sca = new SuperClass[10];
}
//非主动使用类字段演示
//常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
@Test
public void NotInitialization2() {
System.out.println(ConstClass.HELLOWORD);
}
}
调用NotInitialization()方法后
- 对于静态字段,只有直接定义这个字段的类才会被初始化
调用NotInitialization1()方法后
- 通过数组定义来引用来,不会触发此类的初始化
调用NotInitialization2()方法后
- 常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
接口的加载的区别
- 接口也有初始化过程,但接口中不能用static{}静态代码块,
- 接口初始化除了“有且仅有”的第三种外(一个接口初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化),其他均与类相同
类加载的过程
类加载的全过程:加载、验证、准备、解析和初始化 5个阶段
加载
在加载阶段,JVM需要完成一下三件事情:
1.通过一个类的全限定名来获取定义此类的二进制字节流
2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 仅仅通过全限定类名获取此类的二进制字节流,并没有指明必须从某个Class文件获取,则发展出以下获取途径
zip压缩包中读取
成为JAR、EAR、WAR格式的基础
从网络中获取
典型应用为Web Applet
运行时计算生成
如动态代理技术
在java.lang.reflect.Proxy中,就是用了ProxyGenerator.gengrateProxyClass()来为特定接口生成形式为"*$Proxy"的代理类的二进制字节流
由其他文件生成
JSP
由JSP文件生成对应的Class文件
从数据库中读取
中间件服务器(SAP Netweaver)可以选择吧程序安装到数据库中来完成程序代码在集群间的分发
从加密文件中获取
防止Class文件被反编译的保护措施,通过加载时解密Class文件来保障运行逻辑不被窥探
非数组加载
是开发人员可控性最强的阶段。
可以使用JVM内置的引导类加载器完成,也可以使用用户自定义的类加载器完成(重写一个类加载器的findClass()或loadClass()方法)
数组加载
数组类本身不通过类加载器创建,由JVM直接在内存中动态构造/
数组类的元素类型最终还是要靠类加载器来完成加载
数组类创建过程规则
- 如果数组的组件类型是引用类型,采用递归定义的加载过程去加载这个组件类型
- 不是引用类型(int[]),JVM会吧数组标记为与引导类加载器关联
- 数据类可访问性与他的组件类型的可访问性一致
总结
加载结束后,JVM外部的二进制字节流按照虚拟机所设定的格式存储在方法区之中,方法去中的数据存储格式完全由JVM的实现自行定义
类型数据安置在方法区后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的类型数据的外部接口
验证
目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害JVM自身的安全
Java语言本身比较安全,但字节码文件却不一定安全。如果不检查输入的字节流,可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃
验证阶段的工作量在JVM的类加载过程中占了相当大的比重,大致上会有4个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
文件格式验证
是否符合Class文件格式的规范,并且能被当前版本的JVM处理
目的
保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
通过该阶段后,字节流才被允许进入JVM内存的方法区中进行存储
元数据验证(数据类型验证)
对字节码描述的信息进行语义分析,确保其符合《Java语言规范》的要求
- 验证信息
1.该类是否有父类(除了Object之外,所有类都应该有父类)
2.这个类的父类是否继承了不被允许继承的类(final修饰的类)
3.如果不为抽象类,是否实现了其父类或接口之中要求实现的所有方法
4.类中的字段、方法是否与父类产生矛盾(覆盖了父类的final字段,或者出现不符合规则的方法重载)
字节码验证(方法体验证)
通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的,保证方法在运行时不会出现危害虚拟机的安全行为
- 确保的安全行为
1.保证任意时刻操作数栈的数据类型与指令代码都能配合工作(例如防止栈放置了int类型,使用时却按long类型啦加载如本地变量表中)
2.保证任何跳转指令都不会跳转到方法体以外的字节码指令上
3.保证方法体重的类型转换总是有效的
总结
即使再严密的检查也不能保证其就一定是安全的,通俗解释即为通过程序去校验程序逻辑是无法做到绝对准确的,不可能使用程序来准确判定一段程序是否存在BUG
符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候
- 校验内容
1.符号引用中通过字符串描述的全限定名是否能找到对应的类
2.在指定类中是否存在符合方法的字段描述及简单名称所描述的方法和字段
3.符号引用中的类、字段、方法的可访问性(private、protected、public等)是否可被当前类访问
验证总结
验证是一个费城重要的、但却不是必须要执行的阶段。
如果程序运行的全部代码都已经被反复使用和验证过,再生产环境的实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
准备
为类中定义的变量(静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
被分配在方法区
分配的仅包括类变量,不包括实例变量
解析
JVM将常量池内的符号引用替换为直接引用的过程
符号引用(Symbolic References)
用一组符号来描述所引用的目标,符号可以实任何形式的字面量。只要使用时能无歧义地定位到目标即可。
引用的目标不一定是已经加载到虚拟机内存当中的内容
直接引用(Direct References)
可以直接指向目标的指针、相对偏移量或者是一个能简介定位到目标的句柄
引用的目标必定已经再虚拟机的内存中存在
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行,分别对应于常量池的8种常量类型。
后4种和动态语言支持密切相关
类或接口的解析
初始化
类加载的最后一个步骤,除了在加载阶段用户应用应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全有Java虚拟机来主导控制
初始化阶段就是执行类构造器
()方法执行过程
()方法由编译器自动收集类中的多有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在其之后的变量,在签名的静态语句块可以赋值,但不能访问
()方法与类的构造函数( ()方法)不同,其不需要显式地调用父类构造器 - 父类的
()方法先执行,则父类中静态语句要优先与子类的变量赋值操作 ()方法对于类或接口来说不是必需的 - 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成
()方法,但接口的 ()方法执行不需要先执行父类的 ()方法 - JVM必须保证一个类的
()方法在多线程环境中被正确地加锁同步,如果多个线程同时初始化一个类,那么只会有其中一个线程取执行这个类的 ()方法,其他线程都需要阻塞等待,直到 ()方法执行完毕
类加载器
- 定义
通过一个类的全限定名来获取描述该类的二进制字节流,这个动作在JVM外部实现,以便让应用程序自己决定如何取获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader )
类与类加载器
类加载器虽然只用于实现类的加载动作,但其在Java程序中起到的作用远超类加载阶段。
比较2个类是否“相等”,只有在这2个类是由同一个类加载器加载的前提下才有意义,否则,即时这2个文件来源于同一个Class文件,被同一个JVM加载,只要类加载器不同,那么这2个类就必定不相等
- “相等”包括:
1.Class对象的equals()方法
2.isAssignableForm()方法
3.isInstance()方法
4.instanceof关键字
/**
* @Author jtao
* @Date 2021/1/27 21:38
* @Description 不同的类加载器对instanceof关键字运算的结果的影响
*/
public class ClassLoaderTest {
public static void main(String[] args) throws Exception{
ClassLoader myLoader=new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName=name.substring(name.lastIndexOf(".")+1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if(is==null){
return super.loadClass(name);
}
byte[] b=new byte[is.available()];
is.read(b);
return defineClass(name,b,0,b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("第七章.类加载器.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof 第七章.类加载器.ClassLoaderTest);
}
}
如上图,通过自己实现的类加载器,虽然Class相同,但ClassLoader不同,相比较也是不同的,有2个相同名字的,但类不同的ClassLoaderTest类
双亲委派模型
JVM角度
总体可以分为2种不同的类加载器:
- 1.启动类加载器(Bootstrap ClassLoader),使用C++语言实现,JVM内部
- 2.其他所有类加载器,都是由Java语言实现,JVM外部,都继承自java.lang.ClassLoader
Java开发人员角度
自JDK1.2依赖,保持者三层类加载器、双亲委派的类加载架构
JDK8及之前版本的三层类架构和双亲委派模型
3个系统提供的类加载器
- 启动类加载器(Bootstrap Class Loader)
存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径存放,而且是Java虚拟机能够识别的(按照文件名识别,如jt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机中的内存中 - 扩展类加载器(Extension Class Loader)
在类sun.mise.Launcher$ExtClassLoader中以Java代码的形式实现的。负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所制定的路径中所有的类库
可以在程序中使用扩展类加载器来加载Class文件
- 应用程序类加载器(Application Class Loader)
由sun.misc.Launcher$AppClassLoader来实现
可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中的默认的类加载器
双亲委派模型工作过程
还可以加入自定义的类加载器来进行扩展(如除磁盘位置外的Class文件来源),或者通过类加载器实现类的隔离、重载等功能
各种类加载器之间的层次关系被称为类加载器的“双亲委派模型”
-
工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己取尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,所有的加载请求最终都应该传送到最顶层的启动类夹在其中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载 -
好处
无论哪一个类加载器要加载类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。如果没有使用双亲委派模型,那么就会出现多个不同的Object类,Java类型体系中最基础的行为也无法保证 -
原理
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
代码的逻辑为:先检查请求加载的类型是否已经被加载过,如没有则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。假如父类加载器加载失败,抛出ClassNotFoundException异常,调用自己的findClass()方法尝试进行加载
破坏双亲委派模型
该模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。
Java模块化系统
//TODO
Comments | 0 条评论