jvm中类加载

一、

首先,java源文件->java编译机->Class文件->ClassLoader类加载器->类加载

其中类加载又分为:加载->验证->准备->解析->初始化->使用->卸载

二、研究类加载机制的意义

从上图可以看出,类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指导开发者采取更有效的措施配合程序执行。

研究类加载机制的第二个目的是让程序能动态的控制类加载,比如热部署等,提高程序的灵活性和适应性。
 
三、类加载的一般过程
原理:双亲委托模式
1、寻找jre目录,寻找jvm.dll,并初始化JVM;
2、产生一个Bootstrap Loader(启动类加载器);
3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。
4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
5、最后由AppClass Loader加载HelloWorld类。
 
四、类加载器的特点
1、运行一个程序时,总是由AppClass Loader(系统类加载器)开始加载指定的类。
2、在加载类时,每个类加载器会将加载任务上交给其父,如果其父找不到,再由自己去加载。 
3、Bootstrap Loader(启动类加载器)是最顶级的类加载器了,其父加载器为null.
 
五、类加载器的获取
很容易,看下面例子
public  class HelloWorld {  
         public  static  void main(String[] args) {  
                 HelloWorld hello =  new HelloWorld();  
                 Class c = hello.getClass();  
                 ClassLoader loader = c.getClassLoader();  
                 System.out.println(loader);  
                 System.out.println(loader.getParent());  
                 System.out.println(loader.getParent().getParent());  
         }  
}
打印结果:
sun.misc.Launcher$AppClassLoader@19821f  
sun.misc.Launcher$ExtClassLoader@addbf1  
null  
从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是Bootstrap Loader(启动类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
 
六、类的加载
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过Class.getClassLoader() .loadClass()方法动态加载
三种方式区别比较大,看个例子就明白了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package  zhongqiu.common.base;
 
public  class  ClassLoadDemo {
     static  {
         System.out.println( "ClassLoadDemo静态初始化块执行了!" );
     }
 
     public  static  void  main(String[] args)  throws  ClassNotFoundException {
         ClassLoader loader2 = ClassLoadDemo. class .getClassLoader();
         System.out.println(loader2);
         // 使用ClassLoader.loadClass()来加载类,不会执行初始化块
         // loader2.loadClass("zhongqiu.test.Test");
         // 使用Class.forName()来加载类,默认会执行初始化块
         // Class.forName("zhongqiu.test.Test");
         // 使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
         Class.forName( "zhongqiu.test.Test" false , loader2);
     }
}

 

七、自定义ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package  zhongqiu.common.base.classload;
 
import  java.net.MalformedURLException;
import  java.net.URL;
import  java.net.URLClassLoader;
 
public  class  MyClassLoader {
     @SuppressWarnings ( "resource" )
     public  static  void  main(String[] args)
             throws  MalformedURLException, ClassNotFoundException, IllegalAccessException, InstantiationException {
         URL url =  new  URL( "file:/D:/javaworkspace/JavaCommon/src/" );
         ClassLoader myloader =  new  URLClassLoader( new  URL[] { url });
         Class c = myloader.loadClass( "zhongqiu.common.base.classload.Test" );
         Test t3 = (Test) c.newInstance();
     }
}

Java.lang包里有个ClassLoader类,ClassLoader 的基本目标是对类的请求提供服务,按需动态装载类和资
源,只有当一个类要使用(使用new 关键字来实例化一个类)的时候,类加载器才会加载这个类并初始化。
一个Java应用程序可以使用不同类型的类加载器。例如Web Application Server中,Servlet的加载使用开发
商自定义的类加载器, java.lang.String在使用JVM系统加载器,Bootstrap Class Loader,开发商定义的其他类
则由AppClassLoader加载。在JVM里由类名和类加载器区别不同的Java类型。因此,JVM允许我们使用不同
的加载器加载相同namespace的java类,而实际上这些相同namespace的java类可以是完全不同的类。这种
机制可以保证JDK自带的java.lang.String是唯一的。

 

八、为什么要使用这种双亲委托模式呢?

  1. 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  2. 考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时被加载,所以用户自定义类是无法加载一个自定义的ClassLoader。

详解类加载全过程:

扫描二维码关注公众号,回复: 490264 查看本文章

 

加载

 

在加载阶段,虚拟机需要完成以下3件事情:

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

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问接口。

  非数组类的加载是可控性最强的。用户除了使用系统提供的引导类加载类来完成,也可以由用户自定义的类加载器去加载(重写一个类加载器的loadClass())。 

  注意:数组类本身不通过类加载器创建,它是由JVM直接创建的。但数组类和类加载器仍有很紧密的关系,因为数组类的元素类型最终是靠类加载器去创建。 

  加载完成后,虚拟机外部的二进制字节流就按照虚拟机所需格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义。然后在内存中实例化一个java.lang.Class类的对象(可以在Java堆中,也可以在方法区中),该对象将作为程序去访问方法区中的这些类型数据的外部接口。 

  加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未结束,连接阶段就可能开始了。但是夹在加载阶段进行的动作,仍然属于连接阶段的内容。

 

验证

  验证是连接的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危及虚拟机本身的安全。 

验证阶段的四个步骤:文件格式检验、元数据检验、字节码检验、符号引用检验。

1)文件格式检验: 

