JVM(五). 虚拟机类加载(双亲委派)
1.概述
上一章节说到Class 文件的格式细节,不仅仅限定于文件,也可以通过网络,内存,数据库 中加载的一串二进制流,只要瞒住Class文件的格式,就可以被虚拟机加载;Java虚拟机把符合Class格式的数据加载到内存,经过校验,转换,解析,和初始化,最后被虚拟机可以直接使用的Java类,这个过程叫做虚拟机的类加载机制;类的加载都是在程序的运行期间完成的;
2.什么时候才进行类加载
一个类从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载 Loading、验证Verification、准备Preparation、解析Resolution、初始化 Initialization、使用Using和卸载Unloading七个阶段,其中验证、准备、解析三个部分称为连接Linking
第一阶段加载《Java虚拟机规范》中没有指定,但是严格规定了有且只有六种情况必须立即对类进行 初始化(但是你想,都初始化了,肯定经过第一步加载了吧);初始化的时机为:
- 执行
new、getstatic、putstatic或invokestatic
这四条字节码指令时;如果类没有初始化,就触发初始化。源代码触发这几条指令的代码有:- new 对象
- 读取 static (除了final 修饰的)静态类
- 调用静态方法
- 反射对类进行了调用,如果没有初始化则触发初始化;
- 如果有父类,先初始化父类
- 虚拟机启动,先初始化main 方法的类
- 当使用
JDK 7
新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic、REF_putStatic,REF_invokeStatic、REF_newInvokeSpecial
四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化; JDK 8
接口中有 default ,这个接口的实现类触发了初始化,这个接口要先被初始化;
示例
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 123;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
注:对于静态字段, 只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化
public static void main(String[] args) {
System.out.println(SubClass.value);
}
//输出
SuperClass init!
123
注:数组类型不会初始化 ,虚拟机会自动生成 类的数组类
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
Class<? extends SuperClass[]> aClass = sca.getClass();
System.out.println(aClass);
System.out.println(1);
}
//输出
class [Lcom.jvm.chapter7.SuperClass; //类的数组类
1
注:常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化
public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
//ConstClass.HELLOWORLD
public class NotInitialization_3 {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);//编译时 ConstClass.HELLOWORLD 已经存在本类的 常量池中
}
}
3.类加载过程
3.1 加载
类加载 Class Loading过程中的第一个阶段,虚拟机需要完成
- 类的全限定名获取(可以是jar,war ,网络,动态代理生成,文件)此类的二进制字节流
- 字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
3.2 验证
是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,这些信息被当作代码运行后不会危害虚拟机自身的安全;四个阶段的检验动作:文件格式验证、元数据验证、字节码验证和符号引用验证
文件格式验证
流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理:魔数,主次版本,常量池,常量池引用数据准确性 ……;这些之后,正确的话,存储于方法区之内,后续步骤直接操作这个存储结构,就不需要读取二进制字节流;
元数据验证
字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求;是否有父类,父类是否继承了不允许被继承的类;类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法;
字节码验证
复杂的一个阶段;确定程序语义是合法的、符合逻辑的。这阶段就要Class文件中的Code属性进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为;跳转指令是否正常,操作数栈的数据类型与指令代码序列都能配合工作……;
符号引用验证
该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源:全限定名是否能找到对应的类,指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段,符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问 ……;
3.3 准备
准备阶段是正式为类中定义静态static 变量,分配内存并设置类变量初始值的阶段;
数据类型的零值
数据类型 | 零值 | 数据类型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | '\u0000' | reference | null |
byte | (byte)0 |
3.4 解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程;
1.类或接口的解析 2.字段解析 3.方法解析 4.接口方法解析
3.5 初始化
初始化阶段,Java虚拟机才真正开始执行类中编写的Java程序代码,将主导权移交给应用程序;是执行类构造器();
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的 语句合并产生的;
注意:Java虚拟机会保证在子类的()方法执行前,父类的()方法已经执行完毕。因此在Java虚拟机中第一个被执行的()方法的类型肯定是java.lang.Object
static int a = 13;
static {
System.out.println("SubClass init!");
}
//编译后
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 7 L0
BIPUSH 13
PUTSTATIC com/jvm/chapter7/SubClass.a : I
L1
LINENUMBER 9 L1
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "SubClass init!"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L2
LINENUMBER 10 L2
RETURN
MAXSTACK = 2
MAXLOCALS = 0
4. 类加载器
通过一个类的全限定名来获取描述该类的二进制字节流;类加载器干的就是这个活Class Loader
4.1 类与类加载器
JAVA 类的唯一性是由类本身和加载这个类的加载器决定的;比如说是一个Class 文件,是由两个不同的类加载器加载的,对应类对象的equal() instance等方法就是不相等的
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("com.jvm.chapter7.ClassLoaderTest").newInstance();
System.out.println(obj instanceof ClassLoaderTest);//false
}
}
4.2 双亲委派模型
虚拟机角度两种虚拟机:
- C++实现的 启动类加载器(
Bootstrap ClassLoader
) 是虚拟机实现的一部分; - Java语言实现的,虚拟机外部,并且全都继承自抽象类
java.lang.ClassLoader
自JDK 1.2
以来,Java一直保持着三层类加载器、双亲委派的类加载架构;
- 启动类加载器(Bootstrap Class Loader):加载<JAVA_HOME>\lib目录或者
Xbootclasspath
参数的路径 并且是虚拟机按照文件名称识别的 比如rt.jar
,tools.jar
;启动类加载器不能被Java程序直接使用,如果某个类被加载且类加载器是null ,说明就是启动类加载器加载的; - 扩展类加载器(Extension Class Loader):这个类加载器是在类
sun.misc.Launcher$ExtClassLoader
中以Java代码的形式实现的(也是继承自抽象类java.lang.ClassLoader
),加载<JAVA_HOME>\lib\ext目录或者被java.ext.dirs
指定的路径中所有的类库。 - 应用程序类加载器/系统类加载器(Application Class Loader):这个类加载器由
sun.misc.Launcher$AppClassLoader
来实现(也是继承自抽象类java.lang.ClassLoader
)。 如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派的实现
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;
}
}
4.3 破坏双亲委派模型
三次破坏
- 第一次“被破坏”其实发生在双亲委派模型出现之前;
- 自身的缺陷导致:双亲委派为了保证基础类型的一致性问题,即是基础类不能被修改,但是基础类型又要调用回用户的代码,那该怎么办呢?
打破双亲委派
- 第三次“被破坏”是由于用户对程序动态性的追求而导致的,这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等;