JVM之二-ClassLoader

类的生命周期


类从加载到卸载,它的整个生命周期包括:

1、加载(Loading)

2、链接(Linking):验证(Validation),准备 (Preparation),解析(Resolution),

3、初始化(Initialization),

4、使用(Using)

5、卸载 (Unloading)。

加载

在加载阶段,虚拟机主要完成三件事:

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

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

3。在堆中生成一个代表这个类的java.lang.Class对象。

在Java中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名包括包名和类名。但在JVM中一个类用其全名和一个加载类ClassLoader的实例作为唯一标识,不同类加载器加载的类将被置于不同的命名空间。

 

Java 虚拟机规范并没有对类装载时机做严格的定义,这就使得 JVM 在实现上可以根据自己的特点提供采用不同的装载策略。

一个应用程序总是由很多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制。

System classes are automatically loaded by the bootstrap class loader。To see which:

java -verbose:class Test.java

Arrays are created by the VM,not by a class loader。


VM预定义的三种类型类加载器,当一个 JVM 启动的时候,Java 缺省开始使用如下三种类型类加载器:

启动(Bootstrap)类加载器:启动类加载器是用本地代码实现的,它负责将jdk_home/lib目录下的核心api 或 -Xbootclasspath 选项指定的jar包加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。比如,如果我们试图得到一个核心 java 运行时类的一个类加载器,我们将得到 null 值,如下:
log(java.lang.String.class.getClassLoader());

标准扩展(Extension)类加载器:扩展类加载器是由 Sun 的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将jdk_home/lib/ext目录下的jar包或-Djava.ext.dirs 指定目录下的jar包加载到内存中。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器:系统类加载器是由 Sun 的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将-classpath或者-Djava.class.path所指的目录下的类与jar包加载到内存中。开发者可以直接使用系统类加载器。

UserCustom ClassLoader/用户自定义类加载器:java.lang.ClassLoader的子类,在程序运行期间, 通过java.lang.ClassLoader的子类动态加载class文件, 体现java动态实时类装入特性。

JVM在加载类时默认采用的是 双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

虚拟机出于安全等因素考虑,不会加载JDK_Home/lib目录下存在的陌生类开发者通过将要加载的非JDK自身的类放置到此目录下,期待启动类加载器加载是不可能的

Java类加载使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder装载一个类时,除非显式的使用另外一个ClassLoder,该类所依赖及引用的所有其它类也由这个ClassLoder载入;“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想如果一个人写了一个恶意的基础类(如java.lang.String)并加载到JVM将会引起严重的后果,但有了全盘负责制,java.lang.String永远是由根装载器来装载,避免以上情况发生。

扩展类加载器(ExtClassLoader)将会加载 java.ext.dirs 目录下的所有 .jar 文件。开发者可以为自己的应用增加新的 .jar 文件或者类库,只要他把它们添加到 java.ext.dirs目录下面。

当我们在命令行输入 java XX(.class)的时候,java.exe根据一定的规则找到JRE,接着找到JRE之中的JVM.DLL(真正的java虚拟机),最后载入这个动态链接库启动java虚拟机。
虚 拟机一启动先做一些初始化的动作,比如获取系统参数等。之后就产生第一个类加载器,即所谓的Bootstrap Loader。Bootstrap Loader是由C++编写的,这个Loader进行了一些初始化操作以后,最重要的是载入定义在sun.misc命名空间下的 Launcher.java之中的ExtClassLoader(因为是inner class ,所以编译之后变成Launcher$ExtClassLoader.class),并设定parent为null,代表其父加载器为Bootsrap Loader。然后,Bootstrap Loader再载入Launcher.java之中的AppClassLoader(同理为 Launcher$AppClassLoader.class)并设定parent为ExtClassLoader。

如何在运行时判断系统类加载器能加载哪些路径下的类?
一是可以直接调用ClassLoader.getSystemClassLoader()或者其他方式获取到系统类加载器(系统类加载器和扩展类加载器本身都派生自URLClassLoader),调用URLClassLoader中的getURLs()方法可以获取到;
二是可以直接通过获取系统属性java.class.path 来查看当前类路径上的条目信息 , System.getProperty("java.class.path")。

如何在运行时判断标准扩展类加载器能加载哪些路径下的类?

