java虚拟机机制简析

JVM作用,java源代码编译成class文件,并不是操作系统识别执行的二进制指令,通过java虚拟机(JVM)识别并解释、运行,转化为操作系统识别的指令。
JVM基本结构图
JVM是一种解释执行class文件的规范技术;是一个想象中的机器,通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器、相应的指令系统等。
JVM历史,目前使用较多的是Sun公司的JDK,从JDK1.2到JDK1.6默认使用的虚拟机是HotSpot,2009年,Oracle公司收购Sun,加上之前收购的EBA公司,Oracle公司拥有三大虚拟机中的两个:HotSpot和JRockit.Oracle有意整合两大虚拟机,不过在最新的JDK1.8还是使用的HotSpot,本文所解析的也是HotSpot.
一、类加载过程
编写好的java代码,编译成为class文件,虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。以下是类加载过程:
1 装载:查找和导入Class文件;具体包括
(a) 通过一个类的全限定名来获取定义此类的二进制字节流;
(b) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
(c) 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
2 链接:把类的二进制数据合并到JRE中;
(a)校验:检查载入Class文件数据的正确性;
(b)准备:给类的静态变量分配存储空间;
(c)解析:将符号引用转成直接引用;
3 初始化:对类的静态变量,静态代码块执行初始化操作.类初始化的过程可看博客《类加载过程实例分析》
类加载器的启动过程:
1、寻找jre目录,寻找jvm.dll,并初始化JVM;
2、产生一个Bootstrap Loader(启动类加载器);将存放于\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用
3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为Bootstrap Loader。将\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。
4、Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为Extended Loader。
5、最后由AppClass Loader加载类。负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。
当一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。(双亲委派模型)
好处:保护了源码。如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
二、内存空间的分配
1. 程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。每个程序计数器只用来记录一个线程的行号。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2. 虚拟机栈(JVM Stack):JVM栈生命周期与线程相同,一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。即虚拟机栈描述的是Java方法执行的内存模型。
通常划分的栈内存指的是虚拟机栈中的局部变量表部分。局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型)、返回地址等,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
  需要注意的是,局部变量表是在编译时就已经确定 好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。
  虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多 数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,知道内存不足,此时,会抛出OutOfMemoryError(内存溢出)。
3. 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与
虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
4. 堆,对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
  Java堆是垃圾收集器管理的主要区域,现在收集器基本都是采用的分代回收算法。分代回收算法的过程:  
1)新生成的对象在Eden区完成内存分配
2)当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收。(为什么是eden+1survivor:两个survivor中始终有一个survivor是空的,空的那个被标记成To Survivor)
3)minorGC时,Eden中不是垃圾的对象被放入到空的survivor(to),另一个survivor(from)里不是垃圾的对象会放到这里,原来的from和Ede被清空,同时form被称为to,原来的to被称为from。
4)当做第3步的时候,如果发现存放对象的那个survivor满了,则这些对象被copy到老年代,或者survivor区没有满,但是有些对象的年龄够老(通过XX:MaxTenuringThreshold参数来设置年龄阈值),也会被放入老年代。
5)当Old区被放满的之后,进行完整的垃圾回收,即 Full GC
6)Full GC时,整理的是Old Generation里的对象(有待证实:把存活的对象放入到Permanent Generation里。)
4. 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。对于习惯在HotSpot虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory来实现方法区的规划了。
  Java虚拟机规范对这个区域的限制非常宽松,除了和Java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这个区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是有必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。 根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
5. 运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。 运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的intern()方法。 既然运行时常量池是方法区的一部分,自然会受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
6. JVM寄存器: 所有的CPU均包含用于保存系统状态和处理器所需信息的寄存器组。通过寄存器可以得到信息而不必对栈或内存进行访问,从而有利于提高运行速度。然而,如果虚拟机的寄存器比实际CPU的寄存器多,在运行虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器。针对这种情况,JVM只设置了4个最常用的的寄存器:PC程序计数器、optop操作数栈顶指针,frame当前执行环境指针,vars指向当前执行环境中第一个局部变量的指针,所有的寄存器均为32位。pc寄存器用于记录程序的执行。optop、frame、vars用于记录指向java栈区的指针。
7. 分析实例

package com.sinojava;
public class jyMain {
    public static void main(String[] args) {
        Sample sampleOject=new Sample("测试");
        sampleOject.printName();
    }
};

package com.sinojava;
public class Sample {
    private String name;
    public Sample(String name){
        this.name=name;
    }
    public void printName(){
        System.out.println(name);
    }
}

JVM启动时,读取jyMain的class字节码信息,加载到方法区。接着,主线程中的main方法会依据自身的程序计数器执行它的指令。第一条语句就是new Sample对象,会在栈存放一个指向Sample对象的引用sampleOject,即引用变量,堆存放Sample对象实例和对象类型数据的引用,方法区中存放对象类型数据,如类信息等。通过sampleOject可访问到方法区的方法printName()方法,执行printName()方法包含的指令。调用这个方法时,JVM会往栈里压入一个栈帧,栈帧中存放了局部变量数组、操作数栈、常量池的引用。局部变量数组包含了方法所属对象的引用this、传递给方法的参数”测试”、局部变量name;操作数栈存放临时数据与局部变量数组交换数据。方法返回时弹出栈帧,操作数栈也就没了。
参考资料:
http://blog.csdn.net/witsmakemen/article/details/38404423
http://hxraid.iteye.com/blog/428891
http://hxraid.iteye.com/blog/747625
http://blog.csdn.net/witsmakemen/article/details/28600127/

       后续更新中------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值