SlideShare uma empresa Scribd logo
1 de 18
JVM      学习笔记
Jack.Wang(本文未完,待续…..)

摘要:JVM 作为 Java 的核心技术,很多朋友想必也有研究。一直都在关注 JVM 方面的技术,以前看过一些书籍
和网上的资料,自己也发了些 Blog 文章,不过还是没有彻底的了解 JVM 机制,最近有时间研究了研究,特此写
下一篇文章并结合笔者多年实践以揭露 JVM 实现机理,本文后面提供资料下载,读者可进一步研究。

关键字: JVM,Java 技术,虚拟机,Java 架构

目录
1 JVM 架构引言
2 JVM 安全框架
3 JVM 内部机理
  3.1 JVM 的生命周期
  3.2 JVM 的框架
  3.3 数据类型
    3.3.1 Java 数据类型
    3.3.2 浮点运算
  3.4 方法区
  3.5 操作数栈
  3.6 本地方法栈
  3.7 执行引擎
4 类文件结构
5 线程同步
6 垃圾回收机制
7 总结
8 参考资料

文中的图片没有上传,不过想看图片的可以下载原文。
本文下载地址 JVM 学习笔记.doc



     1        JVM 架构引言

     Java 的平台独立,安全和网络可移植性是的 Java 最适合网络计算环境,java 的四大核心技术是,Java 语
     言,字节码格式,Java API,JVM。每次你编写 Java 程序时你就用到了这四项技术,那么你是否对他们有足
     够的了解呢?

       很多人把 JVM 看成是 Java 的解释器,这是错误的理解,并不是所有的 JVM 实现都 Java 的解释器,
     有的也用到了 JIT (Just-In-Time) 技术。Java 是不允许直接操作本地机器指令的,对于 Java 方法来说有
     两种:Java 和 native,对于 native 我更习惯用 C++ 写 DLL,但 Java 并不提倡你这么做,因为有损 Java 平台
     独立性。JVM 中除了执行引擎就是类加载器了,ClassLoader 也分为两种:原始加载器和加载器 Object,原
     始加载器使用和写 JVM 一样的语言写的,比如用 C 写的类加载器,而加载器 Object 就是用 Java 实现的类加
     载器,方便我们扩展,比如你自己可以 New 一个 URLClassLoader 从网络上下载字节码到本地运行。一个类
     的加载和他参考的类的加载应该用同一个 ClassLoader。这一点在发生异常的时候很难找出,比如 OSGI 中
     每个 bundle 都有自己独立的 ClassLoader,对于新手很容易犯错误而无从下手,我们熟悉的 WEB 服务器
Tomcat 的类加载器是分层(有继承关系)的,所以在应用整合的时候也很容易发生 ClassLoader 相关的异
常,而这样的异常往往很难定位。平台互异的字节序问题,在 Java 中,字节码是大字节序的。Java 为支持
开发者开发应用软件提供了大量的 API,可以说,在计算机领域的大部分计算中 Java 都有对应的解决方案。

  C++中可能比较受关注和困扰的就是指针了,而在 Java 中用“参考”这样一个类似的东西代替了,参考不
向指针那样允许参与计算,避免了开发人员直接操作内存,还有个垃圾回收机制也避免了开发者手动释放内
存,还有就是 C++ 中的数组是不进行边界检查的而 Java 中每次使用数组的时候都要进行边界检查,岂不安
全。 可见 Java 相比 C++ 提高了开发效率和安全性。Java 和 C++ 比运行速度是个大问题,因此任何语言都不
万能的,在开发是我们应该适当权衡,Java 运行速度低的原因主要有:

    Ø                   Interpreting bytecodes is 10 to 30 times slower than native execution.

    Ø                   Just-in-time compiling bytecodes can be 7 to 10 times faster than
    interpreting, but still not quite as fast as native execution.

    Ø                   Java programs are dynamically linked.

    Ø                   The Java Virtual Machine may have to wait for class files to download
    across a network.

    Ø                   Array bounds are checked on each array access.

    Ø                   All objects are created on the heap (no objects are created on the
    stack).

    Ø                   All uses of object references are checked at run-time for null.

    Ø                   All reference casts are checked at run-time for type safety.

    Ø                   The garbage collector is likely less efficient (though often more
    effective) at managing the heap than you could be if you managed it directly as in C++.

    Ø                   Primitive types in Java are the same on every platform, rather than
    adjusting to the most efficient size on each platform as in C++.

    Ø                   Strings in Java are always UNICODE. When you really need to manipulate
    just an ASCII string, a Java program will be slightly less efficient than an equivalent C++
    program.

2             JVM 安全框架

Java 允许基于网络的代码运行和传播,为其带了安全问题。但 Java 也提供了内嵌的安全模型,开发者可高
枕无忧。Java 的沙箱安全模型使即时你不信任的代码也可让他在本机执行,如果是恶意的代码他的恶意行为
也会被沙箱拦截,所以在运行任何你有点怀疑的代码前请确保你的沙箱没有缺陷。

对于沙箱的四大基础组件是:类加载器,类文件验证,JVM 安全特性,安全管理的 API,其中最重要的是类加
载器和安全管理 API,因为他们可以客制化。对于加载器,每个 JVM 都可以有多个,同一个类可以加载多次
到不同的 ClassLoader 中,类跨 ClassLoader 是不可见的,而在同一 ClassLoader 中是可直接访问的,这样
可以隔离一些不安全的类。
类型检查是很必要的,分为两个阶段,第一是在类加载进来的时候要进行类的合法性和完整性检查,第二是
运行时确认该类所参考的类,方法和属性是否存在。类文件头都是以一个四个字节的幻数开头
(0xCAFEBABE)来标识是个类文件,当然也有文件大小域,第一阶段确保加载进来的类是正确格式,内部
一直,Java 语法语义限制一直,包括安全的可执行代码,在这个过程中如果有错误,JVM 会抛出异常,该类就
不会被使用。第二阶段其实由于动态连接的原因,需要在运行时检查参考,因为 ClassLoader 在需要某些类
时才去加载,延迟加载,在 ORM 产品中,比如 Hibernate, jdo 等都有所谓的延迟加载

SecurityManager 有一系列的 checkXXX 的方法,用来检测相关操作是否合法,一般我们的程序是不用
SecurityManager 的,除非你安装一个 SecurityManager,如果没有写自己的策略文件,一般是用 jre 下面
的默认策略文件的设置,当然也可在 VM 运行参数设置策略文件的位置。SecurityManager 类的相关方
法。

publicstatic SecurityManager getSecurityManager() {

return security;

    }

publicstatic

void setSecurityManager(final SecurityManager s) {

        try {

            s.checkPackageAccess("java.lang");

        } catch (Exception e) {

            // no-op

    }

    setSecurityManager0(s);

}

privatestaticsynchronized

void setSecurityManager0(final SecurityManager s) {

SecurityManager sm = getSecurityManager();

        if (sm != null) {

        // ask the currently installed security manager if we

        // can replace it.

        sm.checkPermission(new RuntimePermission

                   ("setSecurityManager"));
}

    if ((s != null) && (s.getClass().getClassLoader() != null)) {

            AccessController.doPrivileged(new PrivilegedAction() {

            public Object run() {

              s.getClass().getProtectionDomain().implies

              (SecurityConstants.ALL_PERMISSION);

                returnnull;

        }

         });

    }

security = s;

InetAddressCachePolicy.setIfNotSet(InetAddressCachePolicy.FOREVER);}

设置自己的 SercurityManager:

System.setSecurityManager(new JackSecurityManager());

SecurityManager sm = System.getSecurityManager();

    3               JVM 内部机理

             3.1           JVM 的生命周期

             任何一个类的 main 函数运行都会创建一个 JVM 实例,当 main 函数结束 JVM 实例也就结束了,如果三
             个类分别运行各自的 main 函数则会创建 3 个不同的 JVM 实例。JVM 实例启动时默认启动几个守护线
             程,而 main 方法的执行是在一个单独的非守护线程中执行的。只要母线程结束,子线程就自动销
             毁,只要非守护 main 线程结束 JVM 实例就销毁了。

             3.2           JVM 的框架

             JVM 主要由 ClassLoader 子系统和执行引擎子系统组成,运行数据区分为五个部分,他们是方法区,
             堆,栈,指令寄存器,本地方法栈。方法区和堆是所有线程共享的,一般我们习惯对临时变量放在寄
             存器中,但 JVM 中不用寄存器而是用栈,每个线程都有自己独立的栈空间和指令计数器,其中栈里的
             元素叫帧,帧有三部分组成分别是局部变量,操作数栈和帧数据。正式因为栈是每个线程独有的,所
             以对于 local 变量,是没有多线程冲突问题的。栈和帧的数据结构和大小规范里没有硬性规定,由
             实现者具体实做。

             3.3           数据类型
3.3.1   Java 数据类型



虚拟机的数据类型分为:原始类型和参考类型,有人一直说 Java 只有 90%是 OO 的,其中原始类型就算
的上,不过还好有对应的封装类型如 int->Integer,JDK1.5 可以用这些封装类型直接做算术运算,不
过 Java 内部要做拆箱和装箱的工作。似乎  OO 了些,不过现代的程序员才不过你这些,能用就行。

字长是数据值得基本单元,一般像 int, byte, short 等类型的值用一个字长存储,而浮现类型用两个
字长存储,字长的大小一般和机器的指针大小相同,最小是 32 位。

像 byte, short, or char 这样的类型在方法区和堆中 Java 是原来类型,但在程序运行时放在栈帧
中,统一用整型(int)来表示

对于类型之间的转换 JVM 有对应的指令如:

上图是小范围向大范围转变,叫做向上转型,当然也有向下转型的指令,不过可能会造成数据被破坏,
如图:

不管你怎么变化,栈帧里都是以字为单位的,32 位机子一个字是 4 个字节,正好是一个整型大小,如前
所述原始类型在栈中都被表示为 int 型,当然如果是 long 和 double 那就用两个字来表示咯。

      3.3.2   浮点运算

      浮点运算算法因该是倍受关注的了,Java 中浮点数的表示遵照 IEEE 754 1985 的规范,这个规范
      定义了 32 位和 64 位浮点格式和运算,我都知道 Java 中的 float 是 32 位而,double 是 64 位。对
      于浮点数大家都很清楚他分为四个部分:符号,尾数,基数和幂,要得到浮点数值必须将这四个
      部分相乘才能得到值。所有的浮点数都要经过标准化,我们都知道计算机里只有 0,1 这样的表
      示,尾数和幂都要二进制表示,对于尾数 float 用它的 23 位表示,double 用它的 52 位来表示,
      对于幂数 float 用它的 8 位表示,double 用它的 11 为表示,剩下以为表示符号。

        JVM 也有对应的浮点运算指令如图(加法指令)