由于标准扩展类加载器是系统类加载器的双亲,所以可以用:
URL[] extURLs = ((URLClassLoader)ClassLoader.getSystemClassLoader().getParent()).getURLs();
for (int i = 0; i < extURLs.length; i++) {
     System.out.println(extURLs[i]);
}

验证

验证的作用是保证class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段:

1。文件格式验证:验证字节流文件是否符合class文件格式的规范。

2。元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。这次检查不需要查看字节码,也不需要查看和装载任何其他类型。在这趟扫描中,检验器查看每个组成部分,确认它们是否是其所属类型的实例,它们的结构是否正确。比如,方法描述符(它的返回类型,以及参数的类型和个数)在class文件中被存储为一个字符串,这个字符串必须符合特定的上下文无关文法。另外,还会检查这个类本身是否符合特定的条件,它们是由java编程语言规定的。比如,除Object外,所有类都必须要有一个超类,final的类不能被子类化,final方法也 没有被覆盖,检查常量池中的条目是合法的,而且常量池的所有索引必须指向正确类型的常量池条目。

3。字节码验证:主要是进行数据流和控制流的分析,保证类的方法在运行时不会危害虚拟机。字节码检验器必须确保类的字段中必须总是被赋予正确类型的值,类的方法被调用时总是传递正确数值和类型的参数。字节码检验器还必须保证每个操作码都是合法的,即都有合法的操作数,以及对每一个操作码,合适类型的数值位于局部变量中或是在操作数栈中。这些仅仅是字节码检验器所做的大量检验工作中的一小部分,在整个检验过程通过后,它就能保证这个字节码流可以被java虚拟机安全的执行。

4。符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转换在解析阶段中发生。符号引用验证确保这个引用是有效而且正确的。

准备

准备过程则是创建Java类中的静态域(non-final),并将这些域的值设为默认值(注意:不是指定的值)。准备过程不会执行代码。

解析

在一个Java类中会包含对其它类或接口的形式引用,解析过程就是将符号引用替换成直接引用,并确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。

主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。

不同的JVM实现可能选择不同的解析策略。一种做法是在链接的时候,就递归的把所有依赖的形式引用都进行解析。而另外的做法则可能是只在一个形式引用真正需要的时候才进行解析。也就是说如果一个Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。

类初始化

Java 虚拟机规范对类的初始化时机做了严格定义:"initialize on first active use"。类的初始化时机就是在"在首次主动使用时",首次主动使用的情形:

  • 创建某个类的新实例时--new、反射、克隆或反序列化;
  • 调用某个类的静态方法时;
  • 使用某个类的静态字段或对该字段赋值时(final字段除外);
  • 调用Java的某些反射方法时
  • 初始化某个类的子类时
  • 虚拟机启动时,含有main()方法的启动类。

 除接口以外,在一个类被初始化之 前,它的直接父类也需要被初始化,并且该初始化过程是由 Jvm 保证线程安全的。

 类的初始化过程的主要操作是执行静态初始化,将会按照源代码中从上到下的顺序依次执行静态代码块和静态域。Java 编译器把所有的静态初始化语句按照顺序(不管是静态域还是静态初始化块),全部收集到 <clinit> 方法内,该方法只能被 Jvm 调用,专门承担初始化工作。并非所有的类都会拥有一个 <clinit>() 方法。     

对象实例化

Java 编译器在编译每个类时都会为该类至少生成一个实例初始化方法--即 "<init>()" 方法。此方法与每个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造方法,与此同时也会生成一个相对应的 "<init>()" 方法。

通常来说,<init>() 方法内包括的代码内容大概为:

1。调用另一个<init>() 方法;

2。按照源代码中声明的顺序,对成员变量进行初始化;

3。对应构造方法内的代码。

 

如果构造方法是明确地从调用同一个类中的另一个构造方法开始,那它对应的 <init>() 方法体内包括的内容为:

1。一个对本类的<init>() 方法的调用;

2。对应构造方法内的所有字节码。

如果构造方法不是通过调用自身类的其它构造方法开始,并且该对象不是 Object 对象,那 <init>() 法内则包括的内容为:

1。一个对父类 <init>() 方法的调用;

2。对成员变量初始化;

3。对应构造函数内的字节码。

如果这个类是 Object,那么它的 <init>() 方法则不包括对父类 <init>() 方法的调用。



================================================================================================================================

【注:以下内容大部分引用java深度历险】

