深入理解JVM内部结构

来源:互联网 发布:中国煤炭资源 知乎 编辑:程序博客网 时间:2024/06/10 11:24
 这篇文章主要是解释java虚拟机(JVM)的内部结构。下图显示了符合Java SE 7 版本的Java虚拟机规范的一个典型JVM中的关键内部组件。


     图中显示的组件将会在下面两部分中进行逐一的解释。第一部分涉及JVM为每一个线程都会创建的组件;第二部分则是独立于线程进行创建的组件。

1. Thread
     Thread是一个程序中的一个执行线程。JVM允许一个应用程序有多个执行线程并发运行。在Sun的Hotspot JVM中,Java线程与本地操作系统线程间存在一个直接的一一映射。JVM先为Java线程准备好所有的状态,如线程局部存储、分配缓冲区、同步对象、栈和程序计数器,之后对应的本地线程被创建。Java线程终止以后其本地线程将会被回收利用,由操作系统负责调度所有的线程并将它们分派到可用的CPU。一旦本地线程已经初始化完成,则会调用Java线程的run()方法,当run()方法返回时,未捕获的异常将被处理,之后本地线程判断JVM是否需要终止运行(如此线程是最后一个非守护线程)。当线程终止以后,本地线程及Java线程占用的所有资源都会被释放。

1.1 JVM系统线程
    如果你曾使用jconsole或其它的一些调试工具,也许你会注意到JVM中有许多的线程运行在后台。这些运行的后台线程,除了main线程以外,都是作为调用public static void main(String [])的一部分被创建,此外,还有一些是被main线程所创建。以Sun的Hotspot JVM为例,主要的后台系统线程有:

(1) VM Thread 
     此线程用于等待请求JVM到达一个“安全点”的操作的出现。之所以所有这类操作需要在一个单独的线程——VM Thread中进行,是因为它们都要求JVM处于“安全点”,当JVM位于“安全点”时,不能对heap执行修改操作。VM Thread执行的操作类型是会“stop-the-world”的垃圾回收、线程堆栈转储、线程挂起和偏向锁撤销。
注:stop-the-world,也即当执行此操作时,Java虚拟机需要停止运行。

(2) Periodic task thread(周期任务线程)
     此线程负责用于调度周期性操作的执行的定时事件。

(3) GC threads(垃圾回收线程)
      这些线程用于支持JVM中进行的不同类型的垃圾回收活动。


(4) Compiler threads
      这些线程在运行时将字节码编译成本地代码。

(5) Signal dispatcher thread(信号分发线程)
      此线程负责接收发送给JVM进程的信号,处理信号,根据系统设置调用合适的JVM方法。

1.2 Per Thread
     每一个执行的线程都具有下列组件:
(1) 程序计数器(program Counter,PC)
    除非是本地代码,否则PC指向当前指令(也即opcode)的地址。如果当前方法是本地的,则PC是未定义的。所有的CPU都有一个PC,一般情况下,PC会在每一个指令之后进行递增,以保存下一个执行指令的地址。JVM使用PC来记录当前指令执行的位置,PC实际上指向了方法区的一个内存地址。

(2) 栈(Stack)
   每个线程都有自己的栈,栈为线程中执行的每个方法保存了一个对应的帧(Frame)。栈是后进先出(LIFO)的数据结构,因此当前执行的方法总是位于栈的顶端。当调用一个方法时,为其创建一个新的帧并压栈,当方法正常返回或者是由于执行期间抛出了未捕获的异常而退出时,其对应的帧将被删除(也即出栈)。除了执行帧的压栈与出栈,无法对栈进行其它的直接操作,因此帧对象也许是在堆(Heap)中进行内存分配,它们的内存也就无需是连续的。

(3) 本地栈(Native Stack)
    并非所有的JVM都支持本地方法,然而,那些支持的JVM一般会为每个线程创建本地方法栈。如果JVM是使用“C链接”模型实现Java本地调用(JNI)的,则本地栈将是一个C栈。在这种情况下,本地栈中参数及返回值的顺序将与典型的C程序是一致的。一个本地方法一般会调回JVM,并调用Java方法,此类从本地方法到Java方法调用发生于正常的Java栈中,线程会离开本地栈,并在Java栈中为方法创建一个新的帧。

(4)栈的限制(Stack Restrictions)
栈可以是动态的,也可以是固定大小的。如果一个线程要求超过允许的大小的栈,则JVM会抛出StackOverflowError。如果线程要求创建一个新的帧,但是系统没有足够内存进行分配,则JVM会抛出OutOfMemoryError。

(5) 帧(Frame)
原文内容与栈部分大致一致,不冗述。
每个帧中包含有:1. 局部变量数组;2. 返回值;3. 操作对象栈;4. 到方法所属类的运行时常量池的引用。

(6) 局部变量数组(Loca Variables Array)
   局部变量数组中包含了方法运行期间使用到的所有变量,包括到this变量的引用、所有的方法参数及其它方法中定义的局部变量。对于类方法(静态方法),方法参数从“零”开始,而对于实例方法,“零”位置预留给this变量。
一个局部变量可以是:
  • boolean
  • byte
  • char
  • long
  • short
  • int
  • float
  • double
  • reference
  • returnAddress
除了long和double,所有的类型都在局部变量数组中占据一个位置,long及double则占用两个连续的位置(slot),由于它们是双倍的宽度,也即64位大小。