3.4           方法区

ClassLoader 负责把 class 加载进来并解析类信息放在方法区,方法区是被所有线程共享的,所以方法
区必须保证线程的安全。比如两个线程同时需要一个类,必须保证只有一个线程去加载而另一个线程
等待。

方法区的大小不是固定的,一般会在运行时 JVM 会根据运行情况作出调整,但 JVM 实现者都留有接口可
供调节,比如最大和最小值。

我们都知道堆是被垃圾回收器回收的,在方法区的类也是会被回收的,类也有他的生命周期,当某个
类不再被参考之后也就没有这个类的引用和对象了那么他的存在就没有意义了,只是站着内存,回收
器就会按照一定的算法将它卸载了。对于一个类信息的表述都是用全名的,在方法区中也有其他信
息,比如常量池,属性和方法信息,还有就是指向堆中的 ClassLoader 和 Class 的参考。我大家都熟
悉的就是类的静态变量,也放在方法区的由所有的对象实例共享。
可以想象在方法区内放置了大量的二进制的 Class 信息,为了加快访问速度,JVM 实现者都会维护一个
方法表,记得读研一的时候中间件老师讲过这些东西是结合指针讲的 C++ 的内存模型。另外注意的就
是方法表只针对可实例化的类,对抽象类和接口没有意义。

每个 JVM 实例都只有一个堆,所有的线程共享其中的对象,这才出现了多线程安全问题。JVM 有 new 对
象的指令但没有释放对象的指令,当让这些指令都是虚拟指令,这些对象的释放是有 GC 来做的,GC
在 JVM 规范中并没有硬性的规定,有实现者设计他的实现形式和算法。

想必很多同人都想知道,对象是怎么样在堆里表示的,其实很简单。其实面 JVM 规范也没有细致的规定
对象怎么在堆里表示的,

如图是一个参考的堆模型,具体实现可能不是这样的,这个是 HeapOfFish applet 的一个演示模型,
具体内容可以看看 JVM 规范。当然也有很多其他的模型,这个模型的好处就是在堆压缩的时候很方便,
而在 reference 直接 point 到一个对象的模型来说在堆压缩方面是很麻烦的,因为你要考虑到方法
区,堆,栈里可能的参考,你都要修改。对象还有一个很重要的数据结构就是方法表,方法表可以加
快访问速度,但并不是说所有的 JVM 实现都有。

堆中的每个对象都有指向方法区的指针,而自己主要保留对象属性信息,如图:

看一个方法区链接的例子,看看一个类是怎么加载进来,怎么链接初始化的:

有一 Salutation 类

 class Salutation {

     private static final String hello = "Hello, world!";

     private static final String greeting = "Greetings, planet!";

     private static final String salutation = "Salutations, orb!";

     private static int choice = (int) (Math.random() * 2.99);

     public static void main(String[] args) {

         String s = hello;

         if (choice == 1) {

             s = greeting;

         }

         else if (choice == 2) {

             s = salutation;

         }

         System.out.println(s);
}

   }

   Assume that you have asked a Java Virtual Machine to run Salutation. When the virtual
   machine starts, it attempts to invoke the main() method of Salutation. It quickly
   realizes, however, that it canít invoke main(). The invocation of a method declared in a
   class is an active use of that class, which is not allowed until the class is
   initialized. Thus, before the virtual machine can invoke main(), it must initialize
   Salutation. And before it can initialize Salutation, it must load and link Salutation.
   So, the virtual machine hands the fully qualified name of Salutation to the primordial
   class loader, which retrieves the binary form of the class, parses the binary data into
   internal data structures, and creates an instance of java.lang.Class.

   常量池里的内容:

Index           Type                            Type Value
   1 CONSTANT_String_info                       CONSTANT_String
                                                _info 30
   2   CONSTANT_String_info                     CONSTANT_String
                                                _info 31
   3   CONSTANT_String_info                     CONSTANT_String
                                                _info 39
   4   CONSTANT_Class_info                      CONSTANT_Class_
                                                info 37
   5   CONSTANT_Class_info                      CONSTANT_Class_
                                                info 44
   6   CONSTANT_Class_info                      CONSTANT_Class_
                                                info 45
   7   CONSTANT_Class_info                      CONSTANT_Class_
                                                info 46
   8   CONSTANT_Class_info                      CONSTANT_Class_
                                                info 47
   9   CONSTANT_Methodref_info                  CONSTANT_Method
                                                ref_info 7, 16
   10 CONSTANT_Fieldref_info                    CONSTANT_Fieldr
                                                ef_info 4, 17
   11 CONSTANT_Fieldref_info                    CONSTANT_Fieldr
                                                ef_info 8, 18
   12 CONSTANT_Methodref_info                   CONSTANT_Method
                                                ref_info 5, 19
   13 CONSTANT_Methodref_info                   CONSTANT_Method
                                                ref_info 6, 20
   14 CONSTANT_Double_info                      CONSTANT_Double
                                                _info 2.99
   16 CONSTANT_NameAndType_info                 CONSTANT_NameAn
                                                dType_info 26,
                                                22
   17 CONSTANT_NameAndType_info                 CONSTANT_NameAn
                                                dType_info 41,
                                                32
18 CONSTANT_NameAndType_info                   CONSTANT_NameAn
                                               dType_info 49,
                                               34
19 CONSTANT_NameAndType_info                   CONSTANT_NameAn
                                               dType_info 50,
                                               23
20 CONSTANT_NameAndType_info                   CONSTANT_NameAn
                                               dType_info 51,
                                               21
21 CONSTANT_Utf8_info                          CONSTANT_Utf8_i
                                               nfo "()D"
22 CONSTANT_Utf8_info                          CONSTANT_Utf8_i
                                               nfo "()V"
23 CONSTANT_Utf8_info               CONSTANT_Utf8_info (Ljava/
                                    lang/String;)V"
