深入理解Java虚拟机5~虚拟机类加载机制

时间:2023-01-03 12:16:36

Java源代码被编译成class字节码,最终需要加载到虚拟机中才能运行。整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。

深入理解Java虚拟机5~虚拟机类加载机制

加载

1、通过一个类的全限定名获取描述此类的二进制字节流;
2、将这个字节流所代表的静态存储结构保存为方法区的运行时数据结构;
3、在java堆中生成一个代表这个类的java.lang.Class对象,作为访问方法区的入口;

虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,实现这个动作的代码称为“类加载器”,JVM提供了3种类加载器:
1、启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2、扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
3、应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

JVM基于上述类加载器,通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

深入理解Java虚拟机5~虚拟机类加载机制

双亲委派模型工作过程:当一个类加载器收到类加载任务,优先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试执行加载任务。

双亲委派模型有什么好处?
比如位于rt.jar包中的类java.lang.Object,无论哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,确保了Object类在各种加载器环境中都是同一个类。



1、概述

 Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这为java应用程序提供高度的灵活性。

例如:如果编写一个面向接口的应用程序,可用等到运行时再指定其实际的实现类;用户可以通过java预定义的和自定义的类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分。

2、类加载的时机

类的生命周期:

深入理解Java虚拟机5~虚拟机类加载机制

从上图中可以明显看出各个阶段是有顺序的,加载、验证、准备、初始化这个5个阶段的顺序是固定的,也就是说类的加载过程必须按照这种顺序按部就班开始;解析阶段则不一定,解析阶段的工作完全可能在初始化之后才开始,之所以这么设计,就是为了支持Java语言的动态绑定。还有一点需要注意的是,虽然上述的5个阶段可能按照顺序开始,但是并不是说一个接一个阶段完成后才开始,一个阶段的进行完全可能激活另一个阶段的进行,交叉混合式的进行

那么什么情况下需要开始类初始化呢,这就不得不涉及两个概念:主动引用和被动引用。根据Java虚拟机的规范,只有5中情况属于主动引用:

  1. 遇到new(使用new 关键字实例化一个对象)、getstatic(读取一个类的静态字段)、putstatic或者invokestatic(设置一个类的静态字段)这4条指令的时候,如果累没有进行过初始化。则需要先触发其初始化。

  2. 使用反射进行反射调用的时候,如果类没有初始化,则需要先触发其初始化。

  3. 当初始化一个类的时候,如果其父类没有初始化,则需要先触发其父类的初始化

  4. 程序启动需要触发main方法的时候,虚拟机会先触发这个类的初始化

  5. 当使用jdk1.7的动态语言支持的时候,如果一个java.lang.invoke.MethodHandler实例最后的解析结果为REF_getStatic、REF_pusStatic、REF_invokeStatic的方法句柄(句柄中包含了对象的实例数据和类型数据,句柄是访问对象的一种方法。句柄存储在堆中),并且句柄对应的类没有被初始化,那么需要先触发这个类的初始化。

5种之外情况就是被动引用。被动引用的经典例子有:

  1. 通过子类引用父类的静态字段 
    这种情况不会导致子类的初始化,因为对于静态字段,只有直接定义静态字段的类才会被触发初始化,子类不是定义这个静态字段的类,自然不能被实例化。

  2. 通过数组定义来引用类,不会触发该类的初始化 
    例如, Clazz[] arr = new Clazz[10];并不会触发。

  3. 常量不会触发定义常量的类的初始化 
    因为常量在编译阶段会存入调用常量的类的常量池中,本质上并没有引用定义这个常量的类,所以不会触发定义这个常量的类的初始化。

对于这5种主动引用会触发类进行初始化的场景,在java虚拟机规范中限定了“有且只有”这5种场景会触发类的加载。



3、类加载器 

       虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到java虚拟机外部去实现,

 以便让应用程序自己决定如何去获取所需要的类实现这个动作的代码模块称为“类加载器”。

