关于类的加载和反射

 

(一)先简单了解一下类加载的过程,如下:

  • 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

  • 链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。

  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。

//有几点注意信息:

1.以上这才是类加载的全过程,这细化了类加载的过程,是对的,以前所认为的类的静态代码块是在类加载的时候的时候执行是不准确的,静态代码块而是在类加载loading之后,只有在初始化的时候执行(因为类加载了,但不一定会进行初始化,这要看具体加载的方式)

2.加载类,就是把类的.class文件加载到JVM中(而不是真的加载这个类,也不是加载.java文件),而JVM是运行在内存中,而类的.class文件里面保存的就是Class对象(暂时可以这么认为,其实里面保存的信息要比这更丰富)

(*)对以上整个类加载过程的解释

1、加载(Loading)

  在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下三件事情:

  (1). 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);

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

  (3). 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

  加载阶段和连接阶段(Linking)的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

  特别地,第一件事情(通过一个类的全限定名来获取定义此类的二进制字节流)是由类加载器完成的,具体涉及JVM预定义的类加载器、双亲委派模型等内容,详情请参见我的转载博文《深入理解Java类加载器(一):Java类加载原理解析》中的说明,此不赘述。


2、验证(Verification)

  验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段大致会完成4个阶段的检验动作:

  • 文件格式验证:验证字节流是否符合Class文件格式的规范(例如,是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型)

  • 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求(例如:这个类是否有父类,除了java.lang.Object之外);

  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;

  • 符号引用验证:确保解析动作能正确执行。

      验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。


3、准备(Preparation)

  准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

    public static int value = 123;

  那么,变量value在准备阶段过后的值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器方法<clinit>()之中,所以把value赋值为123的动作将在初始化阶段才会执行。至于“特殊情况”是指:当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。

  public static final int value = 123;

4、解析(Resolution)

  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。


5、初始化(Initialization)

  类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。

  在准备阶段,变量已经赋过一次系统要求的初始值(零值);而在初始化阶段,则根据程序猿通过程序制定的主观计划去初始化类变量和其他资源,或者更直接地说:初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如下:

public class Test{
    static{
        i=0;
        System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}

  那么注释报错的那行代码,改成下面情形,程序就可以编译通过并可以正常运行了。

public class Test{
    static{
        i=0;
        //System.out.println(i);
    }

    static int i=1;

    public static void main(String args[]){
        System.out.println(i);
    }
}/* Output: 
        1
 *///:~

  类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。

  虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的,如下所示:

public class DealLoopTest {
    static{
        System.out.println("DealLoopTest...");
    }
    static class DeadLoopClass {
        static {
            if (true) {
                System.out.println(Thread.currentThread()
                        + "init DeadLoopClass");
                while (true) {      // 模拟耗时很长的操作
                }
            }
        }
    }

    public static void main(String[] args) {
        Runnable script = new Runnable() {   // 匿名内部类
            public void run() {
                System.out.println(Thread.currentThread() + " start");
                DeadLoopClass dlc = new DeadLoopClass();
                System.out.println(Thread.currentThread() + " run over");
            }
        };

        Thread thread1 = new Thread(script);
        Thread thread2 = new Thread(script);
        thread1.start();
        thread2.start();
    }
}/* Output: 
        DealLoopTest...
        Thread[Thread-1,5,main] start
        Thread[Thread-0,5,main] start
        Thread[Thread-1,5,main]init DeadLoopClass
 *///:~

  如上述代码所示,在初始化DeadLoopClass类时,线程Thread-1得到执行并在执行这个类的类构造器<clinit>() 时,由于该方法包含一个死循环,因此久久不能退出。

 

(二)类什么时候加载:

类的加载是通过类加载器(Classloader)完成的,它既可以是饿汉式[eagerly load](只要有其它类引用了它就加载)加载类,也可以是懒加载[lazy load](等到类初始化发生的时候才加载)(从以下jvm规定的类初始化有且仅有的条件可以看出,类里的东西要马上被用到了,才会立马触发初始化,那这肯定就触发加载)。不过我相信这跟不同的JVM实现有关,然而他又是受JLS保证的(当有静态初始化需求的时候才被加载)。

//上面并没有太说清楚类加载的时机问题(饿汉式和懒加载再查一查,到底JVM用的是哪种)

//但类的什么静态加载和动态加载就不要再纠结了:

1.首先:编译期是没有类加载的,转了另一篇javac编译期会做哪些事情的博文可以看到,编译期没有触发类加载.所以这不是我想象的静态加载

2.然后:又有说在编译期就已确定好的类,在加载时是静态加载;在运行期才能确定的类(比如反射),这样的类在加载时是叫动态加载;

但感觉这样说都是没说清楚的类是什么时机加载的,这样搞什么静态动态加载,没有任何意义,所以看下面的文字(详细说类加载时机)

 

Java动态加载类的意义和目的:

      Java动态加载类主要是为了不改变主程序代码,通过修改配置文件就可以操作不同的对象执行不同的功能。主要有利于系统的扩展,例如当我要改变一个功能,只需要做一个类,然后编写相应的功能,通过配置文件就可以使用新的功能,不需要修改系统的任何地方,只需要添加一个类;充分实现了松散耦合。满足了开闭原则(对修改关闭,对添加或删除开放);

所以:类的动态加载主要说的是具体加载哪个类没有写死(写死那就叫静态加载了),可以根据需求的改变,通过修改配置文件,而不去修改系统,灵活动态的加入到系统中,而不是我刚开始想的在运行期出现的类加载,叫什么动态加载(事实除了预加载的类,其他的类都是在运行期动态的加载)

 

Java 语言是一种具有动态性的解释型编程语言,当指定程序运行的时候, Java 虚拟机就将编译生成的 . class 文件按照需求和一定的规则加载进内存,并组织成为一个完整的 Java 应用程序。 Java 语言把每个单独的类 Class 和接口 Implements 编译成单独的一个 . class 文件,这些文件对于 Java 运行环境来说就是一个个可以动态加载的单元。正是因为 Java 的这种特性,我们可以在不重新编译其它代码的情况下,只编译需要修改的单元,并把修改文件编译后的 . class 文件放到 Java 的路径当中, 等到下次该 Java 虚拟机器重新激活时,这个逻辑上的 Java 应用程序就会因为加载了新修改的 .class 文件,自己的功能也做了更新,这就是 Java 的动态性。

  1. 预先加载与依需求加载

Java 运行环境为了优化系统,提高程序的执行速度,在 JRE 运行的开始会将 Java 运行所需要的基本类采用预先加载( pre-loading )的方法全部加载要内存当中,因为这些单元在 Java 程序运行的过程当中经常要使用的,主要包括 JRE 的 rt.jar 文件里面所有的 .class 文件。

当 java.exe 虚拟机开始运行以后,它会找到安装在机器上的 JRE 环境,然后把控制权交给 JRE , JRE 的类加载器会将 lib 目录下的 rt.jar 基础类别文件库加载进内存,这些文件是 Java 程序执行所必须的,所以系统在开始就将这些文件加载,避免以后的多次 IO 操作,从而提高程序执行效率。

图( 2 )我们可以看到多个基础类被加载, java.lang.Object,java.io.Serializable 等等。

 

图( 2 )

相对于预先加载,我们在程序中需要使用自己定义的类的时候就要使用依需求加载方法( load-on-demand ),就是在 Java 程序需要用到的时候再加载,以减少内存的消耗,因为 Java 语言的设计初衷就是面向嵌入式领域的。

在这里还有一点需要说明的是, JRE 的依需求加载究竟是在什么时候把类加载进入内部的呢?

我们在定义一个类实例的时候,比如 TestClassA testClassA ,这个时候 testClassA 的值为 null ,也就是说还没有初始化,没有调用 TestClassA 的构造函数,只有当执行 testClassA = new TestClassA() 以后, JRE 才正真把 TestClassA 加载进来。

 

  1. 隐式加载和显示加载

Java 的加载方式分为隐式加载( implicit )和显示加载( explicit ),上面的例子中就是用的隐式加载的方式。所谓隐式加载就是我们在程序中用 new 关键字来定义一个实例变量, JRE 在执行到 new 关键字的时候就会把对应的实例类加载进入内存。隐式加载的方法很常见,用的也很多, JRE 系统在后台自动的帮助用户加载,减少了用户的工作量,也增加了系统的安全性和程序的可读性。

相对于隐式加载的就是我们不经常用到的显示加载。所谓显示加载就是有程序员自己写程序把需要的类加载到内存当中,下面我们看一段程序:

class TestClass{

public void method(){

        System.out.println("TestClass-method");

}

}

 

public class CLTest {

public static void main(String args[]) {

        try{

               Class c = Class.forName("TestClass");

               TestClass object = (TestClass)c.newInstance();

               object.method();

        }catch(Exception e){

               e.printStackTrace();

        }

}

}

我们通过 Class 类的 forName (String s) 方法把自定义类 TestClass 加载进来,并通过 newInstance ()方法把实例初始化。事实上 Class 类还很多的功能,这里就不细讲了,有兴趣的可以参考 JDK 文档。

Class 的 forName() 方法还有另外一种形式: Class forName(String s, boolean flag, ClassLoader classloader) , s 表示需要加载类的名称, flag 表示在调用该函数加载类的时候是否初始化静态区, (意思是也可以不初始化静态区???)classloader 表示加载该类所需的加载器。

forName (String s) 是默认通过 ClassLoader.getCallerClassLoader() 调用类加载器的,但是该方法是私有方法,我们无法调用,如果我们想使用 Class forName(String s, boolean flag, ClassLoader classloader) 来加载类的话,就必须要指定类加载器,可以通过如下的方式来实现:

Test test = new Test();//Test 类为自定义的一个测试类;

ClassLoader cl = test. getClass().getClassLoader();