24 CONSTANT_Utf8_info               CONSTANT_Utf8_info
                                    ([Ljava/lang/String;)V"
25 CONSTANT_Utf8_info               CONSTANT_Utf8_info
                                    <clinit"
26 CONSTANT_Utf8_info               CONSTANT_Utf8_info <init"
27 CONSTANT_Utf8_info               CONSTANT_Utf8_info "Code"
28 CONSTANT_Utf8_info               CONSTANT_Utf8_info
                                    "ConstantValue"
29 CONSTANT_Utf8_info               CONSTANT_Utf8_info
                                    "Exceptions"
30 CONSTANT_Utf8_info               CONSTANT_Utf8_info
                                    "Greetings,planet!"
31 CONSTANT_Utf8_info               CONSTANT_Utf8_info "Hello,
                                    world!"
32 CONSTANT_Utf8_info               CONSTANT_Utf8_info "I"
33 CONSTANT_Utf8_info               CONSTANT_Utf8_info
                                    "LineNumberTable"
34                34                "Ljava/io/PrintStream;"
                  CONSTANT_Utf8_info
35   CONSTANT_Utf8_info              CONSTANT_Utf8_info "Ljava/
                                     lang/String;"
36   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "LocalVariables"
37   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "Salutation"
38   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "Salutation.java"
39   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "Salutations, orb!"
40   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "SourceFile"
41   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "choice"
42   CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                     "greeting"
43   CONSTANT_Utf8_info              CONSTANT_Utf8_info "hello"
44   CONSTANT_Utf8_info              CONSTANT_Utf8_info
"java/io/PrintStream"
45 CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                   "java/lang/Math"
46 CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                   "java/lang/Object"
47 CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                   "java/lang/System"
48 CONSTANT_Utf8_info              CONSTANT_Utf8_info "main"
49 CONSTANT_Utf8_info              CONSTANT_Utf8_info "out"
50 CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                   "println"
51 CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                   "random"
52 CONSTANT_Utf8_info              CONSTANT_Utf8_info
                                   "salutation"
As part of the loading process for Salutation, the Java Virtual Machine must make sure
all of Salutationís superclasses have been loaded. To start this process, the virtual
machine looks into Salutationís type data at the super_class item, which is a seven. The
virtual machine looks up entry seven in the constant pool, and finds a
CONSTANT_Class_info entry that serves as a symbolic reference to class java.lang.Object.
See Figure 8-5 for a graphical depiction of this symbolic reference. The virtual machine
resolves this symbolic reference, which causes it to load class Object. Because Object
is the top of Salutationís inheritance hierarchy, the virtual machine and links and
initializes Object as well.

3.5           操作数栈

操作数栈是 Java 运行时的核心栈,看看 i+j 的一个简单运算,

iload_0

iload_1

iadd

istore_2

以上是四个 JVM 指令,完成 i+j 并把结果保存到 k 中,如图示:

在堆中不可能分配一个原始类型的空间放值,而是先用对象封装才能存在堆空间中,带 Java 栈中也不
可能放对象,而只有原始类型和参考类型。上次有人争议数组放在何处?在 Java 中数组和对象是同等
地位的,都放在堆中而他的参考是放在栈里,JVM 有对应的指令比如 newarray, anewarray 等。

3.6           本地方法栈

想必很多人用过 JNI 结束,Java 是不提倡这么做的,而且在这放的设计和实现上,个人觉得不是那
么好,至少他比不那么方便,所以很少见应用开发者去写些 Native 方法,每次你去看 Java 原代码是
你经常看到 native 方法,也看到 JDK 下的 DLL 文件,大部分 JVM 都是用 C 或 C++写的。前面也提
过,这样就破坏了 Java 的平台独立性,在本地方法运行的时候也有专门的栈去处理。Java 在执行本
地方法的时候暂时放弃 Java stack 的操作,转向本地方法,本地方法有自己的栈或堆的处理方
式,Java 在执行本地方法时会在本地栈和 Java 栈之间切换,如图:
a thread first invoked two Java methods, the second of which invoked a native method.
            This act caused the virtual machine to use a native method stack. In this figure, the
            native method stack is shown as a finite amount of contiguous memory space. Assume it is
            a C stack. The stack area used by each C-linkage function is shown in gray and bounded
            by a dashed line. The first C-linkage function, which was invoked as a native method,
            invoked another C-linkage function. The second C-linkage function invoked a Java method
            through the native method interface. This Java method invoked another Java method, which
            is the current method shown in the figure,做过 JNI 开发的朋友应该了解 Java 和 C,C++ 是如
            何交互的,这些都是执行引擎的事。

            3.7            执行引擎

            执行引擎,应该是 JVM 的核心了,一般我把它看作指令集合。JVM 规范详细的描述了每个指令的作用,
            但没有进一步描述如何实现,还由实现厂商自己设计,可以用解释的方式,JIT 方式,编译本地代码的
            方式,或者几者混合的方式,当然也可用一些新的不为我们知道的技术。

            每一个线程都有一个自己的执行引擎,据我了解 JVM 指令用一个字节表示,也就是 JVM 最多有 256 个指
            令,目前 JVM 已有 160(目前可能多于这些)个指令。就有这百十个指令组成了我的系统,JVM 指令一
            般只有操作码没有操作数,一般操作数放在常量池和 Java 栈中,设计指令集的最重要的目标应该是平
            台独立性,同时在验证 bytecode 也比较方便。有些指令的操作都是基于具体类型的,有的就没有比如
            goto,如图

            指令前缀表示操作类型, 可见 Java 编译器和 JVM 对类型操作要求很严格,为何使指令尽量用一个字节
            表示,很多类型如 byte, char 都没有直接运算,而是在运算前把他们转换成整型。

            执行过程的在某一时刻内容如图:

            在帧里的局部变量有四个分别都是 reference,都指向不同的对象,有的时候我们编程当操作完成,
            最好把 o, greeter ,c, gcl 这四个参考付为 null,这样他们指向的对象不可达,对象不可达,他
            们在方法区的类信息也不可达,类信息不可达,堆中的 Class 对象不可达,你可以看到图中的所有对
            象,类信息都是可回收状态,这样 GC 某个时刻就可以释放了这些内存了。

            写方法时我们希望 try cache 起来,当然也有需要在后面 加上 finally,我们都知道在 finally 里
            的代码被执行玩前所有的 return 操作都会压入栈里,等 finally 块执行完了再弹出返回,如:

            static boolean test(boolean bVal) {

            while (bVal) {

            try {

            return true;

            }

finally {

            break;

            }
}

    return false;

    }

    可以看出这个方法的执行以返回 false 告终。

4         类文件结构

读者可以用 UltrEdit 之类的编辑工具打开一个 class 文件查看,每个类文件都包含如下内容如图:其中
u2 表示两个字节大小,u4 表示四个字节大小。

如第一行所示,每个类文件都以幻数开头固定为(0xCAFEBABE),用 UltraEdit 等编辑工具可以直接看到,当
然你也可以看到很多如图所示的属性和方法的描述:

常量池是以类型来划分不同的表格的,如 CONSTANT_Double_info 表就只是 double 常量
值,CONSTANT_Class_info 表存储参考信息,比如类,接口,属性和方法的符号参考,类文件的核心数据结构
就是常量池了,而所有的类,属性和方法的描述以及值都是通过 index 到常量池获得的,所以常量池是个很
重要的概念,有兴趣的朋友可进一步参考 JVM 规范。

5         线程同步

        对多线程的支持是 java 语言的一大亮点,java 实现线程同步的方式是 monitor,具体有两种方
式:互斥和协同,每个对象都有一把锁,通过 lock 和 unlock 实现互斥的操作,Object 类有 wait notify
等这样的方法可以实现线程为实现一个共同的目标而协同工作。monitor 就像一个建筑有一个特殊的房间每次
只允许一个线程访问。都知道线程同步有一个重要的概念就是临界区,任何一个线程要想进入临界区就必须
获得 monitor,如果线程发现 monitor 被其他线程获得,此线程必须排队,等到他前面的线程也退出了
monitor 的占有,则此线程才能操作 monitor 的临界区,当然也有优先级 的问题。此图说明了多个线程协
作工作的过程。

    前面介绍过,堆和方法区是被线程共享的,这两大块要做同步,而对于局部变量是由线程独有的所
以不存在同步问题,Java API 中有一个很重要也很简单的类就是 ThreadLocal,他是用 Theard key 来标识
变量,是非局部变量局部化的一个很好的例子。

    对象锁的概念大家都很熟悉了,Java 中通过这个锁有两种方式,一种是同步块,一种是同步方法,
具体的内容请看看 java 多线程编程的书。

6         垃圾回收机制

          堆里聚集了所有由应用程序创建的对象,JVM 也有对应的指令比如 new, newarray, anewarray
和 multianewarray,然并没有向 C++ 的 delete,free 等释放空间的指令,Java 的所有释放都由 GC 来
做,GC 除了做回收内存之外,另外一个重要的工作就是内存的压缩,这个在其他的语言中也有类似的实
现,相比 C++ 不仅好用,而且增加了安全性,当然她也有弊端,比如性能这个大问题。

垃圾回收的算法有很多,不过怎么样,任何一个算法都要做两件事情,一是识别垃圾对象,二是清理内存以
备运行程序使用。区分活着的对象和垃圾对象的主要方法就是参考计数和追踪,参考计数就是每个对象都维
护一个引用 count,而每次追踪都去标记对象,当一次跟踪完成没有被标记的对象就是不可到达的那就是垃
圾了。参考计数的方式现在已经很少 JVM 实现用了,因为对于 child, parent 的内部参考,count 永远不
会是 0,也就是不会是垃圾对象,而且每次修改引用都要加减 count。而跟踪是从根节点开始扫描所有对
象,并作标记。知道那些对象是垃圾了之后,就要释放了,有两种方式一个是压缩,一个是拷贝,不管哪种
     方式改变了对象的物理位置,就要修改他的参考值,每个对像都有一个 finalize 方法,一般我们不去
     override 他,他是在回收时期执行,但并不是所有的对象的 finalize 方法都会被执行,具体内容请看
     《inside in java machine》.

    7            总结

     本文结合作者的一些观点和几年的开发实践,谈谈对 JVM 规范的看法,由于完稿仓促,也没多少时间整理,
     如果有什么问题可以进一步讨论,本人编译器这方面也是比较薄弱,真正按照 JVM 规范实现虚拟机还有很长
     的路要走,可能在将来的某天有机会进一步研究一下。多谢各位的指导和帮助,我会陆续在 Blog 里发些文
     章。

8           参考资料

    1)inside in java machine (想要的请点击下载)

    2)http://www.blogjava.net/Jack2007/archive/2008/05/23/202485.html《Java 虚拟机深入研究》

    3)http://www.blogjava.net/Jack2007/archive/2008/05/21/202018.html

    4)http://www.tudou.com/programs/view/hN_4sQJMoFQ/

    5)http://www.blogjava.net/Jack2007/archive/2008/04/11/192288.html

6)http://developer.51cto.com/art/200805/73338.htm




Java 虚拟机深入研究

文章来自:http://www.qqread.com/java/w872354600.html

Java 技术与 Java 虚拟机

说起 Java,人们首先想到的是 Java 编程语言,然而事实上,Java 是一种技术,它由四方面组成:Java 编程语
言、Java 类文件格式、Java 虚拟机和 Java 应用程序接口(Java API)。它们的关系如下图所示:
图 1 Java 四个方面的关系

  运行期环境代表着 Java 平台,开发人员编写 Java 代码(.java 文件),然后将之编译成字节码(.class 文件)。
最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的
转换成机器码执行。从上图也可以看出 Java 平台由 Java 虚拟机和 Java 应用程序接口搭建,Java 语言则是进入
这个平台的通道,用 Java 语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示:




                           (点击查看原图)

  在 Java 平台的结构中, 可以看出,Java 虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关
的关键。它的下方是移植接口,移植接口由两部分组成:适配器和 Java 操作系统, 其中依赖于平台的部分称为适
配器;JVM 通过移植接口在具体的平台和操作系统上实现;在 JVM 的上方是 Java 的基本类库和扩展类库以及它
们的 API, 利用 Java API 编写的应用程序(application) 和小程序(Java applet) 可以在任何 Java 平台上运行
而无需考虑底层平台, 就是因为有 Java 虚拟机(JVM)实现了程序与操作系统的分离,从而实现了 Java 的平台无
关性。

那么到底什么是 Java 虚拟机(JVM)呢?通常我们谈论 JVM 时,我们的意思可能是:

对 JVM 规范的的比较抽象的说明;
对 JVM 的具体实现;
在程序运行期间所生成的一个 JVM 实例。

对 JVM 规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》
(《Java 虚拟机规范》)中被详细地描述了;对 JVM 的具体实现要么是软件,要么是软件和硬件的组合,它已经
被许多生产厂商所实现,并存在于多种平台之上;运行 Java 程序的任务由 JVM 的运行期实例单个承担。在本文
中我们所讨论的 Java 虚拟机(JVM)主要针对第三种情况而言。它可以被看成一个想象中的机器,在实际的计算机
上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自己相应的指令系统。

JVM 在它的生存周期中有一个明确的任务,那就是运行 Java 程序,因此当 Java 程序启动的时候,就产生 JVM 的
一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从 JVM 的体系结构和它的运行过程这两个方
面来对它进行比较深入的研究。

Java 虚拟机的体系结构

刚才已经提到,JVM 可以由不同的厂商来实现。由于厂商的不同必然导致 JVM 在实现上的一些不同,然而 JVM
还是可以实现跨平台的特性,这就要归功于设计 JVM 时的体系结构了。

我们知道,一个 JVM 实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部
分,它们描述了 JVM 的一个抽象的内部体系结构,其目的不光规定实现 JVM 时它内部的体系结构,更重要的是提
供了一种方式,用于严格定义实现时的外部行为。每个 JVM 都有两种机制,一个是装载具有合适名称的类(类或是
接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个 JVM 又
包括方法区、堆、Java 栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起
组成的体系结构图为:




                       图 3 JVM 的体系结构

  JVM 的每个实例都有一个它自己的方法域和一个堆,运行于 JVM 内的所有的线程都共享这些区域;当虚拟机
装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时
候,JVM 把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和 Java 栈,其
中程序计数器中的值指向下一条即将被执行的指令,线程的 Java 栈则存储为该线程调用 Java 方法的状态;本地
方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。

下面分别对这几个部分进行说明。

执行引擎处于 JVM 的核心位置,在 Java 虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规
范很详细地说明了当 JVM 执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java 虚拟机
支持大约 248 个字节码。每个字节码执行一种基本的 CPU 运算,例如,把一个整数加到寄存器,子程序转移
等。Java 指令集相当于 Java 程序的汇编语言。

Java 指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有 0 个或多个操作数,提供操作所需的参
数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。

虚拟机的内层循环的执行过程如下:

do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)