接下来直接进入类装载的委托模型实例,写两个文件,如下:

文件:Test1.java

Public class Test1 {

   Public static void main(String[] arg) {

       System.out.println(Test1.class.getClassLoader());

       Test2 t2 = new Test2();

       T2.print();

  }

}

 

文件: Test2.java

Public class Test2 {

   Public void print() {

       System.out.println(this.getClass().getClassLoader());

    }

}

编译后,我们复制生成的class文件,分别置于 <JRE所在目录>\classes下(没有此目录,需自己建立) ,和 <JRE所在目录>\lib\ext\classes下(没此目录,手工建立), 然后切换到D:\TestClassLoder目录下开始测试。

 测试一:

<JRE所在目录>\classes下

Test1.class

Test2.class

 

<JRE所在目录>\lib\ext\classes下

Test1.class

Test2.class

 

D:\TestClassLoder下

Test1.class

Test2.class

 

输入运行命令,结果如下:

D:\TestClassLoder>java Test1

Null

Null

从输出结果我们可以看出,当AppClassLoader要载入Test1.class时,先请其Parent,也就是ExtClassLoader来载入,而ExtclassLoader又请求其Parent,即Bootstrap Loader来载入Test1.class. 由于 <JRE所在目录>\Classes目录为Bootstrap Loader的搜索路径之一,所以Bootstrap Loader找到了Test1.class,因此将它载入,接着在Test1.class之内有载入Test2.class的需求,由于 Test1.class是由Bootstrap Loader所载入,所以Test2.class内定是由Bootstrap Loader根据其搜索路径来找,因Test2.class也位于Bootstrap Loader可以找到的路径下,所以也被载入了,最后我们看到Test1.class与Test2.class都是由Bootstrap Loader(null)载入。

 

测试二:

<JRE所在目录>\classes下

Test1.class

 

<JRE所在目录>\lib\ext\classes下

Test1.class

Test2.class

 

D:\TestClassLoder下

Test1.class

Test2.class

 

dos下输入运行命令,结果如下:

D:\TestClassLoder>java Test1

Null

Exception in thread “main” java.lang.NoClassdefFoundError:Test2 at Test1.main。。。//全盘负责委托机制

从输出结果我们可以看出,当AppClassLoader要载入Test1.class时,先请其Parent,也就是ExtClassLoader来载入,而ExtclassLoader又请求其Parent,即Bootstrap Loader来载入Test1.class. 由于 <JRE所在目录>\Classes目录为Bootstrap Loader的搜索路径之一,所以Bootstrap Loader找到了Test1.class,因此将它载入,接着在Test1.class之内有载入Test2.class的需求,由于 Test1.class是由Bootstrap Loader所载入,所以Test2.class内定是由Bootstrap Loader根据其搜索路径来找,但是因为Bootstrap Loader根本找不到Test2.class,而Bootstrap Loader又没有Parent,所以无法载入Test2.class.最后我们看到Test1.class是由Bootstrap Loader(null)载入,而Test2.class则无法载入。

 

测试三

<JRE所在目录>\classes下

Test2.class

 

<JRE所在目录>\lib\ext\classes下

Test1.class

Test2.class

 

D:\TestClassLoder下

Test1.class

Test2.class

 

dos下输入运行命令,结果如下:

D:\TestClassLoder>java Test1

。。。ExtClassLoader。。。

Null

从输出结果我们可以看出,当AppClassLoader要载入Test1.class时,先请其Parent,也就是ExtClassLoader来载入,而ExtclassLoader又请求其Parent,即Bootstrap Loader来载入Test1.class.但是Bootstrap Loader无法在其搜索路径下找到Test1.class(被我们删掉了),所以ExtClassLoader只得自己搜索,因此 ExtClassLoader在其搜索路径 <JRE所在目录>\lib\ext\classes下找到了Test1.class,因此将它载入,接着在Test1.class之内有载入Test2.class的需求,由于Test1.class是由ExtClassLoader所载入,所以Test2.class内定是由 ExtClassLoader根据其搜索路径来找,但是因为ExtClassLoader有Parent,所以先由Bootstrap Loader帮忙寻找,Test2.class位于Bootstrap Loader可以找到的路径下,所以被Bootstrap Loader载入了.最后我们看到Test1.class是由ExtClassLoader载入,而Test2.class则是由Bootstrap Loader(null)载入。


 



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值