1.类生命周期
JVM类生命周期分为5个阶段:
- 1.加载
- 2.连接
- 3.初始化
- 4.使用
- 5.卸载
1.1 加载
加载是”类加载”过程的第一个阶段,主要是将已编译好的class文件(以8字节为基础单位的二进制流,包含一个或多个类)加载到JVM内存区域中;
这一阶段主要完成3件事情:
- 1.通过类的全限定名(带包路径的类名:cn.ants.loader.test.TestClass)来获取定义此类的二进制字节流;
- 2.将这个字节流所代表的静态存储结构(类中声明的静态变量,静态方法,常量,类自身结构)转换成方法区的运行时数据结构;
- 3.在内存中生成一个代表这个类的java.lang.Class对象,做为这个类的各种数据的访问入口(对于HotSpot虚拟机来讲,Class对象存放在方法区);
通过类的全限定名来获取定义此类的二进制字节流,这句话并没有规定二进制字节流需要从一个class文件中加载,在实际运用中有多种方式,但本质都是得到类编译后的字节码(即二进制字节流)将其加载到JVM内存区域中:
- 1.从压缩包中获取,普遍的就是引入jar包加载类,发布web项目打包成war包;
- 2.从网络中获取,典型的应用是Applet;
- 3.运行时计算生成,比如现在java产生了许多字节码生成,修改工具,比如asm,cglib技术动态生成字节码,或者修改字节码;
- 4.由其他文件生成,典型场景是JSP应用,即由JSP生成对应的Class类(我们将会探究JSP热部署的实现);
- 5.从数据库中读取
相对于类加载过程的其他阶段,非数组类的加载阶段(是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的(可控性:开发人员可以选择二种方式的来完成这个操作):
- 1.通过使用系统提供的引导类加载器来完成;
- 2.通过自定义的类加载器来完成操作(重写loadClass方法);
对于数组类就不一样,因为数组类型使用JVM创建的(不通过类加载器),但是数组的元素类型最终还是需要靠类加载器去创建;
加载阶段与连接阶段的部分内容是交叉进行的,加载阶段还未完成,可能连接阶段就已经开始,但是两个阶段开始时间依然保持先后的顺序;
1.2 连接
连接阶段分为3个子过程:
1.验证
- 验证过程是连接阶段的第一步,主要目的:为了确保class文件的字节流中包含的信息符合虚拟机的要求,不会危害虚拟机的安全;(这里就不一一贴出来了:详见深入理解Java虚拟机)主要验证class文件的规范;
2.准备
-
准备过程是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中分配;
-
1.这里讲的类变量指的是static修饰的变量,不包括实例变量,实例变量跟随对象实例化分配在java堆中;
-
2.这里说的初始值’通常情况’是数据类型的零值,例如:public static int value = 1;那变量value在准备阶段后的初始值是0而不是1;因为这时候并没有开始执行任何java方法,而把value赋值为1的putstatic指令是存放与构造器clinit方法中,所以赋值操作在初始化阶段中执行;(可以通过javap命令查看class文件的字节码执行指令);(但是也有特殊情况:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段会被初始化ConstantValue的值,上述例子javac会为ConstantValue值,在准备阶段将ConstantValue值1赋给value);
-
3.解析
- 解析过程是虚拟机将常量池中符号引用替换为直接引用的过程;(对于此过程在这不做探究由于这里涉及许多指令操作,若感兴趣请自行查阅书籍资料;)
1.3 初始化
类初始化阶段是类加载过程的最后一个阶段,到了这一步开始真正的执行类中定义的java代码(除了在加载阶段使用自定义类加载器加载类);
简单讲:连接阶段准备过程中已经为类变量进行一次系统要求的初始值的初始化,但是初始化阶段执行类构造器的过程(clinit());
- clinit()方法是编译器自动收集类中的所以类变量和静态块(static{ })中的语句合并产生的,编译器收集的顺序是由语句在源文件中的位置决定的,PS:静态语句块能对定义在之后的静态变量进行赋值操作,但是不能访问此静态变量;如下面的例子:
定义一个Test.java源文件:
public class Test{
static{
value = 111;
System.out.println(value);
}
public static int value = 110;
}
编译报错:非法前向引用; 使用javap -verbose Test 查看执行指令(字节码)
Code:
stack=1, locals=0, args_size=0
0: bipush 111
2: putstatic #2 // Field value:I
5: bipush 110
7: putstatic #2 // Field value:I
10: return
最后输出value值是110;
-
<%clinit%>方法与类的构造函数(实例构造器<%init%>方法)不同,它不需要显示的调用父类构造器(<%init%>则需要,类从Object类继承,在<%init%>方法中显示调用java.lang.Object类构造器),虚拟机会保证在子类的<%clinit%>方法的执行之前,父类的<%clinit%>方法已经执行完毕.父类定义的静态块要优先于子类;
-
<%clinit%>对于类与接口不是必需的,若没有静态块和类变量赋值操作,编译器不会产生<%clinit%>方法;
-
接口中不能有静态语句块,但是依然有变量赋值操作,所以一样会生成<%clinit%>方法,但与类不同的是,执行接口的<%clinit%>方法不需要调用父类的<%clinit%>方法,当使用到父接口的变量时,才会触发初始化操作,接口实现类也一样;
-
虚拟机会保证<%clinit%>方法在多线程环境下被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个类执行<%clinit%>方法,其他线程都需要堵塞等待;
-
虚拟机规范严格规定有且只有5种情况必须对类进行“初始化”(加载,连接需在之前进行);
- 1.碰见new(实例化对象),getstatic(读静态变量),putstatic(写静态变量),invokestatic(调用静态方法)这4条指令时,如果类没有进行初始化,先进行初始化;
- 2.使用java.lang.reflect包中的方法对类进行反射调用时,若类没用进行初始化,先进行初始化;
- 3.当初始化一个类时,若此类父类没用进行初始化,先对父类进行初始化;
- 4.当虚拟机启动时,需要指定一个主类(含main()方法),虚拟机会先初始化这个主类;
- 5.使用java7动态语言支持时,即java.lang.invoke.MethodHandle实例解析后REF_getStatic,REF_putStatic,REF_invokeStatic方法句柄,若方法句柄对应的类没有进行初始化,先进行初始化;
2.类加载器
2.1 类加载器概述
类加载器实际上只用于类的加载阶段(动作),引用前面一句话就是”通过类的全限定名来获取描述此类的二进制字节流”;这个虚拟机设计团队把这个动作放到外部去实现;
举个例子:Class<?> strClass = String.class;这实际上去获取String类的类型对象,在获取之前,会进行加载动作,实际上这个加载是虚拟机中内置的类加载器来进行加载动作(这涉及到下面将到的JVM类加载器双亲委托模型);
我们可以实现自定义类加载器来完成类的加载阶段,得到类的Class对象:
class UserDefineClassLoader extends ClassLoader{
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
System.out.println("重写loadClass方法!");
return super.loadClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("重写findClass方法!");
return super.findClass(name);
}
}
我并没用做其它处理,定义完自定义类加载器之后,通过Class<?> classObject = new UserDefineClassLoader().loadClass(“loadClassName(需要完整路径)”);
由此可以明白实际上类加载器完成类的加载阶段(主要通过类的完整名称将类的二进制字节流加载到JVM中得到Class<?>对象),但是有一点需要注意:
* 1.对于任意一个类,在虚拟机中必须需要加载它的类加载器和此类才能确定类的唯一性;比如比较两个类‘相等’,现在编译生成Test.class,自定义两个类加载器TestLoader1.class,TestLoader2.class;分别用这个两个类加载器加载Test.class:
Class<?> testClass = new TestLoader1().loadClass("cn.ants.classloader.test.Test");
Class<?> testClass1 = new TestLoader2().loadClass("cn.ants.classloader.test.Test");
//测试testClass,testClass1是否相等,是否是同一个对象;
System.out.println(testClass.equals(testClass1));
//实例化Test对象;
Object obj = testClass.newInstance();
//判断obj对象是否是cn.ants.classloader.test.Test实例化对象;
System.out.println(testClass instanceof cn.ants.classloader.test.Test);
结果输出false,false;
- 因为有2个类加载器加载cn.ants.classloader.test.Test类,由于类的唯一性,所以在JVM中会产生3个类对象;即由系统加载器加载的Test类,TestLoader1加载的Test类,TestLoader2加载的Test类;
2.2 JVM双亲委托模型
既然知道类是由类加载器和本身确定其唯一性,那么jdk是怎么实现jdk类库是唯一的呢?若我们自定义类加载器加载jdk类库中的类比如加载Object类,这样又会有什么结果呢?
我们先看看jvm中有几种系统类加载器:
//拿到Test类系统类加载器
ClassLoader CLASS_LOADER = Test.class.getClassLoader();
//循环输出系统加载器的父加载器
while(CLASS_LOADER != null){
System.out.println(CLASS_LOADER);
CLASS_LOADER = CLASS_LOADER.getParent();
}
System.out.println(CLASS_LOADER);
最后输出结果如:
sun.misc.Launcher$AppClassLoader@10f243b
sun.misc.Launcher$ExtClassLoader@113c817
null
可以知道:默认是系统加载器(AppClassLoader系统应用类加载器也继承于java.lang.ClassLoader),上一级是(ExtClassLoader扩展加载器也继承于java.lang.ClassLoader),再上一级是null(不代表不存在,只是没用从java.lang.ClassLoader继承,用C++实现的类加载器BootStrap针对于HotSpot虚拟机);
所以JVM类加载器结构如下:
- 启动类加载器(Bootstrap类加载器):这个类加载器负责加载<%JAVA_HOME%>/lib下的类(但是只是加载被虚拟机识别的,并且是按文件名识别rt.jar),或者也可以利用-Xbootclasspath参数设置加载指定路径(但是只是加载虚拟机所识别的),如果需要把加载请求委派给启动类加载器,直接使用null代替即可;
- 扩展类加载器(Extension类加载器): 由sun.misc.Launcher$ExtClassLoader实现,它负责加载<%JAVA_HOME%>/lib/ext目录下的类,或者被java.ext.dirs系统变量所指定路径的所有类库,开发者可以直接使用该类加载器
- 应用程序加载器(Application类加载器):由sun.misc.Launcher$AppClassLoader实现,它负责加载<%classpath%>上所指定的类库,这个类加载器可以通过ClassLoader.getSystemClassLoader()方法获得,如果应用程序中没有自定义类加载器,一般情况下,这个是默认类加载器(由上述例子可以看出);
1.现在引入一些问题:
由于在之前我们提到,JVM中的类由类本身与加载该类的类加载器来确定类的唯一性;那JDK中的类库(例如所有类的祖先Object)是由怎么确定其唯一性呢?如果开发人员自定义类加载器来加载Object类,若加载成功得到的Object类就存在不一致的情况?JDK是怎么来保证JDK类库中的类唯一性呢?
那现在就开始引入JVM的类加载委托机制(首先看ClassLoader类中的loadClass方法(加载类核心方法)):
/**
* 加载类方法,以类的全限定名称来加载类到JVM得到Class<?>类型对象;
*/
protected Class<?> loadClass(String name, boolean resolve){
synchronized(getClassLoadingLock(name)){
//从缓存中查找,是否已经加载该类;
Class c = findLoadedClass(name);
if(c == null){
....
/*
*如果此类加载器父类不为空,则由父类加载;若类加载器为空,则直接委托Bootstrap类加载器来进行加载;
*/
if(parent != null) {
c = parent.loadClass(name,false);
}else{
c = findBootstrapClassOrNull(name);
}
....
/*
*若委托给父类加载依然为空,则调用findClass(name)方法,此方法ClassLoader类给予的实现是直接抛出一个异常;
*所以基本自定义类加载器一般都是重写这个findClass(name)方法;
*/
if(c == null){
c = findClass(name);
}
}
}
.....
return c;
}
所以从代码中我们可以轻易知道JVM中的类加载委托机制:
一个类默认是由应用加载器(AppClassLoader加载器)加载,但是当AppClassLoader加载一个类时,如果其父类不为空,则委托给父类进行加载;若委托加载之后依然未加载成功,则调用findClass来进行加载;我们在上面已经知道AppClassLoader父类是ExtClassLoader,ExtClassLoader父类是null,null代表不是java实现的类加载器,null就是Bootstrap类加载器
所以这就是类加载委托机制(用户自定义类加载器加载指定的类,则只需重写findClass方法即可)如下图所示:
有一点需要注意:如果你自定义一个类加载器,来加载java.lang包下的类,将会收到一个由虚拟机自己抛出的”java.lang.SecurityException”异常;
因为类加载过程还需要JVM解析,验证class时候会进行相关判断;
2.3 Java虚拟机中所提到的JVM类加载器不优雅的设计?
-
第一种:用户自定义类加载器,直接重写loadClass方法,重写的方法中只是包含加载的业务逻辑,若加载失败不委托给父类加载(破坏了双亲委托类加载机制);
-
第二种:双亲委托模型自身的缺陷,双亲委托模型很好的解决了基础类统一的问题(基础类:总是被用户所调用的API)比如我们定义一个类隐式继承Object类;
-
Object类就是基础类,但是如果基础类要调用用户实现的代码(类)怎么办?
- 比如前面一篇博客所谈到的SPF(服务提供者框架)JNDI,JDBC等等;
- 比如JDBC,在java.sql.DriverManager类中调用数据库厂商的(SPI)接口:
-
需要调用Driver类中的connect方法(Driver实际是由Mysql,Oracle等实现的SPI)我们通过这个代码可以看到Java是这么解决这个问题的(通过引入了线程上下文类加载器):
private static Connection getConnection(String url,Properties info,Class<?> caller){
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class){
if(callerCL == null){
//若为空,则拿当前线程的上下文类加载器(这是Java这一方面不太优雅的设计)
callerCL = Thread.currentThread().getContextClassLoader();
}
}
......
//遍历注册驱动集合中所有的驱动类
for(DriverInfo aDriver : registeredDrivers) {
//判断这个驱动是否已经加载!
if(isDriverAllowed(aDriver.driver,callerCL)){
....
//拿到服务提供者实现的驱动类调用connect方法得到数据库连接
Connection conn = aDriver.driver.connect(url,info);
....
}
}
}
线程上下文类加载器通过java.lang.Thread类中的setContextClassLoader方法来进行设置类加载器;如果创建线程时,类加载器还未设置,则会从父加载器中继承一个,如果整个应用程序中都没有设置则默认是应用类加载器(AppClassLoader);
JDBC使用这个类加载器去加载下层需要的Driver实现类(Mysql,Oracle等实现的),这实际上就是父类加载器请求子类加载器去加载所需要的SPI代码;
对于JDBC这一块我没有具体去深入探究(等有闲情再去探究啦),若想了解的人可以去通过结合阅读JDK,Mysql-Connection源码来分析哦;
*第三种:实际就是为了追求程序动态性导致的,最典型的应用就是热部署(HotSwap)比如:JSP(修改JSP页面代码,不需要重启服务器;若修改了Java代码则需要进行服务器reload操作,但是这个是自动进行的);
好了,类加载器这一块我先讲到这啦(其实很惭愧,这篇概念性的东西全都是参考于<深入理解Java虚拟机:JVM高级特性与最佳实践>这本书)
下一篇博文:还是准备延续ClassLoader来讲(主要涉及自定义实现类的热部署(HotSwap)功能,探究分析Tomcat中JSP中的热部署)