<谭锋>整理 java
为了支持跨平台的特性,java语言采用源代码编译成中间字节码,而后又各平台的jvm解释执行的方式。字节码采用了彻底与平台无关的方式进行描述,java只给出了字节码格式的规范,并无规定字节码最终来源是什么,它能够是除了java语言外的其余语言产生,只要是知足字节码规范的,均可以在jvm中很好的运行。正由于这个特性,极大的促进了各种语言的发展,在jvm平台上出现了不少语言,如scala,groovy等 mysql
因为字节码来源并无作限制,所以jvm必须在字节码正式使用以前,即在加载过程当中,对字节码进行检查验证,以保证字节码的可用性和安全性。 c++
在正式介绍以前,先看看jvm内存结构划分: web
结合垃圾回收机制,将堆细化: spring
在加载阶段主要用到的是方法区: sql
方法区是可供各条线程共享的运行时内存区域。存储了每个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法。 数据库
若是把方法的代码看做它的“静态”部分,而把一次方法调用须要记录的临时数据看作它的“动态”部分,那么每一个方法的代码是只有一份的,存储于JVM的方法区中;每次某方法被调用,则在该调用所在的线程的的Java栈上新分配一个栈帧,用于存放临时数据,在方法返回时栈帧自动撤销。
tomcat
jvm将类加载过程分红加载,链接,初始化三个阶段,其中链接阶段又细分为验证,准备,解析三个阶段。 安全
上述三个阶段整体上会保持这个顺序,可是有些特殊状况,如加载阶段与链接阶段的部份内容(一部分字节码的验证工做)是交叉进行的。再如:解析阶段能够是推迟初次访问某个类的时候,所以它可能出如今初始化阶段以后。 服务器
装载阶段主要是将java字节码以二进制的方式读入到jvm内存中,而后将二进制数据流按照字节码规范解析成jvm内部的运行时数据结构。java只对字节码进行了规范,并无对内部运行时数据结构进行规定,不一样的jvm实现能够采用不一样的数据结构,这些运行时数据结构是保存在jvm的方法区中(hotspot jvm的内部数据结构定义能够参见撒迦的博文借助HotSpot SA来一窥PermGen上的对象)。当一个类的二进制解析完毕后,jvm最终会在堆上生成一个java.lang.Class类型的实例对象,经过这个对象能够访问到该类在方法区的内容。
jvm规范并无规定从二进制字节码数据应该如何产生,事实上,jvm为了支持二进制字节码数据来源的可扩展性,它提供了一个回调接口将经过一个类的全限定名来获取描述此类的二进制字节码的动做开放到jvm的外部实现,这就是咱们后面要讲到的类加载器,若是有须要,咱们彻底能够自定义一些类加载器,达到一些特殊应用场景。因为有了jvm的支持,二进制流的产生的方式能够是:
(1) 从本地文件系统中读取
(2) 从网络上加载(典型应用:java Applet)
(3) 从jar,zip,war等压缩文件中加载
(4) 经过动态将java源文件动态编译产生(jsp的动态编译)
(5) 经过程序直接生成。
链接阶段主要是作一些加载完成以后的验证工做,和初始化以前的准备一些工做,它细分为三个阶段。
验证是链接阶段的第一步,它主要是用于保证加载的字节码符合java语言的规范,而且不会给虚拟机带来危害。好比验证这个类是否是符合字节码的格式、变量与方法是否是有重复、数据类型是否是有效、继承与实现是否合乎标准等等。按照验证的内容不一样又能够细分为4个阶段:文件格式验证(这一步会与装载阶段交叉进行),元数据验证,字节码验证,符号引用验证(这个阶段的验证每每会与解析阶段交叉进行)。
准备阶段主要是为类的静态变量分配内存,并设置jvm默认的初始值。对于非静态的变量,则不会为它们分配内存。
在jvm中各种型的初始值以下:
int,byte,char,long,float,double 默认初始值为0
boolean 为false(在jvm内部用int表示boolean,所以初始值为0)
reference类型为null
对于final static基本类型或者String类型,则直接采用常量值(这其实是在编译阶段就已经处理好了)。
解析过程就是查找类的常量池中的类,字段,方法,接口的符号引用,将他们替换成直接引用的过程。
a.解析过程主要针对于常量池中的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四种常量。
b. jvm规范并无规定解析阶段发生的时间,只是规定了在执行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic这13个指令应用于符号指令时,先对它们进行解析,获取它们的直接引用.
c. jvm对于每一个加载的类都会有在内部建立一个运行时常量池(参考上面图示),在解析以前是以字符串的方式将符号引用保存在运行时常量池中,在程序运行过程当中当须要使用某个符号引用时,就会促发解析的过程,解析过程就是经过符号引用查找对应的类实体,而后用直接引用替换符号引用。因为符号引用已经被替换成直接引用,所以后面再次访问时,无需再次解析,直接返回直接引用。
初始化阶段是根据用户程序中的初始化语句为类的静态变量赋予正确的初始值。这里初始化执行逻辑最终会体如今类构造器方法<clinit>()方中。该方法由编译器在编译阶段生成,它封装了两部份内容:静态变量的初始化语句和静态语句块。
jvm规范明确规定了初始化执行条件,只要知足如下四个条件之一,就会执行初始化工做
(1) 经过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法(对应new,getstatic,putstatic,invokespecial这四条字节码指令)。
(2) 经过反射方式执行以上行为时。
(3) 初始化子类的时候,会触发父类的初始化。
(4) 做为程序入口直接运行时的主类。
初始化过程包括两步:
(1) 若是类存在直接父类,而且父类没有被初始化则对直接父类进行初始化。
(2) 若是类当前存在<clinit>()方法,则执行<clinit>()方法。
须要注意的是接口(interface)的初始化并不要求先初始化它的父接口。(接口不能有static块)
并非每一个类都有<clinit>()方法,以下状况下不会有<clinit>()方法:
a. 类没有静态变量也没有静态语句块
b.类中虽然定义了静态变量,可是没有给出明确的初始化语句。
c.若是类中仅包含了final static 的静态变量的初始化语句,并且初始化语句采用编译时常量表达时,也不会有<clinit>()方法。
例子:
public class ConstantExample { public static final int a = 10; public static final float b = a * 2.0f; }编译以后用 javap -verbose ConstantExample查看字节码,显示以下:
{ public static final int a; Constant value: int 10 public static final float b; Constant value: float 20.0f public ConstantExample(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #15; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 12: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this LConstantExample; }
这里因为编译器直接10,看成常量来处理,看到是没有<clinit>()方法存在的。能够看成常量来处理的类型包括基本类型和String类型
对于其余类型:
public class ConstantExample1 { public static final int a = 10; public static final float b = a * 2.0f; public static final Date c = new Date(); }这里虽然c被声明成final,可是仍然会产生<clinit>()方法,以下所示:
{ public static final int a; Constant value: int 10 public static final float b; Constant value: float 20.0f public static final java.util.Date c; static {}; Code: Stack=2, Locals=0, Args_size=0 0: new #17; //class java/util/Date 3: dup 4: invokespecial #19; //Method java/util/Date."<init>":()V 7: putstatic #22; //Field c:Ljava/util/Date; 10: return LineNumberTable: line 19: 0 line 14: 10
在同一个类加载器域下,每一个类只会被初始化一次,当多个线程都须要初始化同一个类,这时只容许一个线程执行初始化工做,其余线程则等待。当初始化执行完后,该线程会通知其余等待的线程。
先上代码
public class TestThread extends Thread implements Cloneable { public static void main(String[] args) { TestThread t = new TestThread(); t.start(); } }
上面这代码中TestThread及相关类在jvm运行的存储和引用状况以下图所示:
其中 t 做为TestThread对象的一个引用存储在线程的栈帧空间中,Thread对象及类型数据对应的Class对象实例都存储在堆上,类型数据存储在方法区,前面讲到了,TestThread的类型数据中的符号引用在解析过程当中会被替换成直接引用,所以TestThread类型数据中会直接引用到它的父类Thread及它实现的接口Cloneable的类型数据。
在同一个类加载器空间中,对于全限定名相同的类,只会存在惟一的一份类的实例及类型数据。实际上类的实例数据和其对应的Class对象是相互引用的。
上面已经讲到类加载器实际上jvm在类加载过程当中的装载阶段开放给外部使用的一个回调接口,它主要实现的功能就是:将经过一个类的全限定名来获取描述此类的二进制字节码。固然类加载器的优点远不止如此,它是java安全体系的一个重要环节(java安全体系结构,后面会专门写篇文章讨论),同时经过类加载器的双亲委派原则等类加载器和class惟一性标识一个class的方式,能够给应用程序带来一些强大的功能,如hotswap。
在jvm中一个类实例的惟一性标识是类的全限定名和该类的加载器,类加载器至关于一个命名空间,将同名class进行了隔离。
从jvm的角度来讲,只存在两类加载器,一类是由c++实现的启动类加载器,是jvm的一部分,一类是由java语言实现的应用程序加载器,独立在jvm以外。
jkd中本身定义了一些类加载器:
(1).BootStrap ClassLoader:启动类加载器,由C++代码实现,负责加载存放在%JAVA_HOME%\lib目录中的,或者通被-Xbootclasspath参数所指定的路径中的,而且被java虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库,即便放在指定路径中也不会被加载)类库到虚拟机的内存中,启动类加载器没法被java程序直接引用。
(2).Extension ClassLoader:扩展类加载器,由sun.misc.Launcher$ExtClassLoader实现,负责加载%JAVA_HOME%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的全部类库,开发者能够直接使用扩展类加载器。
(3).Application ClassLoader:应用程序类加载器,由sun.misc.Launcher$AppClassLoader实现,负责加载用户类路径classpath上所指定的类库,是类加载器ClassLoader中的getSystemClassLoader()方法的返回值,开发者能够直接使用应用程序类加载器,若是程序中没有自定义过类加载器,该加载器就是程序中默认的类加载器。
参考ClassLoader源代码会发现,这些Class之间并非采用继承的方式实现父子关系,而是采用组合方式。正常状况下,每一个类加载在收到类加载请求时,会先调用父加载器进行加载,若父加载器加载失败,则子加载器进行加载。
在java中有两种办法能够在应用程序中主动加载类:
一种是Class类的forName静态方法
public static Class<?> forName(String className) throws ClassNotFoundException //容许指定是否初始化,而且指定类的类加载器 public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
另外一种就是ClassLoader中的loadClass方法
protected synchronized Class<?> loadClass(String name, boolean resolve) //第二个参数表示是否在转载完后进行链接(解析) throws ClassNotFoundException public Class<?> loadClass(String name) throws ClassNotFoundException
上面这两种方式是有区别的,以下例所示
public class InitialClass { public static int i; static { i = 1000; System.out.println("InitialClass is init"); } }
public class InitClassTest { public static void main(String[] args) throws MalformedURLException, ClassNotFoundException { Class classFromForName = Class.forName("com.alibaba.china.jianchi.example.InitialClass", true, new URLClassLoader( new URL[] { new URL( "file:/home/tanfeng/workspace/springStudy/bin/") }, InitClassTest.class.getClassLoader())); Class classFromClassLoader = (new URLClassLoader( new URL[] { new URL( "file:/home/tanfeng/workspace/springStudy/bin/") }, InitClassTest.class.getClassLoader())).loadClass("com.alibaba.china.jianchi.example.InitialClass"); } }
经过运行能够考到用Class.forName()方法会将装载的类初始化,而ClassLoader.loadClass()方法则不会。
咱们常常会看到在数据库操做时,会用Class.forName()的方式加载驱动类,而不是ClassLoader.loadClass()方法,为什么要这样呢?
来看看mysql的驱动类实现,能够看到在类的初始化阶段,它会将本身注册到驱动管理器中(static块)。
package com.mysql.jdbc; public class Driver extends NonRegisteringDriver implements java.sql.Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } ... ... }
(1)部署在一个服务器上的两个Web应用程序自身所使用的Java类库是相互隔离的。
(2)部署在一个服务器上的两个Web应用程序能够共享服务器提供的java共用类库。
(3)服务器尽量的保证自身安全不受部署的Web应用程序影响。
(4)支持对JSP的HotSwap功能。
tomcat主要根据根据java类库的共享范围,分为4组目录:
(1)common目录:能被Tomcat和全部Web应用程序共享。
(2)server目录:仅能被Tomcat使用,其余Web应用程序不可见。
(3)Shared目录:能够被全部Web应用程序共享,对Tomcat不可见。
(4)WEB-INF目录:只能被当前Web应用程序使用,对其余web应用程序不可见。
这几个类加载器分别对应加载/common/*、/server/*、/shared/*和 /WEB-INF/*类库, 其中Webapp类加载器和Jsp类加载器会存在多个,每一个Web应用对应一个Webapp类加载器。
CommonClassLoader加载的类能够被CatalinaClassLoader和ShareClassLoader使用;CatalinaClassLoader加载的类和ShareClassLoader加载的类相互隔离; WebappClassLoader可使用ShareClassLoader加载的类,但各个WebappClassLoader间相互隔离;JspClassLoader仅能用JSP文件编译的class文件。