【转】java虚拟机机制总结

本文旨在给所有希望了解JVM(Java Virtual Machine)的同学一个概念性的入门,主要介绍了JVM的组成部分以及它们内部工作的机制和原理。当然本文只是一个简单的入门,不会涉及过多繁杂的参数和配置,感兴趣的同学可以做更深入的研究,在研究JVM的过程中会发现,其实JVM本身就是一个计算机体系结构,很多原理和我们平时的硬件、微机原理、操作系统都有十分相似的地方,所以学习JVM本身也是加深自我对计算机结构认识的一个很好的途径。

另外需要注意的是,虽然平时我们用的大多是Sun(现已被Oracle收购)JDK提供的JVM,但是JVM本身是一个规范,所以可以有多种实现,除了Hotspot外,还有诸如Oracle的JRockit、IBM的J9也都是非常有名的JVM。

一、JVM结构

下图展示了JVM的主要结构:

可以看出,JVM主要由类加载器子系统、运行时数据区(内存空间)、执行引擎以及与本地方法接口等组成。其中运行时数据区又由方法区、堆、Java栈、PC寄存器、本地方法栈组成。

从上图中还可以看出,在内存空间中方法区和堆是所有Java线程共享的,而Java栈、本地方法栈、PC寄存器则由每个线程私有,这会引出一些问题,后文会进行具体讨论。

众所周知,Java语言具有跨平台的特性,这也是由JVM来实现的。更准确地说,是Sun利用JVM在不同平台上的实现帮我们把平台相关性的问题给解决了,这就好比是HTML语言可以在不同厂商的浏览器上呈现元素(虽然某些浏览器在对W3C标准的支持上还有一些问题)。同时,Java语言支持通过JNI(Java Native Interface)来实现本地方法的调用,但是需要注意到,如果你在Java程序用调用了本地方法,那么你的程序就很可能不再具有跨平台性,即本地方法会破坏平台无关性。

二、类加载器子系统(Class Loader)

类加载器子系统负责加载编译好的.class字节码文件,并装入内存,使JVM可以实例化或以其它方式使用加载后的类。JVM的类加载子系统支持在运行时的动态加载,动态加载的优点有很多,例如可以节省内存空间、灵活地从网络上加载类,动态加载的另一好处是可以通过命名空间的分隔来实现类的隔离,增强了整个系统的安全性。

1、ClassLoader的分类:

a.启动类加载器(BootStrap Class Loader):负责加载rt.jar文件中所有的Java类,即Java的核心类都是由该ClassLoader加载。在Sun JDK中,这个类加载器是由C++实现的,并且在Java语言中无法获得它的引用。

b.扩展类加载器(Extension Class Loader):负责加载一些扩展功能的jar包。

c.系统类加载器(System Class Loader):负责加载启动参数中指定的Classpath中的jar包及目录,通常我们自己写的Java类也是由该ClassLoader加载。在Sun JDK中,系统类加载器的名字叫AppClassLoader。

d.用户自定义类加载器(User Defined Class Loader):由用户自定义类的加载规则,可以手动控制加载过程中的步骤。

2、ClassLoader的工作原理

类加载分为装载、链接、初始化三步。

a.装载

通过类的全限定名和ClassLoader加载类,主要是将指定的.class文件加载至JVM。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。

在内存中,ClassLoader实例和类的实例都位于堆中,它们的类信息都位于方法区。

装载过程采用了一种被称为“双亲委派模型(Parent Delegation Model)”的方式,当一个ClassLoader要加载类时,它会先请求它的双亲ClassLoader(其实这里只有两个ClassLoader,所以称为父ClassLoader可能更容易理解)加载类,而它的双亲ClassLoader会继续把加载请求提交再上一级的ClassLoader,直到启动类加载器。只有其双亲ClassLoader无法加载指定的类时,它才会自己加载类。

双亲委派模型是JVM的第一道安全防线,它保证了类的安全加载,这里同时依赖了类加载器隔离的原理:不同类加载器加载的类之间是无法直接交互的,即使是同一个类,被不同的ClassLoader加载,它们也无法感知到彼此的存在。这样即使有恶意的类冒充自己在核心包(例如java.lang)下,由于它无法被启动类加载器加载,也造成不了危害。

由此也可见,如果用户自定义了类加载器,那就必须自己保障类加载过程中的安全。

b.链接

链接的任务是把二进制的类型信息合并到JVM运行时状态中去。

链接分为以下三步:

