摘要:本文主要讲了Java类加载的机制,这是学习反射的入门基础。
JVM和类
当我们调用Java命令运行某个Java程序时,该命令将会启动1条Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于该Java虚拟机进程里。正如前面介绍的,同1个JVM的所有线程、所有变量都处于同1个进程里,它们都使用该JVM进程的内存区。当系统出现以下几种情况时,JVM进程将被终止:
1、程序运行到最后正常结束。
2、程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序。
3、程序履行进程中遇到未捕获的异常或毛病而结束。
3、程序所在平台强迫结束了JVM进程。
从上面的介绍可以看出,当Java程序运行结束时,JVM进程结束,该进程在内存中状态将会丢失。
类的生命周期
类的加载/类初始化
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化3个步骤来对该类进行初始化,如果没成心外,JVM将会连续完成这3个步骤,所以有时也把这3个步骤统称为类加载或类初始化。
加载:查找并加载类的2进制数据
1、通过1个类的全限定名来获得定义此类的2进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在java堆中生成1个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
注意:将编译后的java类文件(也就是.class文件)中的2进制数据读入内存,并将其放在运行时数据区的方法区内,然后再堆区创建1个Java.lang.Class对象,用来封装类在方法区的数据结构。即加载后终究得到的是Class对象,并且更加值得注意的是:该Java.lang.Class对象是单实例的,不管这个类创建了多少个对象,他的Class对象时唯1的!
连接:
1、验证:确保被加载的类的正确性
2、准备:为类的静态变量分配内存,并将其初始化为默许值
3、解析:把类中的符号援用转换为直接援用。
初始化:为类的静态变量赋予正确的初始值。
注意:连接和初始化阶段,其实静态变量经过了两次赋值:第1次是静态变量类型的默许值;第2次是我们真正赋给静态变量的值。
我简单画了个图,其进程以下:
类加载指的是将类的class文件读入内存,并为之创建1个java.lang.Class对象,也就是说当程序中使用任何类时,系统都会为之建立1个java.lang.Class对象。事实上,每一个类是1批具有相同特点的对象的抽象(或说概念),而系统中所有的类,它们实际上也是对象,它们都是java.lang.Class的实例。
加载由类加载器完成,类加载器通常由JVM提供,这些类加载器也是我们前面所有程序运行的基础,JVM提供的这些类加载器通常被称为系统类加载器。除此以外,开发者可以通过继承ClassLoader基类来创建自己的类加载器。
通过使用不同的类加载器,可以从不同来源加载类的2进制数据,通常有以下几种来源:
1、从本地文件系统来加载class文件,这是绝大部份示例程序的类加载方式。
2、从JAR包中加载class文件,这类方式也是很常见的,前面介绍JDBC编程时用到的数据库驱动类就是放在JAR文件中,JVM可以从JAR文件中直接加载该class文件。
3、通过网络加载class文件。
4、把1个Java源文件动态编译、并履行加载。
类加载器通常不必等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。
Java程序对类的使用方式
主动使用
1、创建类的实例
2、方法某个类或接口的静态变量,或对该静态变量赋值
3、调用类的静态方法
4、反射(如 Class.forName(“com.itzhai.Test”))
5、初始化1个类的子类
6、Java虚拟机启动时被标明为启动类的类(Main Class)
被动使用
除以上6中方式,其他对类的使用都是被动使用,都不会致使类的初始化。类的初始化时机正是java程序对类的首次主动使用,
所有的Java虚拟机实现必须在每一个类或接口被Java程序“首次主动使用”时才初始化它们。
对象初始化
在类被装载、连接和初始化,这个类就随时都可能使用了。对象实例化和初始化是就是对象生命的起始阶段的活动,在这里我们主要讨论对象的初始化工作的相干特点。
Java 编译器在编译每一个类时都会为该类最少生成1个实例初始化方法--即"()" 方法。此方法与源代码中的每一个构造方法相对应,如果类没有明确地声明任何构造方法,编译器则为该类生成1个默许的无参构造方法,这个默许的构造器仅仅调用父类的无参构造器,与此同时也会生成1个与默许构造方法对应的 "()" 方法.
通常来讲,() 方法内包括的代码内容大概为:调用另外一个() 方法;对实例变量初始化;与其对应的构造方法内的代码。
如果构造方法是明确地从调用同1个类中的另外一个构造方法开始,那它对应的() 方法体内包括的内容为:1个对本类的() 方法的调用;对利用构造方法内的所有字节码。
如果构造方法不是通过调用本身类的其它构造方法开始,并且该对象不是 Object 对象,那() 法内则包括的内容为:1个对父类() 方法的调用;对实例变量初始化方法的字节码;最后是对应构造子的方法体字节码。
如果这个类是 Object,那末它的() 方法则不包括对父类() 方法的调用。
1、相同点:
通过这几种方式,得到的都是Java.lang.Class对象(这个是上面讲到的类在加载时取得的终究产物)
例如:
结果:
2、区分:
下面用1个实例来讲说它们的区分
以下新建1个类
然后开始使用:
如果,将Class.forName()去掉:
以下:
结果又变成:
所以,可以得出以下结论:
1)Class cl=Cat.class; JVM将使用类Cat的类装载器,将类A装入内存(条件是:类A还没有装入内存),不对类A做类的初始化工作.返回类A的Class的对象
2)Class cl=对象援用o.getClass();返回援用o运行时真正所指的对象(由于:儿子对象的援用可能会赋给父对象的援用变量中)所属的类的Class的对象 ,如果还没装载过,会进行装载。
3)Class.forName("类名"); 装入类A,并做类的初始化(条件是:类A还没有装入内存)
从JVM的角度看,我们使用关键字new创建1个类的时候,这个类可以没有被加载。但是使用Class对象的newInstance()方法的时候,就必须保证:
1、这个类已加载;
2、这个类已连接了。而完成上面两个步骤的正是Class的静态方法forName()所完成的,这个静态方法调用了启动类加载器,即加载 java API的那个加载器。
现在可以看出,Class对象的newInstance()(这类用法和Java中的工厂模式有着异曲同工之妙)实际上是把new这个方式分解为两步,即首先调用Class加载方法加载某个类,然后实例化。这样分步的好处是不言而喻的。我们可以在调用class的静态加载方法forName时取得更好的灵活性,提供给了1种降耦的手段。
Class.forName().newInstance()和通过new得到对象的区分
1、使用newInstance可以解耦。使用newInstance的条件是,类已加载并且这个类已连接,这是正是class的静态方法forName()完成的工作。newInstance实际上是把new 这个方式分解为两步,即,首先调用class的加载方法加载某个类,然后实例化。
2、newInstance: 弱类型。低效力。只能调用无参构造。 new: 强类型。相对高效。能调用任何public构造。
3、newInstance()是实现IOC、反射、面对接口编程和依赖颠倒等技术方法的必定选择,new只能实现具体类的实例化,不合适于接口编程。
4、 newInstance() 1般用于动态加载类。
5、Class.forName(“”).newInstance()返回的是object 。
6、newInstance( )是1个方法,而new是1个关键字;
注:1般在通用框架里面用的就是class.forName来加载类,然后再通过反射来调用其中的方法,比方Tomcat源码里面,这样就避免了new关键字的耦合度,还有让不同的类加载器来加载不同的类,方便提高类之间的安全性和隔离性.