(7) 操作对象栈
    操作对象栈在字节码指令执行期间使用,作用类似于CPU使用的一般用途的寄存器。大多数的JVM操纵操作对象栈的方式有:压栈、出栈、复制、交换、通过执行某一操作产生或消耗成数值。因此,在字节码中,在局部变量数组与操作对象栈之间移动数值的指令出现的次数很多。例如,一个简单的变量就需要两条与操作对象栈进行交互的字节码。
     
[java] view plaincopy
  1. int i;  
     编译以后的字节码如下:
     
[java] view plaincopy
  1. 0: iconst_0 // Push 0 to top of the operand stack  
  2. 1: istore_1 // Pop value from top of operand stack and store as local variable 1  

(8) 动态链接
       每个帧包含了一个到运行时常量池的引用。此引用指向当前执行的方法对应的类的常量池,用于辅助支持动态链接功能。
     当一个类被编译以后,所有到变量及方法的引用都被存储到类的常量池中作为一个符号引用。一个符号引用是一个逻辑引用而不是直接指向物理内存地址的引用。JVM的实现可以自由选择何时解析符号引用,可能的选择有:类文件被验证时亦或是被加载以后,这类方案被称为是急切的(eager)或静态的(static)解析;还有一种被称为懒加载或延时加载方案,它是在符号引用第一次被使用时进行解析。然而,JVM必须要表现得像是当引用第一次被使用时才进行解析,并且此时需要抛出任何的解析错误。
    绑定是由符号引用标识的域、方法和类被直接引用替换的过程,此过程只发生一次。如果指向类的符号引用还没有被解析则此类会被加载。每一个直接引用都被存储为一个相对偏移值,以与变量或方法的运行时位置关联的存储结构作为基准。

1.3 Shared Between Threads
(1) 堆(Heap)
  堆用于运行时分配类实例及数组。数组及对象无法存储在栈中,因为帧在创建以后,其大小就无法再改变。帧中只能存储指向堆中对象或数组的引用。与简单变量与在局部变量中的引用不同,数组对象总是存储在堆中,因此当方法退出时,它们没有被移除,对象只能被垃圾收集器移除。
     为了支持垃圾回收,堆被分为三个部分:
  • Young Generation(年轻代),通常被分为Eden和Survivor
  • Old Generation(老年代,也叫Tenured Generation)
  • Permanent Generation
(2) 内存管理
      对象及数组不会被显示释放,垃圾回收器会自动回收它们。
此过程如下:
1. 新的对象和数组被创建并放到年轻代中;
2. 一次小的垃圾收集会在年轻代上执行,如果对象存活下来,则它们将被从eden空间移动到survivor空间;
3. 主垃圾收集(Major)执行,它会引起应用线程停止运行,也即stop-the-world,同时会在不同的“代”中移动对象。经过此次垃圾回收后,如果对象仍然存活,则它们会被从年轻代移动到老年代。
4. 在每次老年代被回收时永久代也被回收,当任何一个空间满时,它们都会被回收。

(3) 非堆内存
   从逻辑上考虑,被认为是JVM结构的一部分的对象将不会在堆中进行创建。非堆内存包括有:1.包含方法区及interned字符串的永久代;2. 代码缓存,用于编辑与存储已经被JIT编译器编译为本地代码的方法。

(4) 即时编译
Java字节码采用解释执行方式,因此它没法像直接在JVM所在的主机上执行本地代码那么快。为了提高性能,Oracle Hotspot VM寻找字节码中被周期地执行的“热区域”,并将它们编译为本地代码。本地代码被存储到非堆内存的代码缓存中,通过这样的方式,Hotspot VM尝试选择最合适的方式来权衡编译代码花费的额外时间与执行解释代码花费的额外时间。

(5) 方法区(Method Area)
方法区中存储了每个类的元信息,如:
  • 类加载器引用
  • 运行时常量池
    • 数值常量
    • 字段引用
    • 方法引用
    • 属性值
  • 字段数据
    • 每个字段
      • 名称
      • 类型
      • Modifiers
      • 属性值
  • 方法数据
    • 每个方法
      • 名称
      • 返回类型
      • 参数类型
      • Modifiers
      • 属性值
  • 方法代码
    • 每个方法
      • 字节码
      • 操作对象栈大小
      • 局部变量大小
      • 局部变量表
      • 异常表
        • 每个异常处理器
          • 起始点
          • 结束点
          • 处理器代码的PC偏移值
          • 捕抓的异常类的常量池索引
   所有的线程共享相同的方法区,因此方法区数据的存取及动态链接的过程必须是线程安全的。如果两个线程都企图存取同一个类中的字段或方法,但是,此类还没有被加载,则它必须只被加载一次,并且线程必须等到类被加载完成以后才能继续执行。

(6) 类文件结构
       一个编译完成的类文件包含以下的结构:
[java] view plaincopy
  1. ClassFile {  
  2.     u4          magic;  
  3.     u2          minor_version;  
  4.     u2          major_version;  
  5.     u2          constant_pool_count;  
  6.     cp_info     contant_pool[constant_pool_count – 1];  
  7.     u2          access_flags;  
  8.     u2          this_class;  
  9.     u2          super_class;  
  10.     u2          interfaces_count;  
  11.     u2          interfaces[interfaces_count];  
  12.     u2          fields_count;  
  13.     field_info      fields[fields_count];  
  14.     u2          methods_count;  
  15.     method_info     methods[methods_count];  
  16.     u2          attributes_count;  
  17.     attribute_info  attributes[attributes_count]; 
  18.  
转载: http://blog.csdn.net/shi1122/article/details/8921315

0 0