a.验证:校验.class文件的正确性,确保该文件是符合规范定义的,并且适合当前JVM使用。

b.准备:为类分配内存,同时初始化类中的静态变量赋值为默认值。

c.解析(可选):主要是把类的常量池中的符号引用解析为直接引用,这一步可以在用到相应的引用时再解析。

c.初始化

初始化类中的静态变量,并执行类中的static代码、构造函数。

JVM规范严格定义了何时需要对类进行初始化:

a、通过new关键字、反射、clone、反序列化机制实例化对象时。

b、调用类的静态方法时。

c、使用类的静态字段或对其赋值时。

d、通过反射调用类的方法时。

e、初始化该类的子类时(初始化子类前其父类必须已经被初始化)。

f、JVM启动时被标记为启动类的类(简单理解为具有main方法的类)。

三、Java栈(Java Stack)

Java栈由栈帧组成,一个帧对应一个方法调用。调用方法时压入栈帧,方法返回时弹出栈帧并抛弃。Java栈的主要任务是存储方法参数、局部变量、中间运算结果,并且提供部分其它模块工作需要的数据。前面已经提到Java栈是线程私有的,这就保证了线程安全性,使得程序员无需考虑栈同步访问的问题,只有线程本身可以访问它自己的局部变量区。

它分为三部分:局部变量区、操作数栈、帧数据区。

1、局部变量区

局部变量区是以字长为单位的数组,在这里,byte、short、char类型会被转换成int类型存储,除了long和double类型占两个字长以外,其余类型都只占用一个字长。特别地,boolean类型在编译时会被转换成int或byte类型,boolean数组会被当做byte类型数组来处理。局部变量区也会包含对象的引用,包括类引用、接口引用以及数组引用。

局部变量区包含了方法参数和局部变量,此外,实例方法隐含第一个局部变量this,它指向调用该方法的对象引用。对于对象,局部变量区中永远只有指向堆的引用。

2、操作数栈

操作数栈也是以字长为单位的数组,但是正如其名,它只能进行入栈出栈的基本操作。在进行计算时,操作数被弹出栈,计算完毕后再入栈。

3、帧数据区

帧数据区的任务主要有:

a.记录指向类的常量池的指针,以便于解析。

b.帮助方法的正常返回,包括恢复调用该方法的栈帧,设置PC寄存器指向调用方法对应的下一条指令,把返回值压入调用栈帧的操作数栈中。

c.记录异常表,发生异常时将控制权交由对应异常的catch子句,如果没有找到对应的catch子句,会恢复调用方法的栈帧并重新抛出异常。

局部变量区和操作数栈的大小依照具体方法在编译时就已经确定。调用方法时会从方法区中找到对应类的类型信息,从中得到具体方法的局部变量区和操作数栈的大小,依此分配栈帧内存,压入Java栈。

四、本地方法栈(Native Method Stack)

本地方法栈类似于Java栈,主要存储了本地方法调用的状态。在Sun JDK中,本地方法栈和Java栈是同一个。

五、方法区(Method Area)

类型信息和类的静态变量都存储在方法区中。方法区中对于每个类存储了以下数据:

a.类及其父类的全限定名(java.lang.Object没有父类)

b.类的类型(Class or Interface)

c.访问修饰符(public, abstract, final)

d.实现的接口的全限定名的列表

e.常量池

f.字段信息

g.方法信息

h.静态变量

i.ClassLoader引用

j.Class引用

可见类的所有信息都存储在方法区中。由于方法区是所有线程共享的,所以必须保证线程安全,举例来说,如果两个类同时要加载一个尚未被加载的类,那么一个类会请求它的ClassLoader去加载需要的类,另一个类只能等待而不会重复加载。

此外为了加快调用方法的速度,通常还会为每个非抽象类创建私有的方法表,方法表是一个数组,存放了实例可能被调用的实例方法的直接引用。方法表对于多态有非常重要的意义,具体可以参照《浅谈多态机制的意义及实现》一文中“多态的实现”一节。

在Sun JDK中,方法区对应了持久代(Permanent Generation),默认最小值为16MB,最大值为64MB。

六、堆(Heap)

堆用于存储对象实例以及数组值。堆中有指向类数据的指针,该指针指向了方法区中对应的类型信息。堆中还可能存放了指向方法表的指针。堆是所有线程共享的,所以在进行实例化对象等操作时,需要解决同步问题。此外,堆中的实例数据中还包含了对象锁,并且针对不同的垃圾收集策略,可能存放了引用计数或清扫标记等数据。

