2012年10月9日星期二

JVM原理简述

什么是JVM?

         JVM是Java Virtual Machine(java虚拟机)的简称。它是一种规范。通过在实际计算机上仿真模拟各种计算功能来实现的。Java虚拟机有自己完善的硬件体系架构,例如处理器,堆栈,寄存器等,还有相应的指令系统。由于JAVA虚拟机屏蔽的具体操作系统平台的相关信息,使得java程序只需要生成在java虚拟机上运行的代码(字节码),就可以在多种不同平台上运行。而字节码又经过java虚拟机解释成具体平台的机器指令来执行。

什么是字节码?

          ByteCode指通过编译生成,但是与机器码无关,通常要经过解释器转义后才可以成为机器码的中间代码。编译器将特定的源码编译为字节码,而字节码又经过虚拟机转义成为可以直接运行的指令。

JVM的执行过程

编译

        由java编译器将源码编译为字节码(class文件)
        编译过程如下:
        1. 词法分析,扫描类名,变量和函数
        2. 语法分析
        3. 注解处理
        4. 语义分析,名字,表达式,变量,方法,类型,参数等一系列检查
        5. 生成字节码文件(class)

装载

        首先,由类装载器(classloader)装载class文件。
        1. BootStrapClassLoader装载jdk/lib下和Xbootclasspath指定的所有jar包;
        2. ExtClassLoader装载jdk/lib/ext下和-Djava.ext.dirs指定的所有jar包
        3. 由AppClassLoader装载-classpath和-Djava.class.path指定的目录和jar;
        4. 自定义类装载器,运行时动态加载class文件。
       注:装载时会进行验证,如果找不到class文件会抛出classNotFoundException
       装载完成后,会进行字节码校验:
       1. FileFormat-VerifyError
       2. 引用类:NoClassDefFoundError
       3. 变量:NoSuchFieldError
       4. 方法:NoSuchMethodError
       5. 权限
       检查完毕后,开始初始化class。
       jvm中执行代码有两种方式:1. 解释执行,即由jvm中的java解释器将字节码解释为不同操作系统平台的指令执行;2. 编译执行,将字节码编译为机器码执行。这个编译过程由JVM中的JIT完成。只有使用频率高的代码才会被JIT编译为机器码执行,以提高效率。

JVM内存区域

由图1中可以看出,JVM内存区分为1. 方法区,2. 堆,3. 栈,4. 程序计数器,5. 本地方法栈。

方法区(Perm区)

        由classloader加载的class的类型信息全部存储在方法区中。所有的静态变量也存在于方法区中。方法区是所有线程共享的,因此方法区是线程安全的。方法区是内存永久保存区域,当perm空间不足时,会引发full GC,GC之后还不足,会抛出permGen space不足的错误。-XX:PermSize, -XX:MaxPermSize来指定方法区的最小和最大值。
        主要存储内容:
        1. 此类型的全限定名
        2. 此类型的超类的全限定名
        3. 此类型是接口还是类
        4. 此类型的访问修饰符号(public,private等)
        5. 此类型的常量池,字段,方法信息
        6. 除常量意外的所有静态变量
        7. 一个到classloader的引用
        8. 一个到class的引用

        JVM的堆中存在了所有类的实例和所有数组。一个JVM只有一个堆空间,各个线程都共享这个空间。
        图中可以看出,堆分为young区和old区,一般young和old的配置比例为1:m。
        young区保存大部分新对象,便于高效回收。其中Eden区存储新创建的对象,Eden满的时候,会有一次小范围的youngGC,将Eden中依然存活的对象存储至from,当from满时,此区域的活动对象会移动到to,直到from/to区域都满的时候,依然存活的对象会移到old区。-Xmn设置新生代大小。-XX:NewSize和-XX:MaxNewSize设置young的最小值和最大值;-XX:NewRatio=m设置young:old=1:m。-XX:SurvivorRatio=8设置Eden和from/to的比例为8:1:1
        old区保存存活时间较长的对象,当整个新生代满的时候,会触发一次full GC,将新生代的存活对象移到old。通过-Xmx-Xmn设置旧生代大小。
-Xmx和-Xms来指定heap的最大和最小值,避免JVM动态调整则设置为同一个值即可。

程序计数器

每一个线程都有自己的程序计数器,里面存放了下一条被执行指令的地址。

        每一个线程被创建的时候,都会得到自己的程序计数器和java栈。java栈以帧为单位保存调用信息。当线程调用一个方法,JVM会向栈中压入一个新的栈帧,当方法调用结束时,栈帧会被弹出。java栈帧是线程私有的,不存在多线程的安全性问题。
        栈帧由3部分构成,局部变量区,操作数栈,帧数据区。
        1. 局部变量区
        编译器将调用方法的局部变量和参数按照声明顺序放入局部变量数组中,从0开始计数。如果是实例方法,this将被存储到数组的第一个位置。java中的对象传递都是按照引用传递的,因此java对象全部存储在堆中,而局部变量区和操作数栈中只存储了对象的引用。
        2. 操作数栈
        JVM把操作数栈作为它的工作区,大多数指令都要从这里弹出数据,执行运算,然后把结果压回操作数栈,然后等相关的指令将结果再次弹出。操作数栈扮演了暂存操作数的角色。
        3. 帧数据区
        栈帧还可以存储常量池的解析,正常方法返回和异常派发机制。这些都存在帧数据区。当方法要访问常量池时,会通过帧数据区的指向常量池的指针来访问。
        如果方法正常返回,jvm会将返回值压入调用方法的栈帧的操作数栈中。
        为了处理异常,帧数据区还保存了一个对异常表的引用。

本地方法栈

        主要是为JVM使用本地方法而服务的。线程调用本地方法时,JVM会保持java栈不变,不再压入新的栈帧。在sun的hotspotJVM中,本地方法栈和栈是合二为一的。

垃圾回收

放在下一篇文章中讨论。

1 条评论: