定义:
过程:
优点:高度的灵活性。Java中可以动态扩展的语言特性就是依赖 运行期间动态加载和动态连接 这个特点实现的
缺点:类加载时的性能开销。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括7个阶段
这些阶段的“开始”时机是按照这个顺序
加载 -> 验证 -> 准备 -> 初始化 -> 卸载
这五个阶段的顺序是确定的
“解析”阶段的开始时机是不确定的,在某些情况下可以在初始化阶段之后再开始
可通过静态代码块检查类是否加载
static {
...
}
虚拟机开始类加载的时机规范中没有强制约束,但在以下有且只有5种情况下如果类没有进行初始化,则必须立刻对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)。
(1)遇到new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时。生成这四条指令的常见Java代码场景是:
1⃣使用new
关键字实例化对象的时候
2⃣读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候
3⃣调用一个类的静态方法的时候
(2)使用java.lang.reflect
包的方法对类进行反射调用的时候
(3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类)、虚拟机会先初始化这个主类
(5)当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果是REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化
主动引用与被动引用
(1)主动引用:上述五种场景中的行为称为对一个类的主动引用
(2)被动引用:除上述情况外,所有引用类的方式都不会触发初始化,称为被动引用
被动引用的例子:
1⃣通过子类引用父类的静态字段,不会导致子类初始化
2⃣通过数组定义来引用类,不会触发此类的初始化
//不会触发SuperClass类的初始化
//因为创建动作是由字节码指令 newarray 触发
SuperClass[] sca = new SuperClass[10];
3⃣常量在编译阶段会存入调用类的常量池(常量传播优化)中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
加载阶段需要完成的三件事
(1)通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等)
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区的这个类的各种数据的访问入口
非数组类与数组类的加载(加载阶段中获取类的二进制字节流的动作)
(1)非数组类
loadClass()
方法)(2)数组类
数组类创建过程需遵循的规则
(1)如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就采用普通的加载过程去加载这个组件类型,数组类将在加载该组件类型的类加载器的类名称空间上被标识(一个类必须与类加载器一起确定唯一性)
(2)如果数组的组件类型不是引用类型(eg:int[] 数组),Java虚拟机将会把原数组标记为与引导类加载器关联
(3)数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public
结果
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中
然后在内存中实例化一个java.lang.Class
类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口
目的:为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
执行性能:验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分
验证不通过:如果验证到输入的字节流不符合Class文件格式的约束,虚拟机就应抛出一个java.lang.VerifyError
异常或其子类异常
过程(四个阶段):
(1)文件格式验证(检查字节流)
目的:验证字节流是否符合Class文件格式的规范(保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求),并能被当前版本的虚拟机处理
内容:是否以魔数0xCAFEBABE
开头、主次版本号是否在当前虚拟机处理范围之内、常量池的常量中是否有不被支持的常量类型…
特点:本阶段的验证是基于二进制字节流进行的,通过这个阶段的验证后,字节流才会进入内存的方法区中进行存储。所以后面的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流
(2)元数据验证(检查数据类型)
目的:对字节码描述的信息进行语义分析(语义校验),以保证其描述的信息符合Java语言规范的要求
内容:这个类是否有父类、这个类的父类是否继承了不允许被继承的类(被final修饰的类)、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法…
(3)字节码验证(检查类的方法体)
目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
内容:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证跳转指令不会跳转到方法体以外的字节码指令上、保证方法体中的类型转换是有效的…
将字节验证的类型推导转变为类型检查:为避免过多的时间消耗在字节码验证阶段,给方法体的Code属性的属性表中增加了一项名为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块(Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可
(4)符号引用验证(检查常量池中的信息)
发生时机:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生
目的:对类自身以外(常量池中的各种符号引用)的信息进行匹配性检验,确保解析动作能正常执行
内容:符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定的类中是否存在符合方法访问的字段描述父以及简单名称所描述的方法和字段、在符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
结果:如果无法通过符号引用验证
作用:正式为类变量分配内存并设置类变量初始值的阶段
这些变量所使用的内存都将在方法区中进行分配
本阶段进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量。实例变量将会在对象实例化时随着对象一起分配在Java堆中
本阶段将会对类变量第一次赋初值
//此阶段value被赋的初值是默认的int零值,0
public static int value = 123;
//被final修饰的类变量含有ConstantValue属性,因此此阶段就已经被赋予了值123
public final static int value = 123;
作用:虚拟机将常量池内的符号引用替换为直接引用的过程
类别 | 定义 | 指向目标与内存情况 |
---|---|---|
符号引用 | 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可 | 符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中 |
直接引用 | 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄 | 直接引用是和虚拟机实现的内存布局相关的,如果有了直接引用,那引用的目标必定已经在内存中存在 |
发生时机:规范中并未规定解析阶段发生的具体时间,只要求在执行以下16个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析
anewarray
checkcast
getfield
getstatic
instanceof
invokedynamic
invokeinterface
invokespecial
invokestatic
invokevirtual
ldc
ldc_w
multianewarray
new
putfield
putstatic
对同一个符号引用进行多次解析
(1)除invokedynamic
指令以外的指令(静态的):虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行
(2)对于invokedynamic
指令(动态的):之前解析的结果不一定对后续相同的invokedynamic
指令生效,此指令目的就在于动态语言支持,必须等到程序实际运行到这条指令的时候,解析动作才能进行。
Java是一门强类型、静态类型语言
类型 | 特点 | 例子 |
---|---|---|
静态类型 | 数据类型是在编译其间检查的。在写程序时要声明所有变量的数据类型 | python、ruby、脚本语言 |
动态类型 | 在运行期间才去做数据类型检查。在用动态类型的语言编程时,永远也不用给任何变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来 | C#、Java |
类型 | 特点 | 优缺点 |
---|---|---|
强类型 | 强制数据类型定义的语言。一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了 | 编译速度稍差,类型安全 |
弱类型 | 数据类型可以被忽略的语言。一个变量可以赋不同数据类型的值 | 编译速度较快,类型不安全 |
解析的对象(七类符号引用):
对象 | 常量池中的常量类型 |
---|---|
类或接口 | CONSTANT_Class_info |
字段 | CONSTANT_Fieldref_info |
类方法 | CONTANT_Methodref_info |
接口方法 | CONTANT_InterfaceMethordref_info |
方法类型 | CONSTANT_MethodType_info |
方法句柄 | CONSTANT_MethodHandle_info |
调用点限定符 | CONSTANT_InvokeDynamic_info |
(1)类或接口的解析(最后需进行权限验证):
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,需要如下三个步骤
1⃣ 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C
2⃣ 如果C是一个数组类型,并且数组的元素类型是对象,则会按照1中的规则去加载数组元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象
3⃣ 如果1⃣2⃣没有出现任何异常,则C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前需要进行符号引用验证,确认D是否具备对C的访问权限,如果发现不具备访问权限,将抛出java.lang.IllegalAccessError
(2)字段解析(最后需进行权限验证)
要解析一个未被解析过的字段符号引用,首先会对字段表内的class_index
项中索引的CONSTANT_Class_info
符号引用进行解析,也就是字段所属的类或接口的符号引用。
如果解析成功,将这个字段所属的类或接口用C表示,需要如下步骤对C的后续字段进行搜索
1⃣ 查自身:如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段。则返回这个字段的直接引用,查找结束。
2⃣ 查接口实现:否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接饮用,查找结束。
3⃣ 查父类继承:否则,如果C不是java.lang.Object
的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
4⃣ 否则,查找失败,抛出java.lang.NoSuchFieldError
异常
(3)类方法解析(最后需进行权限验证)
要解析一个未被解析过的类方法,首先会对类方法表内的class_index
项中索引的CONSTANT_Class_info
符号引用进行解析,也就是类方法所属的类或接口的符号引用。
如果解析成功,使用C表示这个类方法所属的类,按如下步骤进行后续类方法搜索
1⃣ 查是否是接口:类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法中发现class_index
中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError
异常
2⃣ 查自身:如果确认该C为一个类,则在类C中查找是否有简单名称和描述符斗鱼目标相匹配的方法,如果有则返回这个方法的直接引用
3⃣ 查父类:否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
4⃣ 查是否为抽象类:否则,在类C实现的接口列表及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,查找结束,抛出java.lang.AbstractMethodError
异常
5⃣ 否则,宣告方法查找失败,抛出java.lang.IllegalAccessError
(4)接口方法解析(无需权限验证,接口方法默认public)
要解析一个未被解析过的接口方法,首先会对接口方法表内的class_index
项中索引的CONSTANT_Class_info
符号引用进行解析,也就是接口方法所属的类或接口的符号引用。
如果解析成功,使用C表示这个接口方法所属的接口,按如下步骤进行后续接口方法搜索
1⃣ 检查是否是类:如果在接口方法表中发现class_index
中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError
2⃣ 查自身:否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
3⃣ 查父接口:否则,在接口C的父接口中递归查找,直到java.lang.Object
(查找范围包括Object类)类为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
4⃣ 否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError
作用:开始真正执行类中定义的Java程序代码(或者说字节码)
在准备阶段,变量已赋过一次系统要求的初值
初始化阶段,是根据程序员通过程序指定的主观计划去初始化类变量和其他资源
()
方法的过程
方法执行过程的特点:
(1)
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}
块)中的语句合并产生的。
public class Test {
static {
i = 0; //给变量赋值可以正常编译通过
System.out.print(i); //这句编译器会提示“非法的向前引用”
}
static int i = 1;
}
(2)
方法与类的构造函数(或者说实例构造器
方法)不同,它不需要显式的调用父类构造器(super),虚拟机会保证在子类的
方法执行之前,父类的
方法已经执行完毕
()
方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作(3)
方法对于类或接口来说并不是必需的。
()
方法(4)接口与类一样都会生成
方法。
原因:接口中不能使用静态语句块,但仍然有变量初始化的赋值操作
接口与类的区别1⃣ :执行接口的
方法不需要先执行父接口的
方法。只有当父接口中定义的变量使用时,父接口才会初始化。
接口与类的区别2⃣ :接口的实现类在初始化时也一样不会执行接口的
方法
(5)虚拟机会保证一个类的
方法在多线程环境中被正确的加锁、同步
如果有多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
方法,其他线程都需要阻塞等待,直到活动线程执行
方法完毕
如果
方法中有耗时很长的操作,可能造成多个进程阻塞。
如果存在线程阻塞的情况,当执行
方法的那条线程退出
方法后,其他线程唤醒之后不会再次进入
方法。因为同一个类加载器下,一个类型只会初始化一次。
判定类是否“相等”
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间
“相等”:代表类的Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括使用instanceof
关键字做对象所属关系判定等情况
比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
从Java虚拟机的角度,类加载器有2种
(1)启动类加载器(Bootstrap ClassLoader)
(2)所有其他的类加载器
java.lang.ClassLoader
从Java开发人员角度,类加载器有3种
(1)启动类加载器(Bootstrap ClassLoader)
使用方式:无法被Java程序直接引用。用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader();
if (cl == null) {
return null;
}
}
作用:负责将存放在
目录中的、并且是被虚拟机识别的(仅按照文件名识别)类库加载到虚拟机内存中
(2)扩展类加载器(Extension ClassLoader)
使用方式:开发者可直接使用
作用:由sun.misc.Launcher$ExtClassLoader
实现,负责加载
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库
(3)应用程序类加载器(Application ClassLoader)
别名:系统类加载器
使用方式:开发者可以直接使用。如果引用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
作用:由sun.misc.Launcher$AppClassLoader
实现。是ClassLoader中的getSystemClassLoader()
方法的返回值,负责加载用户路径(ClassPath)上所指定的类库
要求:
工作过程:
如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成
每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
好处:
Java类随着它的类加载器一起具备了一种带有优先级的层次关系
eg:类java.lang.Object
,它存放在rt.jar
之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
逻辑:
位置:实现双亲委派的代码集中在java.lang.ClassLoader
的loadClass()
方法中
(1)检查是否被加载过
(2)若没有加载则调用父加载器的loadClass()
(3)如果父类加载失败,抛出ClassNotFoundException
异常后,再调用自己的finaClass()
方法进行加载
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();
//调用自己的findClass()加载
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;
}
}
沙箱机制
java.lang.String
的类,由于双亲委派机制的原理,此请求会先交给Bootstrap试图进行加载,但是Bootstrap在加载类时首先通过包和类名查找rt.jar中有没有该类,有则优先加载rt.jar包中的类,因此就保证了java的运行机制不会被破坏.第一次被破坏
原因:为了向前兼容
JDK1.2后,不提倡用户覆盖loadClass()
方法,而应当把自己的类加载逻辑写到findClass()
方法中
loadClass()
方法的逻辑例如果父类加载失败,则会调用自己的findClass()
方法,从而确保新写的类加载器是符合双亲委派规则的。第二次被破坏
原因:双亲委派模型自身的缺陷
问题:基础类要回调用户的代码(eg:JNDI服务对资源进行集中管理和查找)
解决:引入线程上下文类加载器(Thread Context ClassLoader)
违背了双亲委派机制,使父类加载器请求子类加载器去完成类加载
这个类加载器可以通过java.lang.Thread
类的setContextClassLoader()
方法进行设置
如果创建线程时还未设置,它将会从父线程中继承一个
如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器
第三次被破坏
原因:用户对程序动态性的追求
目前OSGi成为业界事实上的Java模块化标准
在OSGi环境下,类加载器不再是双进委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。当收到类加载请求时,OSGi将按照下面的顺序进行类搜索(1⃣2⃣:双亲委派,其它:平级类加载器中的类查找)
1⃣ 将以java.*开头的类委派给父类加载器加载
2⃣ 否则,将委派列表名单内的类委派给父类加载器加载
3⃣ 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载
4⃣ 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
5⃣ 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
6⃣ 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
7⃣ 否则,类查找失败