tips1:即使两个类来源于同一个class文件,被同一个虚拟机加载,如果加载他们的类加载器不同,那么这两个类就必定不相等。

tips2:java.lang.ClassLoader


我相信编写过Applet程序的朋友都知道,通过Http协议我们可以从远程服务器上下载JAVA字节代码进行加载运行,这都归功与JAVA的自定义类加载器,Applet查看器内置了一个类加载器,它不是在本地加载类文件,而是通过Http协议从网络中加载。同时这个加载器还可以做其它事情,通过JAVA的沙箱来限制Applet的安全范围,并且不干扰其他的Applet程序。类加载器ClassLoader是负责加载类的对象。ClassLoader 类是一个抽象类。如果给定类的二进制名称,那么类加载器会试图查找或生成构成类定义的数据。ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。虚拟机的内置类加载器(称为 "bootstrap class loader")本身没有父类加载器,但是可以将它用作 ClassLoader 实例的父类加载器。

实例代码:

import java.io.*;  

class NetworkClassLoader extends ClassLoader {

private String host;
private int port;

public static void main(String[] args)throws Exception{

ClassLoader loader = new NetworkClassLoader("127.0.0.1", 4444);
Object pig = loader.loadClass("Pig.class").newInstance();
}

public NetworkClassLoader(){
}

public NetworkClassLoader(String host,int port){
this.host = host;
this.port = port;
}

//类加载器
public Class loadClass( String name, boolean resolve ) throws ClassNotFoundException{
// 目标Class
Class clas = null;

// 看是否已经加载该类
clas = findLoadedClass( name );


if(clas == null){
clas = findClass(name);
}

//如果class对象不存在则在系统中查找
if (clas==null) {
clas = findSystemClass( name );
}

if(clas == null){

throw new ClassNotFoundException("该类不存在1");
}

//是否需要分析该类
if (resolve && clas != null)
resolveClass( clas );
return clas;
}

//构造该类的Class对象
public Class findClass(String name){
try{
byte[] b = loadClassData(name);
if(b == null){
return null;
}
System.out.println("my findClass method");

return defineClass("Pig", b, 0, b.length);

}catch(IOException e){
System.out.print(e.getMessage());
}
return null;
}

//从网络加载加载类的二进制代码,此处我们使用本地文件
private byte[] loadClassData(String name)throws IOException{

//读取类文件
File file = new File(name);
if(!file.exists()){
return null;
}
FileInputStream input = new FileInputStream(file);
long length = file.length();
byte[] bt = new byte[(int)length];

int rl = input.read(bt);

if(rl != length){
throw new IOException("不能读取所有内容");
}
input.close();

return bt;
}
}

Class loadClass( String name, boolean resolve )方法是ClassLoader 的入口点。

使用指定的二进制名称来加载类。此方法的默认实现将按以下顺序搜索类:

l        调用 findLoadedClass(String) 来检查是否已经加载类。

l        在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。

l        调用 findClass(String) 方法查找类。

l        如果使用上述步骤找到类,并且 resolve 标志为真,则此方法将在得到的 Class 对象上调用 resolveClass(Class) 方法。

鼓励用 ClassLoader 的子类重写 findClass(String),而不是使用此方法。

finalClass<?>defineClass(String name,byte[] b,int off,int len)方法是 ClassLoader 的主要功能部件。该方法接受由编译后的字节的数组并把它转换成 Class对象。原始数组包含如从文件系统或网络装入的数据。

defineClass 管理 JVM 的许多复杂、神秘和倚赖于实现的方面 -- 它把字节码分析成运行时数据结构、校验有效性等等,类似编译器的自动机。而且你没有办法重写这个方法,正如你看到的,他是final类型。

final Class<?> findSystemClass(String name)方法从本地classpath中装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM的缺省类文件装载机制。

final Class<?> findLoadedClass(String name)方法用来判断类加载器是否已经加载了该类,避免不必要的资源浪费,建议首先调用该方法。