在堆的管理上,Sun JDK从1.2版本开始引入了分代管理的方式。主要分为新生代、旧生代。分代方式大大改善了垃圾收集的效率。

1、新生代(New Generation)

大多数情况下新对象都被分配在新生代中,新生代由Eden Space和两块相同大小的Survivor Space组成,后两者主要用于Minor GC时的对象复制(Minor GC的过程在此不详细讨论)。

JVM在Eden Space中会开辟一小块独立的TLAB(Thread Local Allocation Buffer)区域用于更高效的内存分配,我们知道在堆上分配内存需要锁定整个堆,而在TLAB上则不需要,JVM在分配对象时会尽量在TLAB上分配,以提高效率。

2、旧生代(Old Generation/Tenuring Generation)

在新生代中存活时间较久的对象将会被转入旧生代,旧生代进行垃圾收集的频率没有新生代高。

七、执行引擎

执行引擎是JVM执行Java字节码的核心,执行方式主要分为解释执行、编译执行、自适应优化执行、硬件芯片执行方式。

JVM的指令集是基于栈而非寄存器的,这样做的好处在于可以使指令尽可能紧凑,便于快速地在网络上传输(别忘了Java最初就是为网络设计的),同时也很容易适应通用寄存器较少的平台,并且有利于代码优化,由于Java栈和PC寄存器是线程私有的,线程之间无法互相干涉彼此的栈。每个线程拥有独立的JVM执行引擎实例。

JVM指令由单字节操作码和若干操作数组成。对于需要操作数的指令,通常是先把操作数压入操作数栈,即使是对局部变量赋值,也会先入栈再赋值。注意这里是“通常”情况,之后会讲到由于优化导致的例外。

1、解释执行

和一些动态语言类似,JVM可以解释执行字节码。Sun JDK采用了token-threading的方式,感兴趣的同学可以深入了解一下。

解释执行中有几种优化方式:

a.栈顶缓存

将位于操作数栈顶的值直接缓存在寄存器上,对于大部分只需要一个操作数的指令而言,就无需再入栈,可以直接在寄存器上进行计算,结果压入操作数站。这样便减少了寄存器和内存的交换开销。

b.部分栈帧共享

被调用方法可将调用方法栈帧中的操作数栈作为自己的局部变量区,这样在获取方法参数时减少了复制参数的开销。

c.执行机器指令

在一些特殊情况下,JVM会执行机器指令以提高速度。

2、编译执行

为了提升执行速度,Sun JDK提供了将字节码编译为机器指令的支持,主要利用了JIT(Just-In-Time)编译器在运行时进行编译,它会在第一次执行时编译字节码为机器码并缓存,之后就可以重复利用。Oracle JRockit采用的是完全的编译执行。

3、自适应优化执行

自适应优化执行的思想是程序中10%~20%的代码占据了80%~90%的执行时间,所以通过将那少部分代码编译为优化过的机器码就可以大大提升执行效率。自适应优化的典型代表是Sun的Hotspot VM,正如其名,JVM会监测代码的执行情况,当判断特定方法是瓶颈或热点时,将会启动一个后台线程,把该方法的字节码编译为极度优化的、静态链接的C++代码。当方法不再是热区时,则会取消编译过的代码,重新进行解释执行。

自适应优化不仅通过利用小部分的编译时间获得大部分的效率提升,而且由于在执行过程中时刻监测,对内联代码等优化也起到了很大的作用。由于面向对象的多态性,一个方法可能对应了很多种不同实现,自适应优化就可以通过监测只内联那些用到的代码,大大减少了内联函数的大小。

Sun JDK在编译上采用了两种模式:Client和Server模式。前者较为轻量级,占用内存较少。后者的优化程序更高,占用内存更多。

在Server模式中会进行对象的逃逸分析,即方法中的对象是否会在方法外使用,如果被其它方法使用了,则该对象是逃逸的。对于非逃逸对象,JVM会在栈上直接分配对象(所以对象不一定是在堆上分配的),线程获取对象会更加快速,同时当方法返回时,由于栈帧被抛弃,也有利于对象的垃圾收集。Server模式还会通过分析去除一些不必要的同步,感兴趣的同学可以研究一下Sun JDK 6引入的Biased Locking机制。

此外,执行引擎也必须保证线程安全性,因而JMM(Java Memory Model)也是由执行引擎确保的。

时间: 2024-10-25 14:08:08

【转】java虚拟机机制总结的相关文章