检验字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。检验可能包含下列几种:是否以魔数开头、主次版本号是否在虚拟机的处理范围之内,常量池中的常量是否不被支持、文件是否被删除或附加什么信息等等。 

只有通过文件格式检验的二进制字节流才能进入内存的方法区进行存储,所以后面的3个检验阶段都是基于方法区的存储结构进行的,不会在操作字节流。

2)元数据检验: 

对字节码描述的信息进行语义分析,以保证其描述的内容符合Java语言规范的要求。 

验证点包括:是否有父类(除了object)、父类是否继承了不可被继承的类(被final修饰的类)、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法、类中的方法和字段是否与父类产生矛盾(覆盖了父类的final字段、出现不合规矩的方法重载等)。 

元数据检验主要是对类的元数据信息进行语义校验,保证不符合Java语言规范的元数据信息不存在。

3)字节码检验: 

通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。第二阶段是对元数据信息中的数据类型做了检验,这一阶段是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。 

检验点包括:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证指令跳转不会跳转到方法体之外的地方、保证方法体内的类型转换都是有效的。 

事实上,即便是经过字节码检验后的方法体也不一定是安全的。

4)符号引用检验 

最后一个检验发生在虚拟机将符号引用转化为直接引用时,这个转化动作将在连接的第三阶段–解析阶段中发生的。符号引用检验可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 

校验点:符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段、符号引用中的类、字段、方法的访问权限是否能让当前类访问到等。 

符号引用检验的目的是确保解析动作的正常执行,如果无法通过符号引用检验,将会抛出java.lang.IncompatibleClassChangeError异常的子类,如IllegalAccessError、NoSuchfiledError、NoSuchMethodError等。

 

准备

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段。这些变量所使用的内存将在方法区中进行分配。此时进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。另外,在这里分配的静态类变量是将其值定义为0等默认值,而不是我们定义的。因为这时尚未执行任何Java方法,我们定义的赋值的putStatic指令是程序被编译后,存放在类构造器()方法中,所以正确的赋值将在初始化阶段执行。 

  如果类变量被final修饰,那么在这种情况下,在编译时Javac将会为该变量生成ConstantValue属性,在准备阶段虚拟机会根据该属性设置类变量的正确值。

 

解析

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。 

a、符号引用:以一组符号来描述所引用的目标,符号可以是任何形式字面量,只要使用时无歧义地定位到目标就行。 

b、 直接引用:直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。引用的目标已经在内存中存在。 

  虚拟机实现可以根据需要来判断到底在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用时才去解析它。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

 

初始化

类加载的最后一步,真正执行类中定义的Java程序代码(字节码)。 

初始化阶段是执行类构造器()方法的过程,根据程序员通过程序制定的主观的计划去初始化类变量和其他资源。

 

猜你喜欢

转载自1181731633.iteye.com/blog/2367249