jvm类加载过程分析

来源:互联网 发布:注册码万能破解软件 编辑:程序博客网 时间:2024/04/29 07:50

jvm类加载过程分析

目录(?)[+]

  1. jvm运行时内存结构划分
  2. 类加载过程
    1. 装载
    2. 连接
      1. 验证
      2. 准备
      3. 解析
    3. 初始化
      1. 初始化执行时机
      2. 初始化过程
      3. clinit方法存在的条件
      4. 并发性
    4. 在使用过程中类对象在方法区和堆上的分布状态
  3. 类加载器
    1. 双亲委派模型
    2. 两种主动加载方式
    3. 自定义类加载器的应用
      1. Tomcat中类加载器分析
        1. tomcat中通过自定义一组类加载器解决了以下几个问题
        2. tomcat的目录结构
        3. tomcat自定义类加载器


为了支持跨平台的特性,java语言采用源代码编译成中间字节码,然后又各平台的jvm解释执行的方式。字节码采用了完全与平台无关的方式进行描述,java只给出了字节码格式的规范,并没有规定字节码最终来源是什么,它可以是除了java语言外的其他语言产生,只要是满足字节码规范的,都可以在jvm中很好的运行。正因为这个特性,极大的促进了各类语言的发展,在jvm平台上出现了很多语言,如scala,groovy等

由于字节码来源并没有做限制,因此jvm必须在字节码正式使用之前,即在加载过程中,对字节码进行检查验证,以保证字节码的可用性和安全性。


1. jvm运行时内存结构划分

在正式介绍之前,先看看jvm内存结构划分:


结合垃圾回收机制,将堆细化:


在加载阶段主要用到的是方法区:

方法区是可供各条线程共享的运行时内存区域。存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法

如果把方法的代码看作它的“静态”部分,而把一次方法调用需要记录的临时数据看做它的“动态”部分,那么每个方法的代码是只有一份的,存储于JVM的方法区中;每次某方法被调用,则在该调用所在的线程的的Java栈上新分配一个栈帧,用于存放临时数据,在方法返回时栈帧自动撤销。

2. 类加载过程

jvm将类加载过程分成加载,连接,初始化三个阶段,其中连接阶段又细分为验证,准备,解析三个阶段。



上述三个阶段总体上会保持这个顺序,但是有些特殊情况,如加载阶段与连接阶段的部分内容(一部分字节码的验证工作)是交叉进行的。再如:解析阶段可以是推迟初次访问某个类的时候,因此它可能出现在初始化阶段之后。

2.1 装载

装载阶段主要是将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) 通过程序直接生成。


2.2 连接

连接阶段主要是做一些加载完成之后的验证工作,和初始化之前的准备一些工作,它细分为三个阶段。

2.2.1 验证

验证是连接阶段的第一步,它主要是用于保证加载的字节码符合java语言的规范,并且不会给虚拟机带来危害。比如验证这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。按照验证的内容不同又可以细分为4个阶段:文件格式验证(这一步会与装载阶段交叉进行),元数据验证,字节码验证,符号引用验证(这个阶段的验证往往会与解析阶段交叉进行)。

2.2.2 准备

准备阶段主要是为类的静态变量分配内存,并设置jvm默认的初始值。对于非静态的变量,则不会为它们分配内存。

在jvm中各类型的初始值如下:

int,byte,char,long,float,double 默认初始值为0

boolean 为false(在jvm内部用int表示boolean,因此初始值为0)

reference类型为null

对于final static基本类型或者String类型,则直接采用常量值(这实际上是在编译阶段就已经处理好了)。

2.2.3 解析

解析过程就是查找类的常量池中的类,字段,方法,接口的符号引用,将他们替换成直接引用的过程。

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对于每个加载的类都会有在内部创建一个运行时常量池(参考上面图示),在解析之前是以字符串的方式将符号引用保存在运行时常量池中,在程序运行过程中当需要使用某个符号引用时,就会促发解析的过程,解析过程就是通过符号引用查找对应的类实体,然后用直接引用替换符号引用。由于符号引用已经被替换成直接引用,因此后面再次访问时,无需再次解析,直接返回直接引用。

2.3 初始化

初始化阶段是根据用户程序中的初始化语句为类的静态变量赋予正确的初始值。这里初始化执行逻辑最终会体现在类构造器方法<clinit>()方中。该方法由编译器在编译阶段生成,它封装了两部分内容:静态变量的初始化语句和静态语句块。

2.3.1 初始化执行时机

jvm规范明确规定了初始化执行条件,只要满足以下四个条件之一,就会执行初始化工作

(1) 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法(对应new,getstatic,putstatic,invokespecial这四条字节码指令)。