重读《深入理解Java虚拟机》五、虚拟机如何执行字节码?虚拟机执行引擎的工作机制

Class文件二进制字符流通过类加载器和虚拟机加载到内存(方法区)完成在内存上的布局和初始化后,虚拟机字节码执行引擎就可以执行相关代码实现程序所定义的功能.虚拟机执行引擎执行的对象是方法(均特指非本地方法),方法是 着一个程序所定义的一个功能的载体,实现预定的业务功能或者特定的功能等. Java虚拟机内存内针对方法的执行专门划分了一个区域即虚拟机栈.虚拟机栈内通过栈帧结构来存储调用方法和执行方法需要的局部变量,操作数栈.方法返回值等,通过栈帧的出入栈来表示方法的执行顺序. 1.栈帧结构:虚拟机内

Java虚拟机类加载机制

原文出处: 朱小厮 看到这个题目,很多人会觉得我写我的java代码,至于类,JVM爱怎么加载就怎么加载,博主有很长一段时间也是这么认为的.随着编程经验的日积月累,越来越感觉到了解虚拟机相关要领的重要性.闲话不多说,老规矩,先来一段代码吊吊胃口. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public cla

Java虚拟机5:Java垃圾回收(GC)机制详解

http://www.cnblogs.com/xrq730/p/4836700.html 哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象.那么如何找到这些对象? 1.引用计数法 这个算法 的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1:当引用失效时,计数器值-1.任何时刻计数值为0的对象就是不可能再 被使用的.这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很

【深入理解Java虚拟机】类加载机制

本文内容来源于<深入理解Java虚拟机>一书,非常推荐大家去看一下这本书. 本系列其他文章: [深入理解Java虚拟机]Java内存区域模型.对象创建过程.常见OOM [深入理解Java虚拟机]垃圾回收机制 1.类加载机制概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 在java中,类型的加载.连接和初始化过程都是在程序运行期间完成的,这种策略虽然会带来一些性能开销,但是却为jav

Java虚拟机 - 多态性实现机制

[深入Java虚拟机]之五:多态性实现机制--静态分派与动态分派 方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩展能力,使得可以在类运行期间才能确定某些目标方法的直接引用,称为动态连接,也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析.这在前面的"Java内存区域与内存溢出"一文中有提到. 静态解

【深入Java虚拟机】之八:Java垃圾收集机制

转载请注明出处:http://blog.csdn.net/ns_code/article/details/18076173 对象引用 Java中的垃圾回收一般是在Java堆中进行,因为堆中几乎存放了Java中所有的对象实例.谈到Java堆中的垃圾回收,自然要谈到引用.在JDK1.2之前,Java中的引用定义很很纯粹:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用.但在JDK1.2之后,Java对引用的概念进行了扩充,将其分为强引用(Stro

Java虚拟机类加载机制——案例分析

原文出处: 朱小厮 在<Java虚拟机类加载机制>一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的最后留了一个悬念给各位,这里来揭开这个悬念.建议先看完<Java虚拟机类加载机制>这篇再来看这个,印象会比较深刻,如若不然,也没什么关系~~下面是程序代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package jvm.cla

Java虚拟机学习5、Java垃圾回收(GC)机制详解

哪些内存需要回收? 哪些内存需要回收是垃圾回收机制第一个要考虑的问题,所谓“要回收的垃圾”无非就是那些不可能再被任何途径使用的对象.那么如何找到这些对象? 1.引用计数法 这个算法的实现是,给对象中添加一个引用计数器,每当一个地方引用这个对象时,计数器值+1:当引用失效时,计数器值-1.任何时刻计数值为0的对象就是不可能再被使用的.这种算法使用场景很多,但是,Java中却没有使用这种算法,因为这种算法很难解决对象之间相互引用的情况.看一段代码 1 /** 2 * 虚拟机参数:-verbose:g

Java虚拟机的类载入机制

Java虚拟机类载入过程是把Class类文件载入到内存.并对Class文件里的数据进行校验.转换解析和初始化,终于形成能够被虚拟机直接使用的java类型的过程. 在载入阶段,java虚拟机须要完毕下面3件事: a.通过一个类的全限定名来获取定义此类的二进制字节流. b.将定义类的二进制字节流所代表的静态存储结构转换为方法区的执行时数据结构. c.在java堆中生成一个代表该类的java.lang.Class对象,作为方法区数据的訪问入口. Java虚拟机的类载入是通过类载入器实现的, Java中