由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小
是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个 16 位的参数存放时
占用两个字节,其值为:

第一个字节*256+第二个字节字节码。

指令流一般只是字节对齐的。指令 tableswitch 和 lookup 是例外,在这两条指令内部要求强制的 4 字节边界对
齐。

对于本地方法接口,实现 JVM 并不要求一定要有它的支持,甚至可以完全没有。Sun 公司实现 Java 本地接口
(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替 Sun 公司的 JNI。但是这些设计与实
现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。

Java 的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式
释放对象的能力。Java 不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。

Java 方法区与传统语言中的编译后代码或是 Unix 进程中的正文段类似。它保存方法代码(编译后的 java 代码)和
符号表。在当前的 Java 实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了
一个 Java 类或一个 Java 界面的编译后的代码。可以说类文件是 Java 语言的执行代码文件。为了保证类文件的
平台无关性,Java 虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考 Sun 公司的 Java 虚拟机规
范。

Java 虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java 虚拟机的寄存器有四
种:

pc: Java 程序计数器;
optop: 指向操作数栈顶端的指针;
frame: 指向当前执行方法的执行环境的指针;。
vars: 指向当前执行方法的局部变量区第一个变量的指针。

在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。
当线程执行 Java 方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那
么程序计数器的值就不会被定义。

Java 虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。
局部变量区

每个 Java 方法使用一个固定大小的局部变量集。它们按照与 vars 寄存器的字偏移量来寻址。局部变量都是 32 位
的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索
引 n 的局部变量,如果是一个双精度浮点数,那么它实际占据了索引 n 和 n+1 所代表的存储空间)虚拟机规范并不要
求在局部变量中的 64 位的值是 64 位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把
操作数栈中的值写入局部变量的指令。

运行环境区

在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。

动态链接

运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的 class 文件
代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载
必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接
方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。

正常的方法返回

如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在
正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法
调用指令,然后在调用者的执行环境中继续执行下去。

异常捕捉

异常情况在 Java 中被称作 Error(错误)或 Exception(异常),是 Throwable 类的子类,在程序中的原因是:① 动态
链接错,如无法找到所需的 class 文件。②运行时错,如对一个空指针的引用。程序使用了 throw 语句。

当异常发生时,Java 虚拟机采取如下措施:

检查与当前方法相联系的 catch 子句表。每个 catch 子句包含其有效指令范围,能够处理的异常类型,以及处理异
常的代码块地址。

与异常相匹配的 catch 子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理
的异常类型的子类型。如果找到了匹配的 catch 子句,那么系统转移到指定的异常处理块处执行;如果没有找到异
常处理块,重复寻找匹配的 catch 子句的过程,直到当前方法的所有嵌套的 catch 子句都被检查过。

由于虚拟机从第一个匹配的 catch 子句处继续执行,所以 catch 子句表中的顺序是很重要的。因为 Java 代码是结
构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以
用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。

如果找不到匹配的 catch 子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚
刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错
误被传播到最顶层,那么系统将调用一个缺省的异常处理块。

操作数栈区