(2) 通过反射方式执行以上行为时。

(3) 初始化子类的时候,会触发父类的初始化。

(4) 作为程序入口直接运行时的主类。

2.3.2 初始化过程

初始化过程包括两步:

(1) 如果类存在直接父类,并且父类没有被初始化则对直接父类进行初始化。

(2) 如果类当前存在<clinit>()方法,则执行<clinit>()方法。

需要注意的是接口(interface)的初始化并不要求先初始化它的父接口。(接口不能有static块)

2.3.3 <clinit>()方法存在的条件

并不是每个类都有<clinit>()方法,如下情况下不会有<clinit>()方法:

a. 类没有静态变量也没有静态语句块

b.类中虽然定义了静态变量,但是没有给出明确的初始化语句。

c.如果类中仅包含了final static 的静态变量的初始化语句,而且初始化语句采用编译时常量表达时,也不会有<clinit>()方法。

例子:

代码:

[java] view plaincopyprint?
  1. public class ConstantExample {  
  2.   
  3.     public static final int   a = 10;  
  4.     public static final float b = a * 2.0f;  
  5. }  
编译之后用 javap -verbose ConstantExample查看字节码,显示如下:

[java] view plaincopyprint?
  1. {  
  2. public static final int a;  
  3.   Constant value: int 10  
  4. public static final float b;  
  5.   Constant value: float 20.0f  
  6. public ConstantExample();  
  7.   Code:  
  8.    Stack=1, Locals=1, Args_size=1  
  9.    0:   aload_0  
  10.    1:   invokespecial   #15//Method java/lang/Object."<init>":()V  
  11.    4:   return  
  12.   LineNumberTable:   
  13.    line 120  
  14.   
  15.   LocalVariableTable:   
  16.    Start  Length  Slot  Name   Signature  
  17.    0      5      0    this       LConstantExample;  
  18.   
  19. }  
这里由于编译器直接10,当作常量来处理,看到是没有<clinit>()方法存在的。可以当作常量来处理的类型包括基本类型和String类型

对于其他类型:

[java] view plaincopyprint?
  1. public class ConstantExample1 {  
  2.   
  3.     public static final int   a = 10;  
  4.     public static final float b = a * 2.0f;  
  5.     public static final Date  c = new Date();  
  6. }  
这里虽然c被声明成final,但是仍然会产生<clinit>()方法,如下所示:

[java] view plaincopyprint?
  1. {  
  2. public static final int a;  
  3.   Constant value: int 10  
  4. public static final float b;  
  5.   Constant value: float 20.0f  
  6. public static final java.util.Date c;  
  7.   
  8. static {};  
  9.   Code:  
  10.    Stack=2, Locals=0, Args_size=0  
  11.    0:   new #17//class java/util/Date  
  12.    3:   dup  
  13.    4:   invokespecial   #19//Method java/util/Date."<init>":()V  
  14.    7:   putstatic   #22//Field c:Ljava/util/Date;  
  15.    10:  return  
  16.   LineNumberTable:   
  17.    line 190  
  18.    line 1410  

2.3.4 并发性

在同一个类加载器域下,每个类只会被初始化一次,当多个线程都需要初始化同一个类,这时只允许一个线程执行初始化工作,其他线程则等待。当初始化执行完后,该线程会通知其他等待的线程。


2.4 在使用过程中类,对象在方法区和堆上的分布状态

先上代码

[java] view plaincopyprint?
  1. public class TestThread extends Thread implements Cloneable {  
  2.   
  3.     public static void main(String[] args) {  
  4.         TestThread t = new TestThread();  
  5.         t.start();  
  6.     }  
  7. }  

上面这代码中TestThread及相关类在jvm运行的存储和引用情况如下图所示:



其中 t 作为TestThread对象的一个引用存储在线程的栈帧空间中,Thread对象及类型数据对应的Class对象实例都存储在堆上,类型数据存储在方法区,前面讲到了,TestThread的类型数据中的符号引用在解析过程中会被替换成直接引用,因此TestThread类型数据中会直接引用到它的父类Thread及它实现的接口Cloneable的类型数据。

在同一个类加载器空间中,对于全限定名相同的类,只会存在唯一的一份类的实例及类型数据。实际上类的实例数据和其对应的Class对象是相互引用的。

3. 类加载器

上面已经讲到类加载器实际上jvm在类加载过程中的装载阶段开放给外部使用的一个回调接口,它主要实现的功能就是:将通过一个类的全限定名来获取描述此类的二进制字节码。当然类加载器的优势远不止如此,它是java安全体系的一个重要环节(java安全体系结构,后面会专门写篇文章讨论),同时通过类加载器的双亲委派原则等类加载器和class唯一性标识一个class的方式,可以给应用程序带来一些强大的功能,如hotswap。

3.1 双亲委派模型

在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之间并不是采用继承的方式实现父子关系,而是采用组合方式。

正常情况下,每个类加载在收到类加载请求时,会先调用父加载器进行加载,若父加载器加载失败,则子加载器进行加载。

3.2 两种主动加载方式

在java中有两种办法可以在应用程序中主动加载类:

一种是Class类的forName静态方法

[java] view plaincopyprint?
  1. public static Class<?> forName(String className)   
  2.                 throws ClassNotFoundException   
  3. //允许指定是否初始化,并且指定类的类加载器   
  4. public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException   

另一种就是ClassLoader中的loadClass方法

[java] view plaincopyprint?
  1.  protected synchronized Class<?> loadClass(String name, boolean resolve) //第二个参数表示是否在转载完后进行连接(解析)  
  2.     throws ClassNotFoundException  
  3.   
  4. public Class<?> loadClass(String name) throws ClassNotFoundException  

上面这两种方式是有区别的,如下例所示

[java] view plaincopyprint?
  1. public class InitialClass {  
  2.   
  3.     public static int i;  
  4.     static {  
  5.         i = 1000;  
  6.         System.out.println("InitialClass is init");  
  7.     }  
  8.   
  9. }  


[java] view plaincopyprint?
  1. public class InitClassTest {  
  2.   
  3.     public static void main(String[] args) throws MalformedURLException, ClassNotFoundException {  
  4.         Class classFromForName = Class.forName("com.alibaba.china.jianchi.example.InitialClass",  
  5.                                                true,  
  6.                                                new URLClassLoader(  
  7.                                                                   new URL[] { new URL(  
  8.                                                                                       "file:/home/tanfeng/workspace/springStudy/bin/") },  
  9.                                                                   InitClassTest.class.getClassLoader()));  
  10.   
  11.         Class classFromClassLoader = (new URLClassLoader(  
  12.                                                          new URL[] { new URL(  
  13.                                                                              "file:/home/tanfeng/workspace/springStudy/bin/") },  
  14.                                                          InitClassTest.class.getClassLoader())).loadClass("com.alibaba.china.jianchi.example.InitialClass");  
  15.   
  16.     }  
  17. }  

通过运行可以考到用Class.forName()方法会将装载的类初始化,而ClassLoader.loadClass()方法则不会。

我们经常会看到在数据库操作时,会用Class.forName()的方式加载驱动类,而不是ClassLoader.loadClass()方法,为何要这样呢?

来看看mysql的驱动类实现,可以看到在类的初始化阶段,它会将自己注册到驱动管理器中(static块)。

[java] view plaincopyprint?
  1. package com.mysql.jdbc;  
  2. public class Driver extends NonRegisteringDriver implements java.sql.Driver {  
  3.   
  4.     static {  
  5.         try {  
  6.             java.sql.DriverManager.registerDriver(new Driver());  
  7.         } catch (SQLException E) {  
  8.             throw new RuntimeException("Can't register driver!");  
  9.         }  
  10.     }  
  11.       ... ...  
  12. }  

3.3 自定义类加载器的应用

3.3.1 Tomcat中类加载器分析

3.3.1.1 tomcat中通过自定义一组类加载器,解决了以下几个问题:

(1)部署在一个服务器上的两个Web应用程序自身所使用的Java类库是相互隔离的。

(2)部署在一个服务器上的两个Web应用程序可以共享服务器提供的java共用类库。

(3)服务器尽可能的保证自身安全不受部署的Web应用程序影响。

(4)支持对JSP的HotSwap功能。

3.3.1.2 tomcat的目录结构

tomcat主要根据根据java类库的共享范围,分为4组目录:

(1)common目录:能被Tomcat和所有Web应用程序共享。
(2)server目录:仅能被Tomcat使用,其他Web应用程序不可见。
(3)Shared目录:可以被所有Web应用程序共享,对Tomcat不可见。
(4)WEB-INF目录:只能被当前Web应用程序使用,对其他web应用程序不可见。

3.3.1.3 tomcat自定义类加载器


这几个类加载器分别对应加载/common/*、/server/*、/shared/*和 /WEB-INF/*类库, 其中Webapp类加载器和Jsp类加载器会存在多个,每个Web应用对应一个Webapp类加载器。

CommonClassLoader加载的类可以被CatalinaClassLoader和ShareClassLoader使用;CatalinaClassLoader加载的类和ShareClassLoader加载的类相互隔离; WebappClassLoader可以使用ShareClassLoader加载的类,但各个WebappClassLoader间相互隔离;JspClassLoader仅能用JSP文件编译的class文件。

0 0