赞
踩
在java语言里边,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会另类加载时稍微增加一些性能开销,但是会为java语言程序提供高度的灵活性,java里天生就可以动态扩展的语言特性就是依赖运行期间动态加载和动态连接的特点是显得。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。它们开始的顺序如下图所示:
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备、初始化和卸载这五个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
什么时候需要开始类加载过程的第一个阶段:加载?java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把我。但是对于初始化阶段,虚拟机规范则严格规定了只有5种情况必须立即开始对类的“初始化”,(而加载、验证、准备自然需要在此之前开始):
这5类场景称为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,称为被动引用。
PS:
Eg.1
public class SuperClasss {
static{
System.out.println("SuperClass init");
}
public static int val = 123;
}
class SubClass extends SuperClasss{
static {
System.out.println("SubClass init");
}
}
class NotInitialization{
public static void main(String[] args) {
System.out.println(SubClass.val);
}
}
Eg.2
class NotInitialization{
public static void main(String[] args) {
SuperClasss[] sca = new SuperClasss[10];
}
}
Eg.3
class ConstClass{
static {
System.out.println("ConstClass init");
}
public static final String HELLO = "HelloWorld";
}
class NotInitialization{
public static void main(String[] args) {
System.out.println(ConstClass.HELLO);
}
}
接口初始化过程:
当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化的时候,并不要求其父类接口全部都完成了初始化,只有在真正使用到父类接口的时候(如引用接口中定义的常量)才会初始化。
接下来是类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
在加载阶段,虚拟机需要完成以下3件事情:
由于虚拟机规范的这三点要求并不具体,所以虚拟机实现与具体应用的灵活度都是相当大的。例如获取一个类的二进制字节流:
对于非数组类的加载阶段开发人员可以通过自定义的类加载器去完成,通过重写类加载器的loadClass()方法来实现。
对于数组类的加载阶段,数组类本身并不通过类加载器创建,他是由java虚拟机直接创建的,但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型Element Type,指的是数组去掉所有维度的类型,最终是要靠类加载器去创建,一个数组类的创建过程遵从如下规则:
验证是连接阶段的第一步,这个阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
这个阶段主要是为了保证输入的字节流能正确的解析并存储于方法区之中,格式上符合描述了一个java类型信息的要求。
对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息
通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的
对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容
如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
准备阶段不是初始化,这里是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都讲在方法区进行分配,这里的内存分配仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在java堆中,其次这里的初始值指的是“通常情况”下的数据类型的零值,例如int 的0, boolean的false。
这里还需要注意如下几点:
解析阶段是虚拟机将常量池中的符号引用转化为直接引用的过程。在Class类文件结构一文中已经比较过了符号引用和直接引用的区别和关联,这里不再赘述。前面说解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。
这里说明下符号引用和直接引用的区别与关联:
对同一个符号引用进行多次解析请求时很常见的事情,虚拟机实现可能会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用电限定符7类符号引用进行,分别对应于常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info指令四种常量类型。
1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束,查找流程如下图所示:
从下面一段代码的执行结果中很容易看出来字段解析的搜索顺序:
class Super{
public static int m = 11;
static{
System.out.println("执行了super类静态语句块");
}
}
class Father extends Super{
public static int m = 33;
static{
System.out.println("执行了父类静态语句块");
}
}
class Child extends Father{
static{
System.out.println("执行了子类静态语句块");
}
}
public class StaticTest{
public static void main(String[] args){
System.out.println(Child.m);
}
}
执行了super类静态语句块
执行了父类静态语句
33
如果注释掉Father类中对m定义的那一行,则输出结果如下:
执行了super类静态语句
11
另外,很明显这里我们便可以分析如下:static变量发生在静态解析阶段,也即是初始化之前,此时已经将字段的符号引用转化为了内存引用,也便将它与对应的类关联在了一起,由于在子类中没有查找到与m相匹配的字段,那么m便不会与子类关联在一起,因此并不会触发子类的初始化。
最后需要注意:理论上是按照上述顺序进行搜索解析,但在实际应用中,虚拟机的编译器实现可能要比上述规范要求的更严格一些。如果有一个同名字段同时出现在该类的接口和父类中,或同时在自己或父类的接口中出现,编译器可能会拒绝编译。如果对上面的代码做些修改,将Super改为接口,并将Child类继承Father类且实现Super接口,那么在编译时会报出错误.
3、类方法解析:对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。
4、接口方法解析:与类方法解析步骤类似,只是接口不会有父类,因此,只递归向上搜索父接口就行了。
初始化是类加载过程的最后一步,到了此阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器()方法的过程。
这里简单说明下()方法的执行规则:
1、()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
2、()方法与实例构造器()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的()方法执行之前,父类的()方法已经执行完毕。因此,在虚拟机中第一个被执行的()方法的类肯定是java.lang.Object。
3、()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成()方法。
4、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成()方法。但是接口与类不同的是:执行接口的()方法不需要先执行父接口的()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。
5、虚拟机会保证一个类的()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
下面给出一个简单的例子,以便更清晰地说明如上规则
class Father{
public static int a = 1;
static {
a = 2;
}
}
class Child extends Father{
public static int b = a;
}
public class ClinitTest{
public static void main(String[] args) {
System.out.println(Child.b);
}
}
执行上面的代码,会打印出2,也就是说b的值被赋为了2。
我们来看得到该结果的步骤。首先在准备阶段为类变量分配内存并设置类变量初始值,这样A和B均被赋值为默认值0,而后再在调用()方法时给他们赋予程序中指定的值。当我们调用Child.b时,触发Child的()方法,根据规则2,在此之前,要先执行完其父类Father的()方法,又根据规则1,在执行()方法时,需要按static语句或static变量赋值操作等在代码中出现的顺序来执行相关的static语句,因此当触发执行Father的()方法时,会先将a赋值为1,再执行static语句块中语句,将a赋值为2,而后再执行Child类的()方法,这样便会将b的赋值为2.
如果我们颠倒一下Father类中“public static int a = 1;”语句和“static语句块”的顺序,程序执行后,则会打印出1。很明显是根据规则1,执行Father的()方法时,根据顺序先执行了static语句块中的内容,后执行了“public static int a = 1;”语句。
另外,在颠倒二者的顺序之后,如果在static语句块中对a进行访问(比如将a赋给某个变量),在编译时将会报错,因为根据规则1,它只能对a进行赋值,而不能访问。
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:
1)在执行非置信代码之前,自动验证数字签名。
2)动态地创建符合用户特定需要的定制化构建类。
3)从特定的场所取得Java class,例如数据库中和网络中。
事实上当使用Applet的时候,就用到了特定的ClassLoader,因为这时需要从网络上加载java class,并且要检查相关的安全信息,应用服务器也大都使用了自定义的ClassLoader技术。
这几种类加载器的层次关系如下图所示:
这种层次关系称为类加载器的双亲委派模型。我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。
双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。
使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是Java类随着它的类加载器(说白了,就是它所在的目录)一起具备了一种带有优先级的层次关系,这对于保证Java程序的稳定运作很重要。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。
JDK中的ClassLoader
在实现自己的ClassLoader之前,我们先看一下JDK中的ClassLoader是怎么实现的:
protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
方法原理很简单,一步一步解释一下:
1、第5行,首先查找.class是否被加载过
2、 第6行~第12行,如果.class文件没有被加载过,那么会去找加载器的父加载器。如果父加载器不是null(不是Bootstrap ClassLoader),那么就执行父加载器的loadClass方法,把类加载请求一直向上抛,直到父加载器为null(是Bootstrap ClassLoader)为止
3、第13行~第17行,父加载器开始尝试加载.class文件,加载成功就返回一个java.lang.Class,加载不成功就抛出一个ClassNotFoundException,给子加载器去加载
4、第19行~第21行,如果要解析这个.class文件的话,就解析一下,解析的作用类加载的文章里面也写了,主要就是将符号引用替换为直接引用的过程
我们看一下findClass这个方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
是的,没有具体实现,只抛了一个异常,而且是protected的,这充分证明了:这个方法就是给开发者重写用的。
自定义类加载器
从上面对于java.lang.ClassLoader的loadClass(String name, boolean resolve)方法的解析来看,可以得出以下2个结论:
1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可
2、如果想打破双亲委派模型,那么就重写整个loadClass方法
当然,我们自定义的ClassLoader不想打破双亲委派模型,所以自定义的ClassLoader继承自java.lang.ClassLoader并且只重写findClass方法。
第一步,自定义一个实体类Person.java,我把它编译后的Person.class放在D盘根目录下:
package com.xrq.classloader;
public class Person
{
private String name;
public Person()
{
}
public Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String toString()
{
return "I am a person, my name is " + name;
}
}
第二步,自定义一个类加载器,里面主要是一些IO和NIO的内容,另外注意一下 defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class—-只要二进制字节流的内容符合Class文件规 范。我们自定义的MyClassLoader继承自java.lang.ClassLoader,就像上面说的,只实现findClass方法:
public class MyClassLoader extends ClassLoader
{
public MyClassLoader()
{
}
public MyClassLoader(ClassLoader parent)
{
super(parent);
}
protected Class<?> findClass(String name) throws ClassNotFoundException
{
File file = getClassFile(name);
try
{
byte[] bytes = getClassBytes(file);
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
}
catch (Exception e)
{
e.printStackTrace();
}
return super.findClass(name);
}
private File getClassFile(String name)
{
File file = new File("D:/Person.class");
return file;
}
private byte[] getClassBytes(File file) throws Exception
{
// 这里要读入.class的字节,因此要使用字节流
FileInputStream fis = new FileInputStream(file);
FileChannel fc = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
WritableByteChannel wbc = Channels.newChannel(baos);
ByteBuffer by = ByteBuffer.allocate(1024);
while (true)
{
int i = fc.read(by);
if (i == 0 || i == -1)
break;
by.flip();
wbc.write(by);
by.clear();
}
fis.close();
return baos.toByteArray();
}
}
第三步,Class.forName有一个三个参数的重载方法,可以指定类加载器,平时我们使用的Class.forName(“XX.XX.XXX”)都是使用的系统类加载器Application ClassLoader。写一个测试类:
public class TestMyClassLoader
{
public static void main(String[] args) throws Exception
{
MyClassLoader mcl = new MyClassLoader();
Class<?> c1 = Class.
forName("com.xrq.classloader.Person", true, mcl);
Object obj = c1.newInstance();
System.out.println(obj);
System.out.println(obj.getClass().getClassLoader());
}
}
看一下运行结果:
I am a person, my name is null
com.xrq.classloader.MyClassLoader@5d888759
个人的经验来看,最容易出问题的点是第二行的打印出来的是”sun.misc.Launcher$AppClassLoader”。造成这个问题的关键在于MyEclipse是自动编译的,Person.java这个类在ctrl+S保存之后或者在Person.java文件不编辑若干秒后,MyEclipse会帮我们用户自动编译Person.java,并生成到CLASSPATH也就是bin目录下。在CLASSPATH下有Person.class,那么自然是由Application ClassLoader来加载这个.class文件了。解决这个问题有两个办法:
1、删除CLASSPATH下的Person.class,CLASSPATH下没有Person.class,Application ClassLoader就把这个.class文件交给下一级用户自定义ClassLoader去加载了
2、TestMyClassLoader类的第5行这么写”MyClassLoader mcl = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent());”, 即把自定义ClassLoader的父加载器设置为Extension ClassLoader,这样父加载器加载不到Person.class,就交由子加载器MyClassLoader来加载了
ClassLoader.getResourceAsStream(String name)方法作用
ClassLoader中的getResourceAsStream(String name)其实是一个挺常见的方法,所以要写一下。这个方法是用来读入指定的资源的输入流,并将该输入流返回给用户用的,资源可以是图像、声音、.properties文件等,资源名称是以”/”分隔的标识资源名称的路径名称。
不仅ClassLoader中有getResourceAsStream(String name)方法,Class下也有getResourceAsStream(String name)方法,它们两个方法的区别在于:
1、Class的getResourceAsStream(String name)方法,参数不以”/”开头则默认从此类对应的.class文件所在的packge下取资源,以”/”开头则从CLASSPATH下获取
2、ClassLoader的getResourceAsStream(String name)方法,默认就是从CLASSPATH下获取资源,参数不可以以”/”开头
其实,Class的getResourceAsStream(String name)方法,只是将传入的name进行解析一下而已,最终调用的还是ClassLoader的getResourceAsStream(String name),看一下Class的getResourceAsStrea(String name)的源代码:
public InputStream getResourceAsStream(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();
if (cl==null) {
// A system class.
return ClassLoader.getSystemResourceAsStream(name);
}
return cl.getResourceAsStream(name);
}
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) {
Class c = this;
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');
if (index != -1) {
name = baseName.
substring(0, index).replace('.', '/')
+"/"+name;
}
} else {
name = name.substring(1);
}
return name;
}
.class和getClass()的区别
最后讲解一个内容,.class方法和getClass()的区别,这两个比较像,我自己没对这两个东西总结前,也常弄混。它们二者都可以获取一个唯一的java.lang.Class对象,但是区别在于:
1、.class用于类名,getClass()是一个final native的方法,因此用于类实例
2、.class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。