机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存
器或非通用寄存器的机器(如 Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是 32 位的。它用于给方法
传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd 指令将两个整数相加。相加的
两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并
把结果压回到操作数栈中。
每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了 long 和
double 型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个 int 类型的数,如果把
它们当作是一个 long 类型的数则是非法的。在 Sun 的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有
少数操作(操作符 dupe 和 swap),用于对运行时数据区进行操作时是不考虑类型的。

本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问
虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个 C 语言的栈,那么当 C
程序调用 C 函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现 Java 虚拟机时,本地方法
接口使用的是 C 语言的模型栈,那么它的本地方法栈的调度与使用则完全与 C 语言的栈相同。


Java 虚拟机的运行过程

上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。

虚拟机通过调用某个指定类的方法 main 启动,传递给 main 一个字符串数组参数,使指定的类被装载,同时链接
该类所使用的其它的类型,并且初始化它们。例如对于程序:


class HelloApp
{
public static void main(String[] args)
{
System.out.println("Hello World!");
for (int i = 0; i < args.length; i++ )
{
System.out.println(args[i]);
}
}
}

  编译后在命令行模式下键入: java HelloApp run virtual machine

将通过调用 HelloApp 的方法 main 来启动 java 虚拟机,传递给 main 一个包含三个字符
串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行 HelloApp 时可能采取的步骤。

开始试图执行类 HelloApp 的 main 方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代
表,于是虚拟机使用 ClassLoader 试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载
后同时在 main 方法被调用之前,必须对类 HelloApp 与其它类型进行链接然后初始化。链接包含三个阶段:检
验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标
准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的
静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如
下:
(点击查看原图)
                  图 4:虚拟机的运行过程
  结束语

本文通过对 JVM 的体系结构的深入研究以及一个 Java 程序执行时虚拟机的运行过程的详细分析,意在剖析清楚
Java 虚拟机的机理。

Mais conteúdo relacionado

Destaque

Java的垃圾回收之算法
Java的垃圾回收之算法Java的垃圾回收之算法
Java的垃圾回收之算法wensheng wei
 
解读server.xml文件
解读server.xml文件解读server.xml文件
解读server.xml文件wensheng wei
 
超级入门:JAVA从零开始到HelloWorld
超级入门:JAVA从零开始到HelloWorld超级入门:JAVA从零开始到HelloWorld
超级入门:JAVA从零开始到HelloWorldwensheng wei
 
数据库系统安全防入侵技术综述
数据库系统安全防入侵技术综述数据库系统安全防入侵技术综述
数据库系统安全防入侵技术综述wensheng wei
 
20 种提升网页速度的技巧
20 种提升网页速度的技巧20 种提升网页速度的技巧
20 种提升网页速度的技巧wensheng wei
 
保护数据库服务器加强数据库安全
保护数据库服务器加强数据库安全保护数据库服务器加强数据库安全
保护数据库服务器加强数据库安全wensheng wei
 
JavaScript高级程序设计(中文优化版)
JavaScript高级程序设计(中文优化版)JavaScript高级程序设计(中文优化版)
JavaScript高级程序设计(中文优化版)wensheng wei
 
LINUX Admin Quick Reference
LINUX Admin Quick ReferenceLINUX Admin Quick Reference
LINUX Admin Quick Referencewensheng wei
 
LINUX System Call Quick Reference
LINUX System Call Quick ReferenceLINUX System Call Quick Reference
LINUX System Call Quick Referencewensheng wei
 

Destaque (10)

Java的垃圾回收之算法
Java的垃圾回收之算法Java的垃圾回收之算法
Java的垃圾回收之算法
 
解读server.xml文件
解读server.xml文件解读server.xml文件
解读server.xml文件
 
超级入门:JAVA从零开始到HelloWorld
超级入门:JAVA从零开始到HelloWorld超级入门:JAVA从零开始到HelloWorld
超级入门:JAVA从零开始到HelloWorld
 
数据库系统安全防入侵技术综述
数据库系统安全防入侵技术综述数据库系统安全防入侵技术综述
数据库系统安全防入侵技术综述
 
20 种提升网页速度的技巧
20 种提升网页速度的技巧20 种提升网页速度的技巧
20 种提升网页速度的技巧
 
保护数据库服务器加强数据库安全
保护数据库服务器加强数据库安全保护数据库服务器加强数据库安全
保护数据库服务器加强数据库安全
 
创新项目
创新项目创新项目
创新项目
 
JavaScript高级程序设计(中文优化版)
JavaScript高级程序设计(中文优化版)JavaScript高级程序设计(中文优化版)
JavaScript高级程序设计(中文优化版)
 
LINUX Admin Quick Reference
LINUX Admin Quick ReferenceLINUX Admin Quick Reference
LINUX Admin Quick Reference
 
LINUX System Call Quick Reference
LINUX System Call Quick ReferenceLINUX System Call Quick Reference
LINUX System Call Quick Reference
 

Semelhante a JVM 学习笔记

Java JNI 编程进阶
Java JNI 编程进阶     Java JNI 编程进阶
Java JNI 编程进阶 wensheng wei
 
Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...
Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...
Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...Codemotion
 
UglifyJS 使用文档
UglifyJS 使用文档UglifyJS 使用文档
UglifyJS 使用文档明 李
 
Spring Framework 3: Um 'brainstorm' de novas funcionalidades
Spring Framework 3: Um 'brainstorm' de novas funcionalidadesSpring Framework 3: Um 'brainstorm' de novas funcionalidades
Spring Framework 3: Um 'brainstorm' de novas funcionalidadesDr. Spock
 
HTML5 WebSocket 발표 자료
HTML5 WebSocket 발표 자료HTML5 WebSocket 발표 자료
HTML5 WebSocket 발표 자료Zany Lee
 

Semelhante a JVM 学习笔记 (7)

Jetty Introduction
Jetty IntroductionJetty Introduction
Jetty Introduction
 
Java JNI 编程进阶
Java JNI 编程进阶     Java JNI 编程进阶
Java JNI 编程进阶
 
Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...
Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...
Monitorare le applicazioni: costruire un agent plugin per l’introspezione del...
 
UglifyJS 使用文档
UglifyJS 使用文档UglifyJS 使用文档
UglifyJS 使用文档
 
Spring Framework 3: Um 'brainstorm' de novas funcionalidades
Spring Framework 3: Um 'brainstorm' de novas funcionalidadesSpring Framework 3: Um 'brainstorm' de novas funcionalidades
Spring Framework 3: Um 'brainstorm' de novas funcionalidades
 
HTML5 WebSocket 발표 자료
HTML5 WebSocket 발표 자료HTML5 WebSocket 발표 자료
HTML5 WebSocket 발표 자료
 
FISL11 2010 - Automação de Datacenters
FISL11 2010 - Automação de DatacentersFISL11 2010 - Automação de Datacenters
FISL11 2010 - Automação de Datacenters
 

Mais de wensheng wei

你会柔软地想起这个校园
你会柔软地想起这个校园你会柔软地想起这个校园
你会柔软地想起这个校园wensheng wei
 
几米语录(1)
几米语录(1)几米语录(1)
几米语录(1)wensheng wei
 
Installation of Subversion on Ubuntu,...
Installation of Subversion on Ubuntu,...Installation of Subversion on Ubuntu,...
Installation of Subversion on Ubuntu,...wensheng wei
 
高级PHP应用程序漏洞审核技术
高级PHP应用程序漏洞审核技术高级PHP应用程序漏洞审核技术
高级PHP应用程序漏洞审核技术wensheng wei
 
存储过程编写经验和优化措施
存储过程编写经验和优化措施存储过程编写经验和优化措施
存储过程编写经验和优化措施wensheng wei
 
CentOS5 apache2 mysql5 php5 Zend
CentOS5 apache2 mysql5 php5 ZendCentOS5 apache2 mysql5 php5 Zend
CentOS5 apache2 mysql5 php5 Zendwensheng wei
 
Happiness is a Journey
Happiness is a JourneyHappiness is a Journey
Happiness is a Journeywensheng wei
 
Linux Shortcuts and Commands:
Linux Shortcuts and Commands:Linux Shortcuts and Commands:
Linux Shortcuts and Commands:wensheng wei
 
Java正则表达式详解
Java正则表达式详解Java正则表达式详解
Java正则表达式详解wensheng wei
 
Linux Security Quick Reference Guide
Linux Security Quick Reference GuideLinux Security Quick Reference Guide
Linux Security Quick Reference Guidewensheng wei
 
Android模拟器SD Card映像文件使用方法
Android模拟器SD Card映像文件使用方法Android模拟器SD Card映像文件使用方法
Android模拟器SD Card映像文件使用方法wensheng wei
 
如何硬盘安装ubuntu8.10
如何硬盘安装ubuntu8.10如何硬盘安装ubuntu8.10
如何硬盘安装ubuntu8.10wensheng wei
 
数据库设计方法、规范与技巧
数据库设计方法、规范与技巧数据库设计方法、规范与技巧
数据库设计方法、规范与技巧wensheng wei
 
揭秘全球最大网站Facebook背后的那些软件
揭秘全球最大网站Facebook背后的那些软件揭秘全球最大网站Facebook背后的那些软件
揭秘全球最大网站Facebook背后的那些软件wensheng wei
 
入门-Java运行环境变量的图文教程
入门-Java运行环境变量的图文教程入门-Java运行环境变量的图文教程
入门-Java运行环境变量的图文教程wensheng wei
 
上海实习有感
上海实习有感上海实习有感
上海实习有感wensheng wei
 

Mais de wensheng wei (20)

你会柔软地想起这个校园
你会柔软地想起这个校园你会柔软地想起这个校园
你会柔软地想起这个校园
 
几米语录(1)
几米语录(1)几米语录(1)
几米语录(1)
 
我的简历
我的简历我的简历
我的简历
 
Installation of Subversion on Ubuntu,...
Installation of Subversion on Ubuntu,...Installation of Subversion on Ubuntu,...
Installation of Subversion on Ubuntu,...
 
高级PHP应用程序漏洞审核技术
高级PHP应用程序漏洞审核技术高级PHP应用程序漏洞审核技术
高级PHP应用程序漏洞审核技术
 
存储过程编写经验和优化措施
存储过程编写经验和优化措施存储过程编写经验和优化措施
存储过程编写经验和优化措施
 
CentOS5 apache2 mysql5 php5 Zend
CentOS5 apache2 mysql5 php5 ZendCentOS5 apache2 mysql5 php5 Zend
CentOS5 apache2 mysql5 php5 Zend
 
Happiness is a Journey
Happiness is a JourneyHappiness is a Journey
Happiness is a Journey
 
Linux Shortcuts and Commands:
Linux Shortcuts and Commands:Linux Shortcuts and Commands:
Linux Shortcuts and Commands:
 
Java正则表达式详解
Java正则表达式详解Java正则表达式详解
Java正则表达式详解
 
Linux Security Quick Reference Guide
Linux Security Quick Reference GuideLinux Security Quick Reference Guide
Linux Security Quick Reference Guide
 
issue35 zh-CN
issue35 zh-CNissue35 zh-CN
issue35 zh-CN
 
Android模拟器SD Card映像文件使用方法
Android模拟器SD Card映像文件使用方法Android模拟器SD Card映像文件使用方法
Android模拟器SD Card映像文件使用方法
 
如何硬盘安装ubuntu8.10
如何硬盘安装ubuntu8.10如何硬盘安装ubuntu8.10
如何硬盘安装ubuntu8.10
 
ubunturef
ubunturefubunturef
ubunturef
 
数据库设计方法、规范与技巧
数据库设计方法、规范与技巧数据库设计方法、规范与技巧
数据库设计方法、规范与技巧
 
揭秘全球最大网站Facebook背后的那些软件
揭秘全球最大网站Facebook背后的那些软件揭秘全球最大网站Facebook背后的那些软件
揭秘全球最大网站Facebook背后的那些软件
 
入门-Java运行环境变量的图文教程
入门-Java运行环境变量的图文教程入门-Java运行环境变量的图文教程
入门-Java运行环境变量的图文教程
 
Java学习路径
Java学习路径Java学习路径
Java学习路径
 
上海实习有感
上海实习有感上海实习有感
上海实习有感
 

JVM 学习笔记

  • 1. JVM 学习笔记 Jack.Wang(本文未完,待续…..) 摘要:JVM 作为 Java 的核心技术,很多朋友想必也有研究。一直都在关注 JVM 方面的技术,以前看过一些书籍 和网上的资料,自己也发了些 Blog 文章,不过还是没有彻底的了解 JVM 机制,最近有时间研究了研究,特此写 下一篇文章并结合笔者多年实践以揭露 JVM 实现机理,本文后面提供资料下载,读者可进一步研究。 关键字: JVM,Java 技术,虚拟机,Java 架构 目录 1 JVM 架构引言 2 JVM 安全框架 3 JVM 内部机理 3.1 JVM 的生命周期 3.2 JVM 的框架 3.3 数据类型 3.3.1 Java 数据类型 3.3.2 浮点运算 3.4 方法区 3.5 操作数栈 3.6 本地方法栈 3.7 执行引擎 4 类文件结构 5 线程同步 6 垃圾回收机制 7 总结 8 参考资料 文中的图片没有上传,不过想看图片的可以下载原文。 本文下载地址 JVM 学习笔记.doc 1 JVM 架构引言 Java 的平台独立,安全和网络可移植性是的 Java 最适合网络计算环境,java 的四大核心技术是,Java 语 言,字节码格式,Java API,JVM。每次你编写 Java 程序时你就用到了这四项技术,那么你是否对他们有足 够的了解呢? 很多人把 JVM 看成是 Java 的解释器,这是错误的理解,并不是所有的 JVM 实现都 Java 的解释器, 有的也用到了 JIT (Just-In-Time) 技术。Java 是不允许直接操作本地机器指令的,对于 Java 方法来说有 两种:Java 和 native,对于 native 我更习惯用 C++ 写 DLL,但 Java 并不提倡你这么做,因为有损 Java 平台 独立性。JVM 中除了执行引擎就是类加载器了,ClassLoader 也分为两种:原始加载器和加载器 Object,原 始加载器使用和写 JVM 一样的语言写的,比如用 C 写的类加载器,而加载器 Object 就是用 Java 实现的类加 载器,方便我们扩展,比如你自己可以 New 一个 URLClassLoader 从网络上下载字节码到本地运行。一个类 的加载和他参考的类的加载应该用同一个 ClassLoader。这一点在发生异常的时候很难找出,比如 OSGI 中 每个 bundle 都有自己独立的 ClassLoader,对于新手很容易犯错误而无从下手,我们熟悉的 WEB 服务器
  • 2. Tomcat 的类加载器是分层(有继承关系)的,所以在应用整合的时候也很容易发生 ClassLoader 相关的异 常,而这样的异常往往很难定位。平台互异的字节序问题,在 Java 中,字节码是大字节序的。Java 为支持 开发者开发应用软件提供了大量的 API,可以说,在计算机领域的大部分计算中 Java 都有对应的解决方案。 C++中可能比较受关注和困扰的就是指针了,而在 Java 中用“参考”这样一个类似的东西代替了,参考不 向指针那样允许参与计算,避免了开发人员直接操作内存,还有个垃圾回收机制也避免了开发者手动释放内 存,还有就是 C++ 中的数组是不进行边界检查的而 Java 中每次使用数组的时候都要进行边界检查,岂不安 全。 可见 Java 相比 C++ 提高了开发效率和安全性。Java 和 C++ 比运行速度是个大问题,因此任何语言都不 万能的,在开发是我们应该适当权衡,Java 运行速度低的原因主要有: Ø Interpreting bytecodes is 10 to 30 times slower than native execution. Ø Just-in-time compiling bytecodes can be 7 to 10 times faster than interpreting, but still not quite as fast as native execution. Ø Java programs are dynamically linked. Ø The Java Virtual Machine may have to wait for class files to download across a network. Ø Array bounds are checked on each array access. Ø All objects are created on the heap (no objects are created on the stack). Ø All uses of object references are checked at run-time for null. Ø All reference casts are checked at run-time for type safety. Ø The garbage collector is likely less efficient (though often more effective) at managing the heap than you could be if you managed it directly as in C++. Ø Primitive types in Java are the same on every platform, rather than adjusting to the most efficient size on each platform as in C++. Ø Strings in Java are always UNICODE. When you really need to manipulate just an ASCII string, a Java program will be slightly less efficient than an equivalent C++ program. 2 JVM 安全框架 Java 允许基于网络的代码运行和传播,为其带了安全问题。但 Java 也提供了内嵌的安全模型,开发者可高 枕无忧。Java 的沙箱安全模型使即时你不信任的代码也可让他在本机执行,如果是恶意的代码他的恶意行为 也会被沙箱拦截,所以在运行任何你有点怀疑的代码前请确保你的沙箱没有缺陷。 对于沙箱的四大基础组件是:类加载器,类文件验证,JVM 安全特性,安全管理的 API,其中最重要的是类加 载器和安全管理 API,因为他们可以客制化。对于加载器,每个 JVM 都可以有多个,同一个类可以加载多次 到不同的 ClassLoader 中,类跨 ClassLoader 是不可见的,而在同一 ClassLoader 中是可直接访问的,这样 可以隔离一些不安全的类。
  • 3. 类型检查是很必要的,分为两个阶段,第一是在类加载进来的时候要进行类的合法性和完整性检查,第二是 运行时确认该类所参考的类,方法和属性是否存在。类文件头都是以一个四个字节的幻数开头 (0xCAFEBABE)来标识是个类文件,当然也有文件大小域,第一阶段确保加载进来的类是正确格式,内部 一直,Java 语法语义限制一直,包括安全的可执行代码,在这个过程中如果有错误,JVM 会抛出异常,该类就 不会被使用。第二阶段其实由于动态连接的原因,需要在运行时检查参考,因为 ClassLoader 在需要某些类 时才去加载,延迟加载,在 ORM 产品中,比如 Hibernate, jdo 等都有所谓的延迟加载 SecurityManager 有一系列的 checkXXX 的方法,用来检测相关操作是否合法,一般我们的程序是不用 SecurityManager 的,除非你安装一个 SecurityManager,如果没有写自己的策略文件,一般是用 jre 下面 的默认策略文件的设置,当然也可在 VM 运行参数设置策略文件的位置。SecurityManager 类的相关方 法。 publicstatic SecurityManager getSecurityManager() { return security; } publicstatic void setSecurityManager(final SecurityManager s) { try { s.checkPackageAccess("java.lang"); } catch (Exception e) { // no-op } setSecurityManager0(s); } privatestaticsynchronized void setSecurityManager0(final SecurityManager s) { SecurityManager sm = getSecurityManager(); if (sm != null) { // ask the currently installed security manager if we // can replace it. sm.checkPermission(new RuntimePermission ("setSecurityManager"));
  • 4. } if ((s != null) && (s.getClass().getClassLoader() != null)) { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { s.getClass().getProtectionDomain().implies (SecurityConstants.ALL_PERMISSION); returnnull; } }); } security = s; InetAddressCachePolicy.setIfNotSet(InetAddressCachePolicy.FOREVER);} 设置自己的 SercurityManager: System.setSecurityManager(new JackSecurityManager()); SecurityManager sm = System.getSecurityManager(); 3 JVM 内部机理 3.1 JVM 的生命周期 任何一个类的 main 函数运行都会创建一个 JVM 实例,当 main 函数结束 JVM 实例也就结束了,如果三 个类分别运行各自的 main 函数则会创建 3 个不同的 JVM 实例。JVM 实例启动时默认启动几个守护线 程,而 main 方法的执行是在一个单独的非守护线程中执行的。只要母线程结束,子线程就自动销 毁,只要非守护 main 线程结束 JVM 实例就销毁了。 3.2 JVM 的框架 JVM 主要由 ClassLoader 子系统和执行引擎子系统组成,运行数据区分为五个部分,他们是方法区, 堆,栈,指令寄存器,本地方法栈。方法区和堆是所有线程共享的,一般我们习惯对临时变量放在寄 存器中,但 JVM 中不用寄存器而是用栈,每个线程都有自己独立的栈空间和指令计数器,其中栈里的 元素叫帧,帧有三部分组成分别是局部变量,操作数栈和帧数据。正式因为栈是每个线程独有的,所 以对于 local 变量,是没有多线程冲突问题的。栈和帧的数据结构和大小规范里没有硬性规定,由 实现者具体实做。 3.3 数据类型
  • 5. 3.3.1 Java 数据类型 虚拟机的数据类型分为:原始类型和参考类型,有人一直说 Java 只有 90%是 OO 的,其中原始类型就算 的上,不过还好有对应的封装类型如 int->Integer,JDK1.5 可以用这些封装类型直接做算术运算,不 过 Java 内部要做拆箱和装箱的工作。似乎 OO 了些,不过现代的程序员才不过你这些,能用就行。 字长是数据值得基本单元,一般像 int, byte, short 等类型的值用一个字长存储,而浮现类型用两个 字长存储,字长的大小一般和机器的指针大小相同,最小是 32 位。 像 byte, short, or char 这样的类型在方法区和堆中 Java 是原来类型,但在程序运行时放在栈帧 中,统一用整型(int)来表示 对于类型之间的转换 JVM 有对应的指令如: 上图是小范围向大范围转变,叫做向上转型,当然也有向下转型的指令,不过可能会造成数据被破坏, 如图: 不管你怎么变化,栈帧里都是以字为单位的,32 位机子一个字是 4 个字节,正好是一个整型大小,如前 所述原始类型在栈中都被表示为 int 型,当然如果是 long 和 double 那就用两个字来表示咯。 3.3.2 浮点运算 浮点运算算法因该是倍受关注的了,Java 中浮点数的表示遵照 IEEE 754 1985 的规范,这个规范 定义了 32 位和 64 位浮点格式和运算,我都知道 Java 中的 float 是 32 位而,double 是 64 位。对 于浮点数大家都很清楚他分为四个部分:符号,尾数,基数和幂,要得到浮点数值必须将这四个 部分相乘才能得到值。所有的浮点数都要经过标准化,我们都知道计算机里只有 0,1 这样的表 示,尾数和幂都要二进制表示,对于尾数 float 用它的 23 位表示,double 用它的 52 位来表示, 对于幂数 float 用它的 8 位表示,double 用它的 11 为表示,剩下以为表示符号。 JVM 也有对应的浮点运算指令如图(加法指令) 3.4 方法区 ClassLoader 负责把 class 加载进来并解析类信息放在方法区,方法区是被所有线程共享的,所以方法 区必须保证线程的安全。比如两个线程同时需要一个类,必须保证只有一个线程去加载而另一个线程 等待。 方法区的大小不是固定的,一般会在运行时 JVM 会根据运行情况作出调整,但 JVM 实现者都留有接口可 供调节,比如最大和最小值。 我们都知道堆是被垃圾回收器回收的,在方法区的类也是会被回收的,类也有他的生命周期,当某个 类不再被参考之后也就没有这个类的引用和对象了那么他的存在就没有意义了,只是站着内存,回收 器就会按照一定的算法将它卸载了。对于一个类信息的表述都是用全名的,在方法区中也有其他信 息,比如常量池,属性和方法信息,还有就是指向堆中的 ClassLoader 和 Class 的参考。我大家都熟 悉的就是类的静态变量,也放在方法区的由所有的对象实例共享。
  • 6. 可以想象在方法区内放置了大量的二进制的 Class 信息,为了加快访问速度,JVM 实现者都会维护一个 方法表,记得读研一的时候中间件老师讲过这些东西是结合指针讲的 C++ 的内存模型。另外注意的就 是方法表只针对可实例化的类,对抽象类和接口没有意义。 每个 JVM 实例都只有一个堆,所有的线程共享其中的对象,这才出现了多线程安全问题。JVM 有 new 对 象的指令但没有释放对象的指令,当让这些指令都是虚拟指令,这些对象的释放是有 GC 来做的,GC 在 JVM 规范中并没有硬性的规定,有实现者设计他的实现形式和算法。 想必很多同人都想知道,对象是怎么样在堆里表示的,其实很简单。其实面 JVM 规范也没有细致的规定 对象怎么在堆里表示的, 如图是一个参考的堆模型,具体实现可能不是这样的,这个是 HeapOfFish applet 的一个演示模型, 具体内容可以看看 JVM 规范。当然也有很多其他的模型,这个模型的好处就是在堆压缩的时候很方便, 而在 reference 直接 point 到一个对象的模型来说在堆压缩方面是很麻烦的,因为你要考虑到方法 区,堆,栈里可能的参考,你都要修改。对象还有一个很重要的数据结构就是方法表,方法表可以加 快访问速度,但并不是说所有的 JVM 实现都有。 堆中的每个对象都有指向方法区的指针,而自己主要保留对象属性信息,如图: 看一个方法区链接的例子,看看一个类是怎么加载进来,怎么链接初始化的: 有一 Salutation 类 class Salutation { private static final String hello = "Hello, world!"; private static final String greeting = "Greetings, planet!"; private static final String salutation = "Salutations, orb!"; private static int choice = (int) (Math.random() * 2.99); public static void main(String[] args) { String s = hello; if (choice == 1) { s = greeting; } else if (choice == 2) { s = salutation; } System.out.println(s);
  • 7. } } Assume that you have asked a Java Virtual Machine to run Salutation. When the virtual machine starts, it attempts to invoke the main() method of Salutation. It quickly realizes, however, that it canít invoke main(). The invocation of a method declared in a class is an active use of that class, which is not allowed until the class is initialized. Thus, before the virtual machine can invoke main(), it must initialize Salutation. And before it can initialize Salutation, it must load and link Salutation. So, the virtual machine hands the fully qualified name of Salutation to the primordial class loader, which retrieves the binary form of the class, parses the binary data into internal data structures, and creates an instance of java.lang.Class. 常量池里的内容: Index Type Type Value 1 CONSTANT_String_info CONSTANT_String _info 30 2 CONSTANT_String_info CONSTANT_String _info 31 3 CONSTANT_String_info CONSTANT_String _info 39 4 CONSTANT_Class_info CONSTANT_Class_ info 37 5 CONSTANT_Class_info CONSTANT_Class_ info 44 6 CONSTANT_Class_info CONSTANT_Class_ info 45 7 CONSTANT_Class_info CONSTANT_Class_ info 46 8 CONSTANT_Class_info CONSTANT_Class_ info 47 9 CONSTANT_Methodref_info CONSTANT_Method ref_info 7, 16 10 CONSTANT_Fieldref_info CONSTANT_Fieldr ef_info 4, 17 11 CONSTANT_Fieldref_info CONSTANT_Fieldr ef_info 8, 18 12 CONSTANT_Methodref_info CONSTANT_Method ref_info 5, 19 13 CONSTANT_Methodref_info CONSTANT_Method ref_info 6, 20 14 CONSTANT_Double_info CONSTANT_Double _info 2.99 16 CONSTANT_NameAndType_info CONSTANT_NameAn dType_info 26, 22 17 CONSTANT_NameAndType_info CONSTANT_NameAn dType_info 41, 32
  • 8. 18 CONSTANT_NameAndType_info CONSTANT_NameAn dType_info 49, 34 19 CONSTANT_NameAndType_info CONSTANT_NameAn dType_info 50, 23 20 CONSTANT_NameAndType_info CONSTANT_NameAn dType_info 51, 21 21 CONSTANT_Utf8_info CONSTANT_Utf8_i nfo "()D" 22 CONSTANT_Utf8_info CONSTANT_Utf8_i nfo "()V" 23 CONSTANT_Utf8_info CONSTANT_Utf8_info (Ljava/ lang/String;)V" 24 CONSTANT_Utf8_info CONSTANT_Utf8_info ([Ljava/lang/String;)V" 25 CONSTANT_Utf8_info CONSTANT_Utf8_info <clinit" 26 CONSTANT_Utf8_info CONSTANT_Utf8_info <init" 27 CONSTANT_Utf8_info CONSTANT_Utf8_info "Code" 28 CONSTANT_Utf8_info CONSTANT_Utf8_info "ConstantValue" 29 CONSTANT_Utf8_info CONSTANT_Utf8_info "Exceptions" 30 CONSTANT_Utf8_info CONSTANT_Utf8_info "Greetings,planet!" 31 CONSTANT_Utf8_info CONSTANT_Utf8_info "Hello, world!" 32 CONSTANT_Utf8_info CONSTANT_Utf8_info "I" 33 CONSTANT_Utf8_info CONSTANT_Utf8_info "LineNumberTable" 34 34 "Ljava/io/PrintStream;" CONSTANT_Utf8_info 35 CONSTANT_Utf8_info CONSTANT_Utf8_info "Ljava/ lang/String;" 36 CONSTANT_Utf8_info CONSTANT_Utf8_info "LocalVariables" 37 CONSTANT_Utf8_info CONSTANT_Utf8_info "Salutation" 38 CONSTANT_Utf8_info CONSTANT_Utf8_info "Salutation.java" 39 CONSTANT_Utf8_info CONSTANT_Utf8_info "Salutations, orb!" 40 CONSTANT_Utf8_info CONSTANT_Utf8_info "SourceFile" 41 CONSTANT_Utf8_info CONSTANT_Utf8_info "choice" 42 CONSTANT_Utf8_info CONSTANT_Utf8_info "greeting" 43 CONSTANT_Utf8_info CONSTANT_Utf8_info "hello" 44 CONSTANT_Utf8_info CONSTANT_Utf8_info
  • 9. "java/io/PrintStream" 45 CONSTANT_Utf8_info CONSTANT_Utf8_info "java/lang/Math" 46 CONSTANT_Utf8_info CONSTANT_Utf8_info "java/lang/Object" 47 CONSTANT_Utf8_info CONSTANT_Utf8_info "java/lang/System" 48 CONSTANT_Utf8_info CONSTANT_Utf8_info "main" 49 CONSTANT_Utf8_info CONSTANT_Utf8_info "out" 50 CONSTANT_Utf8_info CONSTANT_Utf8_info "println" 51 CONSTANT_Utf8_info CONSTANT_Utf8_info "random" 52 CONSTANT_Utf8_info CONSTANT_Utf8_info "salutation" As part of the loading process for Salutation, the Java Virtual Machine must make sure all of Salutationís superclasses have been loaded. To start this process, the virtual machine looks into Salutationís type data at the super_class item, which is a seven. The virtual machine looks up entry seven in the constant pool, and finds a CONSTANT_Class_info entry that serves as a symbolic reference to class java.lang.Object. See Figure 8-5 for a graphical depiction of this symbolic reference. The virtual machine resolves this symbolic reference, which causes it to load class Object. Because Object is the top of Salutationís inheritance hierarchy, the virtual machine and links and initializes Object as well. 3.5 操作数栈 操作数栈是 Java 运行时的核心栈,看看 i+j 的一个简单运算, iload_0 iload_1 iadd istore_2 以上是四个 JVM 指令,完成 i+j 并把结果保存到 k 中,如图示: 在堆中不可能分配一个原始类型的空间放值,而是先用对象封装才能存在堆空间中,带 Java 栈中也不 可能放对象,而只有原始类型和参考类型。上次有人争议数组放在何处?在 Java 中数组和对象是同等 地位的,都放在堆中而他的参考是放在栈里,JVM 有对应的指令比如 newarray, anewarray 等。 3.6 本地方法栈 想必很多人用过 JNI 结束,Java 是不提倡这么做的,而且在这放的设计和实现上,个人觉得不是那 么好,至少他比不那么方便,所以很少见应用开发者去写些 Native 方法,每次你去看 Java 原代码是 你经常看到 native 方法,也看到 JDK 下的 DLL 文件,大部分 JVM 都是用 C 或 C++写的。前面也提 过,这样就破坏了 Java 的平台独立性,在本地方法运行的时候也有专门的栈去处理。Java 在执行本 地方法的时候暂时放弃 Java stack 的操作,转向本地方法,本地方法有自己的栈或堆的处理方 式,Java 在执行本地方法时会在本地栈和 Java 栈之间切换,如图:
  • 10. a thread first invoked two Java methods, the second of which invoked a native method. This act caused the virtual machine to use a native method stack. In this figure, the native method stack is shown as a finite amount of contiguous memory space. Assume it is a C stack. The stack area used by each C-linkage function is shown in gray and bounded by a dashed line. The first C-linkage function, which was invoked as a native method, invoked another C-linkage function. The second C-linkage function invoked a Java method through the native method interface. This Java method invoked another Java method, which is the current method shown in the figure,做过 JNI 开发的朋友应该了解 Java 和 C,C++ 是如 何交互的,这些都是执行引擎的事。 3.7 执行引擎 执行引擎,应该是 JVM 的核心了,一般我把它看作指令集合。JVM 规范详细的描述了每个指令的作用, 但没有进一步描述如何实现,还由实现厂商自己设计,可以用解释的方式,JIT 方式,编译本地代码的 方式,或者几者混合的方式,当然也可用一些新的不为我们知道的技术。 每一个线程都有一个自己的执行引擎,据我了解 JVM 指令用一个字节表示,也就是 JVM 最多有 256 个指 令,目前 JVM 已有 160(目前可能多于这些)个指令。就有这百十个指令组成了我的系统,JVM 指令一 般只有操作码没有操作数,一般操作数放在常量池和 Java 栈中,设计指令集的最重要的目标应该是平 台独立性,同时在验证 bytecode 也比较方便。有些指令的操作都是基于具体类型的,有的就没有比如 goto,如图 指令前缀表示操作类型, 可见 Java 编译器和 JVM 对类型操作要求很严格,为何使指令尽量用一个字节 表示,很多类型如 byte, char 都没有直接运算,而是在运算前把他们转换成整型。 执行过程的在某一时刻内容如图: 在帧里的局部变量有四个分别都是 reference,都指向不同的对象,有的时候我们编程当操作完成, 最好把 o, greeter ,c, gcl 这四个参考付为 null,这样他们指向的对象不可达,对象不可达,他 们在方法区的类信息也不可达,类信息不可达,堆中的 Class 对象不可达,你可以看到图中的所有对 象,类信息都是可回收状态,这样 GC 某个时刻就可以释放了这些内存了。 写方法时我们希望 try cache 起来,当然也有需要在后面 加上 finally,我们都知道在 finally 里 的代码被执行玩前所有的 return 操作都会压入栈里,等 finally 块执行完了再弹出返回,如: static boolean test(boolean bVal) { while (bVal) { try { return true; } finally { break; }
  • 11. } return false; } 可以看出这个方法的执行以返回 false 告终。 4 类文件结构 读者可以用 UltrEdit 之类的编辑工具打开一个 class 文件查看,每个类文件都包含如下内容如图:其中 u2 表示两个字节大小,u4 表示四个字节大小。 如第一行所示,每个类文件都以幻数开头固定为(0xCAFEBABE),用 UltraEdit 等编辑工具可以直接看到,当 然你也可以看到很多如图所示的属性和方法的描述: 常量池是以类型来划分不同的表格的,如 CONSTANT_Double_info 表就只是 double 常量 值,CONSTANT_Class_info 表存储参考信息,比如类,接口,属性和方法的符号参考,类文件的核心数据结构 就是常量池了,而所有的类,属性和方法的描述以及值都是通过 index 到常量池获得的,所以常量池是个很 重要的概念,有兴趣的朋友可进一步参考 JVM 规范。 5 线程同步 对多线程的支持是 java 语言的一大亮点,java 实现线程同步的方式是 monitor,具体有两种方 式:互斥和协同,每个对象都有一把锁,通过 lock 和 unlock 实现互斥的操作,Object 类有 wait notify 等这样的方法可以实现线程为实现一个共同的目标而协同工作。monitor 就像一个建筑有一个特殊的房间每次 只允许一个线程访问。都知道线程同步有一个重要的概念就是临界区,任何一个线程要想进入临界区就必须 获得 monitor,如果线程发现 monitor 被其他线程获得,此线程必须排队,等到他前面的线程也退出了 monitor 的占有,则此线程才能操作 monitor 的临界区,当然也有优先级 的问题。此图说明了多个线程协 作工作的过程。 前面介绍过,堆和方法区是被线程共享的,这两大块要做同步,而对于局部变量是由线程独有的所 以不存在同步问题,Java API 中有一个很重要也很简单的类就是 ThreadLocal,他是用 Theard key 来标识 变量,是非局部变量局部化的一个很好的例子。 对象锁的概念大家都很熟悉了,Java 中通过这个锁有两种方式,一种是同步块,一种是同步方法, 具体的内容请看看 java 多线程编程的书。 6 垃圾回收机制 堆里聚集了所有由应用程序创建的对象,JVM 也有对应的指令比如 new, newarray, anewarray 和 multianewarray,然并没有向 C++ 的 delete,free 等释放空间的指令,Java 的所有释放都由 GC 来 做,GC 除了做回收内存之外,另外一个重要的工作就是内存的压缩,这个在其他的语言中也有类似的实 现,相比 C++ 不仅好用,而且增加了安全性,当然她也有弊端,比如性能这个大问题。 垃圾回收的算法有很多,不过怎么样,任何一个算法都要做两件事情,一是识别垃圾对象,二是清理内存以 备运行程序使用。区分活着的对象和垃圾对象的主要方法就是参考计数和追踪,参考计数就是每个对象都维 护一个引用 count,而每次追踪都去标记对象,当一次跟踪完成没有被标记的对象就是不可到达的那就是垃 圾了。参考计数的方式现在已经很少 JVM 实现用了,因为对于 child, parent 的内部参考,count 永远不 会是 0,也就是不会是垃圾对象,而且每次修改引用都要加减 count。而跟踪是从根节点开始扫描所有对
  • 12. 象,并作标记。知道那些对象是垃圾了之后,就要释放了,有两种方式一个是压缩,一个是拷贝,不管哪种 方式改变了对象的物理位置,就要修改他的参考值,每个对像都有一个 finalize 方法,一般我们不去 override 他,他是在回收时期执行,但并不是所有的对象的 finalize 方法都会被执行,具体内容请看 《inside in java machine》. 7 总结 本文结合作者的一些观点和几年的开发实践,谈谈对 JVM 规范的看法,由于完稿仓促,也没多少时间整理, 如果有什么问题可以进一步讨论,本人编译器这方面也是比较薄弱,真正按照 JVM 规范实现虚拟机还有很长 的路要走,可能在将来的某天有机会进一步研究一下。多谢各位的指导和帮助,我会陆续在 Blog 里发些文 章。 8 参考资料 1)inside in java machine (想要的请点击下载) 2)http://www.blogjava.net/Jack2007/archive/2008/05/23/202485.html《Java 虚拟机深入研究》 3)http://www.blogjava.net/Jack2007/archive/2008/05/21/202018.html 4)http://www.tudou.com/programs/view/hN_4sQJMoFQ/ 5)http://www.blogjava.net/Jack2007/archive/2008/04/11/192288.html 6)http://developer.51cto.com/art/200805/73338.htm Java 虚拟机深入研究 文章来自:http://www.qqread.com/java/w872354600.html Java 技术与 Java 虚拟机 说起 Java,人们首先想到的是 Java 编程语言,然而事实上,Java 是一种技术,它由四方面组成:Java 编程语 言、Java 类文件格式、Java 虚拟机和 Java 应用程序接口(Java API)。它们的关系如下图所示:
  • 13. 图 1 Java 四个方面的关系   运行期环境代表着 Java 平台,开发人员编写 Java 代码(.java 文件),然后将之编译成字节码(.class 文件)。 最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的 转换成机器码执行。从上图也可以看出 Java 平台由 Java 虚拟机和 Java 应用程序接口搭建,Java 语言则是进入 这个平台的通道,用 Java 语言编写并编译的程序可以运行在这个平台上。这个平台的结构如下图所示: (点击查看原图)   在 Java 平台的结构中, 可以看出,Java 虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无关 的关键。它的下方是移植接口,移植接口由两部分组成:适配器和 Java 操作系统, 其中依赖于平台的部分称为适 配器;JVM 通过移植接口在具体的平台和操作系统上实现;在 JVM 的上方是 Java 的基本类库和扩展类库以及它 们的 API, 利用 Java API 编写的应用程序(application) 和小程序(Java applet) 可以在任何 Java 平台上运行 而无需考虑底层平台, 就是因为有 Java 虚拟机(JVM)实现了程序与操作系统的分离,从而实现了 Java 的平台无
  • 14. 关性。 那么到底什么是 Java 虚拟机(JVM)呢?通常我们谈论 JVM 时,我们的意思可能是: 对 JVM 规范的的比较抽象的说明; 对 JVM 的具体实现; 在程序运行期间所生成的一个 JVM 实例。 对 JVM 规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》 (《Java 虚拟机规范》)中被详细地描述了;对 JVM 的具体实现要么是软件,要么是软件和硬件的组合,它已经 被许多生产厂商所实现,并存在于多种平台之上;运行 Java 程序的任务由 JVM 的运行期实例单个承担。在本文 中我们所讨论的 Java 虚拟机(JVM)主要针对第三种情况而言。它可以被看成一个想象中的机器,在实际的计算机 上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自己相应的指令系统。 JVM 在它的生存周期中有一个明确的任务,那就是运行 Java 程序,因此当 Java 程序启动的时候,就产生 JVM 的 一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从 JVM 的体系结构和它的运行过程这两个方 面来对它进行比较深入的研究。 Java 虚拟机的体系结构 刚才已经提到,JVM 可以由不同的厂商来实现。由于厂商的不同必然导致 JVM 在实现上的一些不同,然而 JVM 还是可以实现跨平台的特性,这就要归功于设计 JVM 时的体系结构了。 我们知道,一个 JVM 实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这些部 分,它们描述了 JVM 的一个抽象的内部体系结构,其目的不光规定实现 JVM 时它内部的体系结构,更重要的是提 供了一种方式,用于严格定义实现时的外部行为。每个 JVM 都有两种机制,一个是装载具有合适名称的类(类或是 接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令,叫做运行引擎。每个 JVM 又 包括方法区、堆、Java 栈、程序计数器和本地方法栈这五个部分,这几个部分和类装载机制与运行引擎机制一起 组成的体系结构图为: 图 3 JVM 的体系结构   JVM 的每个实例都有一个它自己的方法域和一个堆,运行于 JVM 内的所有的线程都共享这些区域;当虚拟机 装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行的时 候,JVM 把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和 Java 栈,其
  • 15. 中程序计数器中的值指向下一条即将被执行的指令,线程的 Java 栈则存储为该线程调用 Java 方法的状态;本地 方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。 下面分别对这几个部分进行说明。 执行引擎处于 JVM 的核心位置,在 Java 虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指令,规 范很详细地说明了当 JVM 执行字节码遇到指令时,它的实现应该做什么,但对于怎么做却言之甚少。Java 虚拟机 支持大约 248 个字节码。每个字节码执行一种基本的 CPU 运算,例如,把一个整数加到寄存器,子程序转移 等。Java 指令集相当于 Java 程序的汇编语言。 Java 指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有 0 个或多个操作数,提供操作所需的参 数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。 虚拟机的内层循环的执行过程如下: do{ 取一个操作符字节; 根据操作符的值执行一个动作; }while(程序未结束) 由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。指令中操作数的数量和大小 是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个 16 位的参数存放时 占用两个字节,其值为: 第一个字节*256+第二个字节字节码。 指令流一般只是字节对齐的。指令 tableswitch 和 lookup 是例外,在这两条指令内部要求强制的 4 字节边界对 齐。 对于本地方法接口,实现 JVM 并不要求一定要有它的支持,甚至可以完全没有。Sun 公司实现 Java 本地接口 (JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替 Sun 公司的 JNI。但是这些设计与实 现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。 Java 的堆是一个运行时数据区,类的实例(对象)从中分配空间,它的管理是由垃圾回收来负责的:不给程序员显式 释放对象的能力。Java 不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。 Java 方法区与传统语言中的编译后代码或是 Unix 进程中的正文段类似。它保存方法代码(编译后的 java 代码)和 符号表。在当前的 Java 实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类文件包含了 一个 Java 类或一个 Java 界面的编译后的代码。可以说类文件是 Java 语言的执行代码文件。为了保证类文件的 平台无关性,Java 虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考 Sun 公司的 Java 虚拟机规 范。 Java 虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java 虚拟机的寄存器有四 种: pc: Java 程序计数器; optop: 指向操作数栈顶端的指针; frame: 指向当前执行方法的执行环境的指针;。 vars: 指向当前执行方法的局部变量区第一个变量的指针。 在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计数器。 当线程执行 Java 方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本地的方法,那 么程序计数器的值就不会被定义。 Java 虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。
  • 16. 局部变量区 每个 Java 方法使用一个固定大小的局部变量集。它们按照与 vars 寄存器的字偏移量来寻址。局部变量都是 32 位 的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例如,一个具有索 引 n 的局部变量,如果是一个双精度浮点数,那么它实际占据了索引 n 和 n+1 所代表的存储空间)虚拟机规范并不要 求在局部变量中的 64 位的值是 64 位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把 操作数栈中的值写入局部变量的指令。 运行环境区 在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。 动态链接 运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的 class 文件 代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载 必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接 方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。 正常的方法返回 如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在 正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法 调用指令,然后在调用者的执行环境中继续执行下去。 异常捕捉 异常情况在 Java 中被称作 Error(错误)或 Exception(异常),是 Throwable 类的子类,在程序中的原因是:① 动态 链接错,如无法找到所需的 class 文件。②运行时错,如对一个空指针的引用。程序使用了 throw 语句。 当异常发生时,Java 虚拟机采取如下措施: 检查与当前方法相联系的 catch 子句表。每个 catch 子句包含其有效指令范围,能够处理的异常类型,以及处理异 常的代码块地址。 与异常相匹配的 catch 子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常类型是其能处理 的异常类型的子类型。如果找到了匹配的 catch 子句,那么系统转移到指定的异常处理块处执行;如果没有找到异 常处理块,重复寻找匹配的 catch 子句的过程,直到当前方法的所有嵌套的 catch 子句都被检查过。 由于虚拟机从第一个匹配的 catch 子句处继续执行,所以 catch 子句表中的顺序是很重要的。因为 Java 代码是结 构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可能的程序计数器的值,都可以 用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的异常情况。 如果找不到匹配的 catch 子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调用者,好像异常刚 刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这种错误将被传播下去。如果错 误被传播到最顶层,那么系统将调用一个缺省的异常处理块。 操作数栈区 机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存 器或非通用寄存器的机器(如 Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是 32 位的。它用于给方法 传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd 指令将两个整数相加。相加的 两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并 把结果压回到操作数栈中。
  • 17. 每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了 long 和 double 型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个 int 类型的数,如果把 它们当作是一个 long 类型的数则是非法的。在 Sun 的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有 少数操作(操作符 dupe 和 swap),用于对运行时数据区进行操作时是不考虑类型的。 本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可以访问 虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个 C 语言的栈,那么当 C 程序调用 C 函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现 Java 虚拟机时,本地方法 接口使用的是 C 语言的模型栈,那么它的本地方法栈的调度与使用则完全与 C 语言的栈相同。 Java 虚拟机的运行过程 上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。 虚拟机通过调用某个指定类的方法 main 启动,传递给 main 一个字符串数组参数,使指定的类被装载,同时链接 该类所使用的其它的类型,并且初始化它们。例如对于程序: class HelloApp { public static void main(String[] args) { System.out.println("Hello World!"); for (int i = 0; i < args.length; i++ ) { System.out.println(args[i]); } } }   编译后在命令行模式下键入: java HelloApp run virtual machine 将通过调用 HelloApp 的方法 main 来启动 java 虚拟机,传递给 main 一个包含三个字符 串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行 HelloApp 时可能采取的步骤。 开始试图执行类 HelloApp 的 main 方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代 表,于是虚拟机使用 ClassLoader 试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载 后同时在 main 方法被调用之前,必须对类 HelloApp 与其它类型进行链接然后初始化。链接包含三个阶段:检 验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标 准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的 静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如 下:
  • 18. (点击查看原图) 图 4:虚拟机的运行过程   结束语 本文通过对 JVM 的体系结构的深入研究以及一个 Java 程序执行时虚拟机的运行过程的详细分析,意在剖析清楚 Java 虚拟机的机理。