类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
通过一个类的全限定名来获取定义此类的二进制字节流;
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对应哪个存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。加载阶段由类加载器负责,过程见类加载器;
连接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备和解析三个阶段。
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等。
文件格式验证:要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
实际上第一阶段的验证点还远不止这些, 上面所列的只是一小部分内容, 该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内, 格式上符合描述一个Java类型信息的要求
元数据检查:第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求
字节码检查:通过数据流分析和控制流分析,确定 程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被校验类的方法在运行时不会做出危害 虚拟机安全的行为
延伸:由于数据流分析和控制流分析的高度复杂性,Java虚拟机的设计团队为了避免过多的执行时间消 耗在字节码验证阶段中,在JDK 6之后的Javac编译器和Java虚拟机里进行了一项联合优化,把尽可能 多的校验辅助措施挪到Javac编译器里进行。具体做法是给方法体Code属性的属性表中新增加了一项名 为“StackMapTable”的新属性,这项属性描述了方法体所有的基本块(Basic Block,指按照控制流拆分 的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,Java虚拟机就不需要根据程 序推导这些状态的合法性,只需要检查StackMapTable属性中的记录是否合法即可。这样就将字节码验 证的类型推导转变为类型检查,从而节省了大量校验时间。理论上StackMapTable属性也存在错误或被 篡改的可能,所以是否有可能在恶意篡改了Code属性的同时,也生成相应的StackMapTable属性来骗过 虚拟机的类型校验,则是虚拟机设计者们需要仔细思考的问题。 JDK 6的HotSpot虚拟机中提供了-XX:-UseSplitVerifier选项来关闭掉这项优化,或者使用参数- XX:+FailOverToOldVerifier要求在类型校验失败的时候退回到旧的类型推导方式进行校验。而到了 JDK 7之后,尽管虚拟机中仍然保留着类型推导验证器的代码,但是对于主版本号大于50(对应JDK 6)的Class文件,使用类型检查来完成数据流分析校验则是唯一的选择,不允许再退回到原来的类型 推导的校验方式
如果一个类型中有方法体的字节码没有通过字节码验证,那它肯定是有问题的;但如果一个方法 体通过了字节码验证,也仍然不能保证它一定就是安全的
符号引用验证:最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在 连接的第三阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外(常量池中的各种符号 引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部 类、方法、字段等资源
符号引用验证的主要目的是确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机 将会抛出一个java.lang.IncompatibleClassChangeError的子类异常,典型的如: java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
准备阶段负责为类中static变量分配空间,并初始化(与程序无关,系统初始化);
解析阶段负责将常量池中所有符号引用转换为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后再执行。可以认为是一些静态绑定的会被解析,动态绑定则只会在运行时进行解析;静态绑定包括一些final方法(不可以重写),static方法(只会属于当前类),构造器(不会被重写);
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通 过程序编码制定的主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器< clinit>()方法的过程。< clinit>()并不是程序员在Java代码中直接编写 的方法,它是Javac编译器的自动生成物,但我们非常有必要了解这个方法具体是如何产生的,以及 < clinit>()方法执行过程中各种可能会影响程序运行行为的细节,这部分比起其他类加载过程更贴近于普通的程序开发人员的实际工作;
类加载器虽然只用于实现类的加载动作, 但它在Java程序中起到的作用却远超类加载阶段。 对于任意一个类, 都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性, 每一个类加载器, 都拥有一个独立的类名称空间。 这句话可以表达得更通俗一些: 比较两个类是否“相等”, 只有在这两个类是由同一个类加载器加载的前提下才有意义, 否则, 即使这两个类来源于同一个Class文件, 被同一个Java虚拟机加载, 只要加载它们的类加载器不同, 那这两个类就必定不相等。
双亲委派模型的工作过程是: 如果一个类加载器收到了类加载的请求, 它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成, 每一个层次的类加载器都是如此, 因此所有的加载请求最终都应该传送到最顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类) 时, 子加载器才会尝试自己去完成加载。
代码实现:ClassLoad类的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
使用双亲委派模型来组织类加载器之间的关系, 一个显而易见的好处就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类java.lang.Object, 它存放在rt.jar之中, 无论哪一个类加载器要加载这个类, 最终都是委派给处于模型最顶端的启动类加载器进行加载, 因此Object类在程序的各种类加载器环境中都能够保证是同一个类。 反之, 如果没有使用双亲委派模型, 都由各个类加载器自行去加载的话, 如果用户自己也编写了一个名为java.lang.Object的类,并放在程序的ClassPath中, 那系统中就会出现多个不同的Object类, Java类型体系中最基础的行为也就无从保证, 应用程序将会变得一片混乱。
双亲委派模型并不是一个具有强制性约束的模型, 而是Java设计者推荐给开发者们的类加载器实现方式。
双亲模型有个问题:父加载器无法向下识别子加载器的资源
JDBC 是启动类加载器加载,但 mysql 驱动是应用类加载器。而 JDBC 运行时又需要去访问子类加载器加载的驱动,就破坏了该模型。它是怎么破坏的呢?其实引入了线程上下文来破坏该模型的。
DriverManager部分源码:
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
}
破坏双亲委派模型:
Tomcat破坏双亲委派模型
Tomcat是个web容器,可能部署不止一个应用,如果使用默认的类加载机制行不行?肯定是不行的!!
那么Tomcat是怎么做的呢?
我们看到,前面3个类加载和默认的一致,Common ClassLoader、Catalina ClassLoader、Shared ClassLoader和Webapp ClassLoader则是Tomcat自己定义的类加载器,它们分别加载/common/*、/server/*、/shared/*(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
Common ClassLoader能加载的类都可以被Catalina ClassLoader和Shared ClassLoader使用,从而实现了公有类库的共用,而Catalina ClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离;
WebApp ClassLoader可以使用Shared ClassLoader加载到的类,但各个WebApp ClassLoader实例之间相互隔离;
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperCLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。