LastUpdataTime:11.11.14
首先,要弄清楚的是,本文记录的是“类型”的生命周期,而非“对象”的生存周期方面的笔记。当然,对象的生存周期实际上属于类型的生命周期问题的一部分。
何为类型的生命周期?
简单的讲,就是Java类型(类或接口)进入JVM开始到最终退出。从大体上讲,可以分为三个部分:开始阶段的装载、连接、初始化,占绝大多数时间的对象实例化、对象的垃圾回收、对象的终止,然后是退出阶段,即类型的卸载。
一、类型装载、连接与初始化
我们知道,类型数据通常保存在class文件中,JVM要使用某个类型,必须将这段信息“读取”进来。
1)装载。将二进制的类型数据读入JVM
很明显,要装载类型,必须要一份二进制的数据流。需要注意的是,这个二进制数据流不只是Java class文件,也可能遵守其他格式的文件。不同是JVM有不同的实现。
然后,JVM还要解析所读取的二进制文件的内部数据结构。
最后,如果解析无错误,JVM会为这个类创建一个Java.lang.Class类的一个对象,来映射读取的类型。
2)验证。确保类型数据格式正确,并且能为JVM使用
关于这一点,JVM规范并没有限定JVM的实现的具体做法,它仅仅是提供了一个接口而已。不同的JVM可能与有不一样的实现。
验证部分,按操作时间,实际上分为三种。
一种验证的是二进制文件的结构的正确性。同样的,JVM规范中并没有显式的定义二进制文件(大所数是class文件)的格式,而是又具体设计者自己设定。需要注意的是,这部分的验证过程是在装载过程中完成的,但在逻辑上还是属于验证部分。
另一种验证是真正的在"验证"过程是实现的它可能做以下事情:
检查final的类不能拥有子类
检查final方法不能被覆盖
检验常量池中的所有特殊字符串是否合乎格式
检查字节码的完整性
等等
还有一种验证是在"验证"过程以外完成的,就是符号引用的验证。
3)准备。负责为类型(包括类变量和类方法等等)分配内存
在准备阶段,JVM为类变量分配内存,并设置为默认值。如int变量赋0,String变量赋null。但是,在进入初始化阶段之前,变量都不会被初始化为真正的初始值,这是因为在准备阶段是不会执行Java代码的。
4)解析。将常量池中的符号引用变为直接引用。
与其他步骤不同,这个过程并不是JVM实现连接过程中的"必选"内容。JVM规范赋予每个JVM实现推迟解析的权利。
2、3、4步一起构成连接过程。
5)初始化。这个类型引入中的最后一个步骤,也就是为类型的每个静态变量或者静态块中的变量赋予"正确"的初始值。
二、何时初始化与自定义ClassLoader
OK,我们现在知道了一个类是怎样开始的,那么,我们现在自然而然的就会有这样一个疑问:一个类是在什么时候开始装载、连接以及初始化(下面我将这3个步骤统一称为装载)的呢?
这是一个很重要的问题,因为,类型的装载是完全由JVM操作的,对于一般的程序员而言是透明的。然而,下面还会继续提到,类型的装载时间是可控的。
在《The Java Language Specification》中给出了这样的建议:装载必须在连接之前,连接必须在初始化之前,唯一的硬性规定是:在首次主动使用时初始化。
何为主动使用,有下面6种情形:
1)创建新的实例,如:new Object();
2)调用静态方法,如:AnyClass.staticFun();
3)使用静态字段,如:AnyClass.staticField;
4)涉及到该类的反射机制,如:Class.forName("NameOfAnyClass");
5)初始化该类的子类,如:new SubClass();
6)JVM启动时那么包含main方法的类
上面给出的建议粗看起来像是废话,但仔细一看就会发现很多东西。
想一想,Java语言的实现只是要求我们在第一次使用某个类之前对其进行初始化就行了。那么,我们是不是要在程序运行之初就装载所有要用到的类呢?显然不要。
OK,假设这么个情形:有这么一个类,它的作用是为程序的安全退出做工作的,因此,在程序运行开始之后的很长一段时间JVM都不需要触碰到它。那么,理论上我们可以推迟它的装载时间----在这个类要用的时候再装载
这是不是和Java的反射机制很相似:在程序运行时再选择装载某个类。事实上,自定义ClassLoader 来动态的选择装载某个类和以前学习的反射是Java实现动态特性的两种方法。
看下面实现自定义ClassLoader 的代码:
package 番外篇_类的生存周期与类加载器管理; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; public class MyClassLoader extends ClassLoader{ private String basePath; //装载路径,类似于我们配置的Path变量 public MyClassLoader(String baseParh){ this.basePath = baseParh; } public MyClassLoader(ClassLoader parent,String basePath){ super(parent); this.basePath = basePath; } protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] classData; classData = getTypeFromBasePath(className); if(null == classData){ throw new ClassNotFoundException("未能找到正确的二进制文件信息"); } //创建一个class对象并返回 //在define()内部会解析数据流为方法区的内部数据结构 //该方法是final的,不能重写 :问题,那么我们能不能定义我们自己格式的class文件呢? return defineClass(className,classData,0,classData.length); } /** * 读取类型信息(byte[]) * @param typeName ; 类的权限名 * @return */ private byte[] getTypeFromBasePath(String typeName){ FileInputStream fis; //通过类名获得路径名 比如在window中:java.lang.Object => java/lang/Object String fileName = basePath + File.separatorChar + typeName.replace('.', File.separatorChar)+".class"; try { fis = new FileInputStream(fileName); } catch (FileNotFoundException e) { e.printStackTrace(); return null; } BufferedInputStream bis = new BufferedInputStream(fis); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { int c = bis.read(); while (c != -1) { out.write(c); c = bis.read(); } } catch (IOException e) { return null; } return out.toByteArray(); } }
首先,自定义类装载器必须继承java.lang.ClassLoader类。然后看构造器,这里实现了2个构造器,第二个构造器有2个参数,前一个参数parent是指定该ClassLoader 的父装载器,我们知道ClassLoader在装载类型的时候会先询问它的父装载器能否装载这个类型,当它的所有超类都不能装载时猜轮到自己。而第一个构造器实际上是指定bootClassLoader,即启动类装载器为父装载器。
接下来看方法find();它接受一个类型的权限定名(如:java.lang.NameOfClass)为参数。在这个方法的内部就做了2件事,一是读取文件(注意,我这里的实现只能读取.class结尾的文件,实际上你也可以用任意的文件后缀名,甚至不判断),二是将读取的byte[]类型数据传给defineClass()方法,并返回一个Class对象。defineClass()方法比较特殊,查看源代码可以看到,其最终是用一个native方法实现的,意思是说,它最终是由本地方法(通常是C或者C++)来支持的。
细心的同学可能发现,实际上这个方法只需要返回一个Class对象就行了,至于怎么获得返回的Class对象并没有规定。这个大家可以自己去测试,看看又什么问题。
当我们创建了一个自定义的ClassLoader对象之后,调用load(String NameOfClass)方法就可以装载某个类了,注意,这里传递的是权限定名,而不是简化类名。
下面看一看怎么用自定义的ClassLoader来实现简单的动态编程。
package 番外篇_类的生存周期与类加载器管理; import java.util.Scanner; public class TestLoadClass { public static void main(String args[]) throws InstantiationException, IllegalAccessException{ MyClassLoader classLoader = new MyClassLoader("D:\\"); Scanner sc = new Scanner(System.in); System.out.println("Please put the name of the Class you want load:"); String className = sc.next(); try { Class c = classLoader.loadClass(className); Object o = c.newInstance(); iCanLoaded ic = (iCanLoaded)o; //iCanLoaded是一个自定义的接口,定义了一个beLoaded()方法 ic.beLoaded(); // //forget all the information about the Class // classLoader = null; // o = null; // c = null; // ic = null; } catch (ClassNotFoundException e) { System.out.println("未找到需要装载的二进制文件"); } } }
先不要看代码,运行一下程序,如果不出意外的话,程序会在输出了一句:Please put the name of the Class you want load:之后阻塞住
现在,我们再编写一个CanLoadedClass类,继承iCanLoaded接口(没给出代码,很简单,自己实现),实现beLoaded()。
注意,直到这个时候,程序都没有退出,一直阻塞着。而且,更重要的是,我们并没有得到一个实现了iCanLoaded接口的class文件。
编译CanLoadedClass类之后,将得到的class文件放到D盘目录。在程序中输入"番外篇_类的生存周期与类加载器管理.CanLoadedClass"----这是类型的权限定名,请原谅我使用了中文包名。回车之后,程序执行了CanLoadedClass对象的beLoaded方法。
看到么,这就是典型的动态编程:在程序运行期间再慢悠悠的写代码
三、对象的生存周期
对象始于类型的实例化。它有很多种实现方法,比如最简单的 new Object()。
同类型一样,一个新对象也要经过分配内存、变量赋默认值、变量赋初始值等过程,当然,还有一个很重要的方法初始化问题。这个比较复杂,以后慢慢学习。
有人说,Java语言的两大核心是JVM和垃圾收集器(GC)。GC就是处理对象的终结任务的——当一个对象不再为程序所引用时。这个算法是一个很重要的东西,以后有时间单独列一篇总结出来。
四、类型的卸载与热更新(未完待续)
A class or interface may be unloaded if and only if its defining class loader may be reclaimed by the garbage collector Classes and interfaces loaded by the bootstrap loader may not be unloaded. --摘自《The Java Language Specification》
首先应该清楚,Java语言中是没有提供显式的卸载类型信息的方法的,不是么?那么怎样卸载类型的信息呢?
在讨论卸载类型之前,让我们先看一看类型卸载的一个重要特性----对程序透明。
我们知道,类型信息的本质不过是一块内存而已,类型的卸载是一种对内存的优化选项。根据程序设计的基本思想,一个依赖于系统对内存优化的程序不是一个好的程序(试想一下,一个程序能在高性能计算机上运行,却不能在老旧计算机上运行,能是好程序么?)。所以说,类型卸载是对程序透明的,这也是为什么Java设计者没有提供相应接口的原因(更大一部分是出于安全考虑)。
要卸载类型,就要回到上面的英文,翻译是这样的:当且仅当装载装载类型的类装载器为GC不可达的时,该类才可以被卸载。被根类装载器装载的类不会被卸载。
这就提供了一个思路:当我们让装载某个类的类装载器被GC回收,那么不就可以卸载类了么? 当然,前提是那个类装载器不是bootClassLoader。看最近贴上去的代码,那个被注释掉的地方就是,让装载ICanLoaded类的MyClassLoader对象classLoader = null ,这时由于classLoader 的引用计数器= 0,会被GC回收,那么它装载的类型信息就会被卸载。
这就完了?当然不是。作为一个程序员,面面俱到的周全考虑是一个好习惯,让我们想一想类型重载问题。
在Java规范当中,给出了类型卸载的建议,但是,注意,这仅仅是一个建议而已。谁都无法保证我们没有错误的卸载一个类型,或者说,谁读没有保证在我们卸载一个类型之后不需要用到这个类型。
类型的重载会导致一系列的问题,比如,考虑这么一种情况:
我们使用某个类的静态成员i并改变它的值,然后“不小心”卸载了该类,那么当我们重新加载这个类的时候,i的值会赋值为原来的初始值,而不是我们想要的改变后的值。事实上,这样的例子还很多,多涉及到类的静态属性,静态方法,静态块等问题。简而言之,类的重载没有想象中的那么简单----想卸载就卸载,想重载就重载。
类的重载对程序而言是不透明的,如果我们要卸载一个类,我们要考虑到之后的很多问题。这是一个很深、很值得研究的地方,而且,还衍生出类装载器管理这样一个领域。有兴趣的朋友可以研究研究,反正我是不懂的,orz
最后一个重要问题--热更新
当程序还在运行的时候,我们突然想更新某个模块,可以么?
这看起来和动态扩展有某些相似性,其实不然,动态扩展是在程序运行期间添加某些功能(实际上就是装载某些原来没有的类),而热更新是在程序运行期间更新程序已经装载了的类。
原理其实很简单,就是卸载该类,然后重载更新后的类(同名)。