您的位置:首页 > 编程语言 > Java开发

【Java JVM】Java 虚拟机类加载机制简单总结

2017-03-20 18:03 363 查看
下面内容大部分为阅读《深入Java虚拟机》一书第七章后的摘要总结

1类加载过程
1 加载

2 连接
21验证

22 准备

23 解析

3 初始化

2方法区与class对象

3类加载器

1、类加载过程

Java程序在编译后,生成.class格式的字节码文件,而class文件最终都需要加载到虚拟机中之后才能运行和使用。在Java中,虚拟机的类加载机制主要包括三个步骤:加载,连接(验证、准备、解析),初始化;类的全部加载过程完成之后,class文件中描述的类的信息会在虚拟机运行时的方法区存储,存储的结构由不同虚拟机决定;同时会在方法区生成一个对应的class对象,该对象完成了对方法区类信息的封装,外界可通过该对象来访问类的信息,从而创建实例,访问静态属性等等;下面简单介绍一下三个步骤分别完成了什么操作。具体信息参考《虚拟机类加载》一书第七章内容。

类从被加载到虚拟机内存开始,到卸载出内存为止,他的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段。其中验证、准备、解析3个部分称为连接,这七个阶段发生的顺序如下图所示:



1.1 加载

Java虚拟机规范中并没有对第一阶段加载过程的开始进行强制约束,这点可以交给虚拟机的具体实现来把握。

加载阶段,虚拟机需要完成三件事情:

(1)通过一个类的权限定名来获取定义此类的二进制字节流。

二进制字节流的来源有以下方式:.class文件;zip包中读取;网络中获取如Applet;运行时计算生成如动态代理;由其他文件生成如jsp应用;从数据库中读取;

(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,结构样式根据不同虚拟机区别;

(3)在内存中声称一个代表这个类的class对象,作为方法区这个类的各种数据的访问入口;

注意,加载阶段与连接阶段的部分内容是交叉进行的。

1.2 连接

连接阶段包括三个步骤,验证,准备和解析。

1.2.1验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。连接阶段与加载阶段同时进行,在加载阶段读取二进制字节流的过程需要对字节流进行验证工作。验证阶段会完成下面4个阶段的检验工作:文件格式验证;元数据验证;字节码验证;符号引用验证。

(1)文件格式验证:该验证的主要目的是保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是机遇二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中仅从存储,所以后面3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

(2)元数据验证:主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息;

(3)字节码验证:主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件;

(4)符号引用验证:发生在虚拟机符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段---解析阶段中发生。目的是确保解析动作能正常执行。

1.2.2 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里需要注意两点:

(1)这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

(2)这里说的初始值通常情况下是数据类型的零值;

1.2.3 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。包括:类或接口的解析;字段解析;类方法解析;接口方法解析;

1.3 初始化

初始化阶段,虚拟机规范则严格规定了,有且仅有如下几种情况必须对类进行初始化:

(1)使用new关键之实例化对象;

(2)读取活着设置一个类的静态字段、调用类的静态方法;

(3)反射调用;

(4)父类的初始化;

(5)虚拟机启动时指定的执行主类;

(6)当使用JDK1.7的动态语言支持;

以上行为都称为对一个类的主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用例如以下几种情况:

(1)通过子类引用父类的静态字段,不会导致子类的初始化;

(2)通过数组定义引用类;

(3)常量在编译阶段会存入调用类的常量池,因此不会导致被调用类的初始化;

类的初始化阶段,是执行类构造器()方法的过程。该方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。

分析完加载的所有过程后,我们以下图来更直观的感受下该过程。



类加载的完整过程,就是将二进制字节流代表的字节码读入内存,进行验证后以一定的数据结构存入方法区,并对类变量进行内存分配、赋值。之后进行符号引用到直接引用的解析过程。最终会在方法区声称该类对应的class对象,作为方法区该类各种信息的访问入口。

2、方法区与class对象

方法区:是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

字节码二进制流被加载到内存中,不同的虚拟机以不同的数据结构进行存储。

而class对象对方法区中的类的信息进行了包装,作为外界访问方法区中类数据的访问入口。

3、类加载器

负责读取字节码文件,将二进制流转换成对应的class对象。

用户可编写自己的类加载器,完成“通过一个类的全限定名来获取描述此类的二进制字节流”动作。虚拟机团队将类加载阶段的该动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。例如如下代码示例,定义自己的读取二进制流的方式。(摘抄自博客:http://jiangzuun2014.blog.51cto.com/8732469/1533979

public class MyClassLoader extends ClassLoader{
String path;//自定义类加载器所负责的文件夹

public MyClassLoader(String path) {
super();
this.path = path;
}

@SuppressWarnings("deprecation")
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//通过 文件输入流 读取 指定的class文件
String file = path+"/"+name+".class";
System.out.println(file);
try {
FileInputStream fis = new FileInputStream(file);
//将读取的class文件对应的 字节数据 写入到内存中
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i = 0;
while ((i = fis.read())!=-1) {
out.write(i);
}
fis.close();
byte[] buf = out.toByteArray();//提取 写到内存中的字节数据到数组
//  public byte[] toByteArray()创建一个新分配的 byte 数组。其大小是此输出流的当前大小,并且缓冲区的有效内容已复制到该数组中。
return defineClass(buf, 0, buf.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
}


public class MyCLassLoaderTest {
static Scanner in = new Scanner(System.in);

public static void main(String[] args) throws Exception {
System.out.println("需要加载的class文件所在文件夹的路径:");
String path = in.nextLine();// 需要加载的class文件的父路径

System.out.println("需要加载的class文件的文件名:");
String name = in.nextLine();
Class clazz = new MyClassLoader(path).loadClass(name);

//执行加载的class文件的main方法
//      Method met = clazz.getMethod("main", String[].class);
//      System.out.println(met.toString());
//      met.invoke(null, (Object)new String[]{});

// 通过自定义类加载器 加载任意目录下的指定class文件
Class clazz2 = new MyClassLoader(path).loadClass(name);
System.out.println("\r\n--------------列出该class恩件中的所有构造方法==========");
Constructor[] cons = clazz2.getConstructors();
for (Constructor constructor : cons) {
System.out.println(constructor.toString());
}
System.out.println("---------------列出该class恩件中的所有putong方法。。....。。");
Method[] methods = clazz2.getMethods();
for (Method method : methods) {
System.out.println(method.toString());
}
}
}


不同的类加载器负责加载不同加载路径下的Java类库;例如:

/*
* 1、Bootstrap Loader(启动类加载器):加载System.getProperty("sun.boot.class.path")所指定的路径或jar。
* 2、Extended Loader(标准扩展类加载器ExtClassLoader):加载System.getProperty("java.ext.dirs")所指定
* 的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld
* 3、AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty("java.class.path")所指定的路径或jar。
* 在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
* ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其
* 他搜索路径加载类,就要产生新的类加载器。
*
* 对于自定义类加载器,可以自定义加载路径,如上述示例代码所示
*/


对于相同类名空间的类,如果是用不同的类加载器加载,可以在虚拟机中共存;

因此加载器可以获取到父类加载器加载路径下的类,而无法加载到与其平级的加载器能加载到的类(例如tomcat中的webapp的加载器),从而可以做到类库的隔离与共享,可以参考tomcat等wab服务器如何利用类加载机制实现不同应用的类库隔离与共享的(《深入Java虚拟机第九章内容》)。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java 虚拟机