                         // 获取 test 的类装载器;

Class c = Class.forName("TestClass", true, cl);

因为一个类要加载就必需要有加载器,这里我们是通过获取加载 Test 类的加载器 cl 当作加载 TestClass 的类加载器来实现加载的。

 

(三)类什么时候初始化:

关于类加载的初始化阶段,在虚拟机规范严格规定了有且只有5种场景必须对类进行初始化,这种行为成为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。(JLS严格的说明:一个类不会被任何除以下之外的原因初始化。):

1.主动引用:

     1. 使用new关键字实例化对象时、读取或者设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段)。

     2. 使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。(class.forName()反射,也属于这条,但它有可能导致ClassNotFoundException,getClass()也会触发初始化,但类型.class只会触发加载loading,不会触发初始化)

     3. 当初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化。

    4. 当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类

   5. 当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是1.7的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,这是一个比较大点的话题,这里暂且打住)(这条也没看懂)

  • //额外有一条:在顶层类中执行assert语句,这也会导致类初始化(不明白这条,再查查)

 

2.被动引用:

  1. 通过子类引用父类中定义的静态字段,只会触发父类的初始化。至于是否会触发子类的加载和验证,取决于虚拟机的具体实现(HotSpot不会加载)。
  2. 通过数组定义来引用类,如 A[] ints = new A[10] ,  不会触发A 类的初始化。而是会会触发名为 LA的类初始化。它是一个由虚拟机自动生成的、直接继承于Object 的子类,创建动作由字节码指令 newarray 触发。这个类代表了一个元素类型为 A 的一位数组,数组中的属性和方法都实现在这个类中。Java 语言中数组的访问比C/C++ 安全是因为这个类封装了数组元素的访问方法。
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

(注意)接口加载的时机:

接口也有初始化过程,但是接口中不能使用static 语句块,但是编译器仍然会为接口生成 <clinit> 类构造器初始化接口中定义的成员变量。 接口与类真正的区别是初始化场景中的第三种情况:当一类在初始化的时候,会要求其所有的父类都初始化完成,但是接口在初始化的时候,并不要求其父接口全部完成了初始化,只有在真正用到了父接口的时候(如引用接口中定义的常量)才会初始化。

(四)类初始化做些什么:

1.类是如何被初始化的:

现在我们知道什么时候触发类的初始化了,他精确地写在Java语言规范中。但了解清楚 域(fields,静态的还是非静态的)、块(block静态的还是非静态的)、不同类(子类和超类)和不同的接口(子接口,实现类和超接口)的初始化顺序也很重要类。事实上很多核心Java面试题和SCJP问题都是基于这些概念,下面是类初始化的一些规则:

  1. 类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
  2. 超类早于子类和衍生类的初始化
  3. 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的初始化即使静态域被子类或子接口或者它的实现类所引用。
  4. 接口初始化不会导致父接口的初始化。
  5. 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
  6. 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类

2.子类继承父类时的初始化顺序:

   1.首先初始化父类的static变量和块,按出现顺序

   2.初始化子类的static变量和块,按出现顺序

   3.初始化父类的普通变量,调用父类的构造函数

   4.初始化子类的普通变量,调用子类的构造函数

 

 

(五)类加载器的阶层体系

讨论了这么多以后,接下来我们仔细研究一下 Java 的类加载器的工作原理:

当执行 java ***.class 的时候, java.exe 会帮助我们找到 JRE ,接着找到位于 JRE 内部的 jvm.dll ,这才是真正的 Java 虚拟机器 , 最后加载动态库,激活 Java 虚拟机器。虚拟机器激活以后,会先做一些初始化的动作,比如说读取系统参数等。一旦初始化动作完成之后,就会产生第一个类加载器―― Bootstrap Loader , Bootstrap Loader 是由 C++ 所撰写而成,这个 Bootstrap Loader 所做的初始工作中,除了一些基本的初始化动作之外,最重要的就是加载 Launcher.java 之中的 ExtClassLoader ,并设定其 Parent 为 null ,代表其父加载器为 BootstrapLoader 。然后 Bootstrap Loader 再要求加载 Launcher.java 之中的 AppClassLoader ,并设定其 Parent 为之前产生的 ExtClassLoader 实体。这两个加载器都是以静态类的形式存在的。这里要请大家注意的是, Launcher$ExtClassLoader.class 与 Launcher$AppClassLoader.class 都是由 Bootstrap Loader 所加载,所以 Parent 和由哪个类加载器加载没有关系。

下面的图形可以表示三者之间的关系:

 

父类

父类

载入

载入

BootstrapLoader

        PARENT

AppClassLoader

PARENT

ExtClassLoader

 

这三个加载器就构成我们的 Java 类加载体系。他们分别从以下的路径寻找程序所需要的类:

BootstrapLoader : sun.boot.class.path

ExtClassLoader:      java.ext.dirs

AppClassLoader:      java.class.path

这三个系统参量可以通过 System.getProperty() 函数得到具体对应的路径。大家可以自己编程实现查看具体的路径。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值