Java虚拟机(JVM)是可运行Java代码的假想计算机。
只要根据JVM规格描述将解释器移植到特定的计算机上,就能保证经过编译的任何Java代码能够在该系统上运行。
本文首先简要介绍从Java文件的编译到最终执行的过程,随后对JVM规格描述作一说明。
一Java源文件的编译、下载、解释和执行
Java应用程序的开发周期包括编译、下载、解释和执行几个部分。
Java编译程序将Java源程序翻译为JVM可执行代码字节码。
这一编译过程同C/C++的编译有些不同。
当C编译器编译生成一个对象的代码时,该代码是为在某一特定硬件平台运行而产生的。
因此,在编译过程中,编译程序通过查表将所有对符号的引用转换为特定的内存偏移量,以保证程序运行。
Java编译器却不将对变量和方法的引用编译为数值引用,也不确定程序执行过程中的内存布局,而是将这些符号引用信息保留在字节码中,由解释器在运行过程中创立内存布局,然后再通过查表来确定一个方法所在的地址。
这样就有效的保证了Java的可移植性和安全性。
运行JVM字节码的工作是由解释器来完成的。
解释执行过程分三部进行:代码的装入、代码的校验和代码的执行。
装入代码的工作由"类装载器"(classloader)完成。
类装载器负责装入运行一个程序需要的所有代码,这也包括程序代码中的类所继承的类和被其调用的类。
当类装载器装入一个类时,该类被放在自己的名字空间中。
除了通过符号引用自己名字空间以外的类,类之间没有其他办法可以影响其他类。
在本台计算机上的所有类都在同一地址空间内,而所有从外部引进的类,都有一个自己独立的名字空间。
这使得本地类通过共享相同的名字空间获得较高的运行效率,同时又保证它们与从外部引进的类不会相互影响。
当装入了运行程序需要的所有类后,解释器便可确定整个可执行程序的内存布局。
解释器为符号引用同特定的地址空间建立对应关系及查询表。
通过在这一阶段确定代码的内存布局,Java很好地解决了由超类改变而使子类崩溃的问题,同时也防止了代码对地址的非法访问。
随后,被装入的代码由字节码校验器进行检查。
校验器可发现 *** 作数栈溢出,非法数据类型转化等多种错误。
通过校验后,代码便开始执行了。
Java字节码的执行有两种方式:
1即时编译方式:解释器先将字节码编译成机器码,然后再执行该机器码。
2解释执行方式:解释器通过每次解释并执行一小段代码来完成Java字节码程序的所有 *** 作。
通常采用的是第二种方法。
由于JVM规格描述具有足够的灵活性,这使得将字节码翻译为机器代码的工作
具有较高的效率。
对于那些对运行速度要求较高的应用程序,解释器可将Java字节码即时编译为机器码,从而很好地保证了Java代码的可移植性和高性能。
二JVM规格描述
JVM的设计目标是提供一个基于抽象规格描述的计算机模型,为解释程序开发人员提很好的灵活性,同时也确保Java代码可在符合该规范的任何系统上运行。
JVM对其实现的某些方面给出了具体的定义,特别是对Java可执行代码,即字节码(Bytecode)的格式给出了明确的规格。
这一规格包括 *** 作码和 *** 作数的语法和数值、标识符的数值表示方式、以及Java类文件中的Java对象、常量缓冲池在JVM的存储映象。
这些定义为JVM解释器开发人员提供了所需的信息和开发环境。
Java的设计者希望给开发人员以随心所欲使用Java的自由。
JVM定义了控制Java代码解释执行和具体实现的五种规格,它们是:
JVM指令系统
JVM寄存器
JVM栈结构
JVM碎片回收堆
JVM存储区
21JVM指令系统
JVM指令系统同其他计算机的指令系统极其相似。
Java指令也是由 *** 作码和 *** 作数两部分组成。
*** 作码为8位二进制数, *** 作数进紧随在 *** 作码的后面,其长度根据需要而不同。
*** 作码用于指定一条指令 *** 作的性质(在这里我们采用汇编符号的形式进行说明),如iload表示从存储器中装入一个整数,anewarray表示为一个新数组分配空间,iand表示两个整数的"与",ret用于流程控制,表示从对某一方法的调用中返回。
当长度大于8位时, *** 作数被分为两个以上字节存放。
JVM采用了"bigendian"的编码方式来处理这种情况,即高位bits存放在低字节中。
这同Motorola及其他的RISCCPU采用的编码方式是一致的,而与Intel采用的"littleendian"的编码方式即低位bits存放在低位字节的方法不同。
Java指令系统是以Java语言的实现为目的设计的,其中包含了用于调用方法和监视多先程系统的指令。
Java的8位 *** 作码的长度使得JVM最多有256种指令,目前已使用了160多种 *** 作码。
22JVM指令系统
所有的CPU均包含用于保存系统状态和处理器所需信息的寄存器组。
如果虚拟机定义较多的寄存器,便可以从中得到更多的信息而不必对栈或内存进行访问,这有利于提高运行速度。
然而,如果虚拟机中的寄存器比实际CPU的寄存器多,在实现虚拟机时就会占用处理器大量的时间来用常规存储器模拟寄存器,这反而会降低虚拟机的效率。
针对这种情况,JVM只设置了4个最为常用的寄存器。
它们是:
pc程序计数器
optop *** 作数栈顶指针
frame当前执行环境指针
vars指向当前执行环境中第一个局部变量的指针
所有寄存器均为32位。
pc用于记录程序的执行。
optop,frame和vars用于记录指向Java栈区的指针。
23JVM栈结构
作为基于栈结构的计算机,Java栈是JVM存储信息的主要方法。
当JVM得到一个Java字节码应用程序后,便为该代码中一个类的每一个方法创建一个栈框架,以保存该方法的状态信息。
每个栈框架包括以下三类信息:
局部变量
执行环境
*** 作数栈
局部变量用于存储一个类的方法中所用到的局部变量。
vars寄存器指向该变量表中的第一个局部变量。
执行环境用于保存解释器对Java字节码进行解释过程中所需的信息。
它们是:上次调用的方法、局部变量指针和 *** 作数栈的栈顶和栈底指针。
执行环境是一个执行一个方法的控制中心。
例如:如果解释器要执行iadd(整数加法),首先要从frame寄存器中找到当前执行环境,而后便从执行环境中找到 *** 作数栈,从栈顶d出两个整数进行加法运算,最后将结果压入栈顶。
*** 作数栈用于存储运算所需 *** 作数及运算的结果。
24JVM碎片回收堆
Java类的实例所需的存储空间是在堆上分配的。
解释器具体承担为类实例分配空间的工作。
解释器在为一个实例分配完存储空间后,便开始记录对该实例所占用的内存区域的使用。
一旦对象使用完毕,便将其回收到堆中。
在Java语言中,除了new语句外没有其他方法为一对象申请和释放内存。
对内存进行释放和回收的工作是由Java运行系统承担的。
这允许Java运行系统的设计者自己决定碎片回收的方法。
在SUN公司开发的Java解释器和HotJava环境中,碎片回收用后台线程的方式来执行。
这不但为运行系统提供了良好的性能,而且使程序设计人员摆脱了自己控制内存使用的风险。
25JVM存储区
JVM有两类存储区:常量缓冲池和方法区。
常量缓冲池用于存储类名称、方法和字段名称以及串常量。
方法区则用于存储Java方法的字节码。
对于这两种存储区域具体实现方式在JVM规格中没有明确规定。
这使得Java应用程序的存储布局必须在运行过程中确定,依赖于具体平台的实现方式。
JVM是为Java字节码定义的一种独立于具体平台的规格描述,是Java平 立性的基础。
目前的JVM还存在一些限制和不足,有待于进一步的完善,但无论如何,JVM的思想是成功的。
对比分析:如果把Java原程序想象成我们的C++原程序,Java原程序编译后生成的字节码就相当于C++原程序编译后的80x86的机器码(二进制程序文件),JVM虚拟机相当于80x86计算机系统,Java解释器相当于80x86CPU。
在80x86CPU上运行的是机器码,在Java解释器上运行的是Java字节码。
Java解释器相当于运行Java字节码的“CPU”,但该“CPU”不是通过硬件实现的,而是用软件实现的。
Java解释器实际上就是特定的平台下的一个应用程序。
只要实现了特定平台下的解释器程序,Java字节码就能通过解释器程序在该平台下运行,这是Java跨平台的根本。
当前,并不是在所有的平台下都有相应Java解释器程序,这也是Java并不能在所有的平台下都能运行的原因,它只能在已实现了Java解释器程序的平台下运行。
JVM是java的核心和基础,在java编译器和os平台之间的虚拟处理器。它是一种基于下层的 *** 作系统和硬件平台并利用软件方法来实现的抽象的计算机,可以在上面执行java的字节码程序。
java编译器只需面向JVM,生成JVM能理解的代码或字节码文件。Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。
JVM执行程序的过程 :
I加载class文件
II管理并分配内存
III执行垃圾收集
JRE(java运行时环境)包含JVM的java程序的运行环境
JVM是Java程序运行的容器,但是他同时也是 *** 作系统的一个进程,因此他也有他自己的运行的生命周期,也有自己的代码和数据空间。
JVM在整个jdk中处于最底层,负责与 *** 作系统的交互,用来屏蔽 *** 作系统环境,提供一个完整的Java运行环境,因此也叫虚拟计算机 *** 作系统装入JVM是通过jdk中Javaexe来完成,通过下面4步来完成JVM环境。
1创建JVM装载环境和配置
2装载JVMdll
3初始化JVMdll并挂接到JNIENV(JNI调用接口)实例
4调用JNIEnv实例装载并处理class类。
内存屏障 又称内存栅栏 是一组处理器指令 用于实现对内存 *** 作的顺序限制 本文假定读者已经充分掌握了相关概念和Java内存模型 不讨论并发互斥 并行机制和原子性 内存屏障用来实现并发编程中称为可见性(visibility)的同样重要的作用
内存屏障为何重要?
对主存的一次访问一般花费硬件的数百次时钟周期 处理器通过缓存(caching)能够从数量级上降低内存延迟的成本这些缓存为了性能重新排列待定内存 *** 作的顺序 也就是说 程序的读写 *** 作不一定会按照它要求处理器的顺序执行 当数据是不可变的 同时/或者数据限制在线程范围内 这些优化是无害的
如果把这些优化与对称多处理(symmetric multi processing)和共享可变状态(shared mutable state)结合 那么就是一场噩梦 当基于共享可变状态的内存 *** 作被重新排序时 程序可能行为不定 一个线程写入的数据可能被其他线程可见 原因是数据 写入的顺序不一致 适当的放置内存屏障通过强制处理器顺序执行待定的内存 *** 作来避免这个问题
内存屏障的协调作用
内存屏障不直接由JVM暴露 相反它们被JVM插入到指令序列中以维持语言层并发原语的语义 我们研究几个简单Java程序的源代码和汇编指令 首先快速看一下Dekker算法中的内存屏障 该算法利用volatile变量协调两个线程之间的共享资源访问
请不要关注该算法的出色细节 哪些部分是相关的?每个线程通过发信号试图进入代码第一行的关键区域 如果线程在第三行意识到冲突(两个线程都要访问) 通 过turn变量的 *** 作来解决 在任何时刻只有一个线程可以访问关键区域
// code run by first thread // code run by second thread
intentFirst = true; intentSecond = true;
while (intentSecond) while (intentFirst) // volatile read
if (turn != ) { if (turn != ) { // volatile read
intentFirst = false; intentSecond = false;
while (turn != ) {} while (turn != ) {}
intentFirst = true; intentSecond = true;
} }
criticalSection(); criticalSection();
turn = ; turn = ; // volatile write
intentFirst = false; intentSecond = false; // volatile write
硬件优化可以在没有内存屏障的情况下打乱这段代码 即使编译器按照程序员的想法顺序列出所有的内存 *** 作 考虑第三 四行的两次顺序volatile读 *** 作 每一个线程检查其他线程是否发信号想进入关键区域 然后检查轮到谁 *** 作了 考虑第 行的两次顺序写 *** 作 每一个线程把访问权释放给其他线程 然后撤销自己访问关键区域的意图 读线程应该从不期望在其他线程撤销访问意愿后观察到其他线程对turn变量的写 *** 作 这是个灾难
但是如果这些变量没有 volatile修饰符 这的确会发生!例如 没有volatile修饰符 第二个线程在第一个线程对turn执行写 *** 作(倒数第二行)之前可能会观察到 第一个线程对intentFirst(倒数第一行)的写 *** 作 关键词volatile避免了这种情况 因为它在对turn变量的写 *** 作和对 intentFirst变量的写 *** 作之间创建了一个先后关系 编译器无法重新排序这些写 *** 作 如果必要 它会利用一个内存屏障禁止处理器重排序 让我们来 看看一些实现细节
PrintAssembly HotSpot选项是JVM的一个诊断标志 允许我们获取JIT编译器生成的汇编指令 这需要最新的OpenJDK版本或者新HotSpot update 或者更高版本 通过需要一个反编译插件 Kenai项目提供了用于Solaris Linux和BSD的插件二进制文件 hsdis是另 一款可以在Windows通过源码构建的插件
两次顺序读 *** 作的第一次(第三行)的汇编指令如下 指令流基于Itanium 多处理硬件 JDK update 本文的所有指令流都在左手边以行号标记 相关的读 *** 作 写 *** 作和内存屏障指令都以粗体标记 建议读者不要沉迷于每一行指令
x de c: adds r = r ;; ;
x de a : ld acq r =[r ];; ; b a a
x de a : nop m x ; c
x de ac: sxt r r =r ;; ;
x de b : cmp eq p p = r ; c
x de b : nop i x ;
x de bc: nd dpnt many x de ;
简短的指令流其实内容丰富 第一次volatile位于第二行 Java内存模型确保了JVM会在第二次读 *** 作之前将第一次读 *** 作交给处理器 也就是按照 程序的顺序 但是这单单一行指令是不够的 因为处理器仍然可以自由乱序执行这些 *** 作 为了支持Java内存模型的一致性 JVM在第一次读 *** 作上添加了注解ld acq 也就是 载入获取 (load acquire) 通过使用ld acq 编译器确保第二行的读 *** 作在接下来的读 *** 作之前完成 问题就解决了
请注意这影响了读 *** 作 而不是写 内存屏障强制读或写 *** 作顺序限制不是单向的 强制读和写 *** 作顺序限制的内存屏障是双向的 类似于双向开的栅栏 使用ld acq就是单向内存屏障的例子
一致性具有两面性 如果一个读线程在两次读 *** 作之间插入了内存屏障而另外一个线程没有在两次写 *** 作之间添加内存屏障又有什么用呢?线程为了协调 必须同时 遵守这个协议 就像网络中的节点或者团队中的成员 如果某个线程破坏了这个约定 那么其他所有线程的努力都白费 Dekker算法的最后两行代码的汇编指令应该插入一个内存屏障 两次volatile写之间
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX:CompileCommand=print WriterReader write WriterReader
x de c : adds r = r ;; ; b
x de c : st rel [r ]=r ;
x de cc: adds r = r ;; ;
x de d : st rel [r ]=r ; a
x de d : mf ;
x de dc: nop i x ;; ;
x de e : mov r =r ;
x de e : mov ret b =r x de e
x de ec: mov i ar pfs=r ; aa
x de f : mov r =r ;
这里我们可以看到在第四行第二次写 *** 作被注解了一个显式内存屏障 通过使用st rel 即 存储释放 (store release) 编译器确保第一次写 *** 作在第二次写 *** 作之前完成 这就完成了两边的约定 因为第一次写 *** 作在第二次写 *** 作之前发生
st rel屏障是单向的 就像ld acq一样 但是在第五行编译器设置了一个双向内存屏障 mf指令 或者称为 内存栅栏 是Itanium 指令集中的完整栅栏 笔者认为是多余的
内存屏障是特定于硬件的
本文不想针对所有内存屏障做一综述 这将是一件不朽的功绩 但是 重要的是认识到这些指令在不同的硬件体系中迥异 下面的指令是连续写 *** 作在多处理 Intel Xeon硬件上编译的结果 本文后面的所有汇编指令除非特殊声明否则都出自于Intel Xeon
x f c: push %ebp ;
x f d: sub $ x %esp ; ec
x f : mov $ x c %edi ; bf c
x f : movb $ x x a f (%edi) ; c d a af
x f f: mfence ; faef
x f : mov $ x %ebp ; bd
x f : mov $ x d %edx ; ba d
x f c: mov l x a f (%edx) %ebx ; fbe a da af
x f : test %ebx %ebx ; db
x f : jne x f ;
x f : movl $ x x a f (%ebp) ; c d a af
x f : movb $ x x a f (%edi) ; c d a af
x f : mfence ; faef
x f b: add $ x %esp ; c
x f e: pop %ebp ; d
我们可以看到x Xeon在第 行执行两次volatile写 *** 作 第二次写 *** 作后面紧跟着mfence *** 作 显式的双向内存屏障 下面的连续写 *** 作基于SPARC
xfb ecc : ldub [ %l + x ] %l ; e c
xfb ecc : cmp %l ; a e
xfb ecc c: bne pn %icc xfb eccb ;
xfb ecc : nop ;
xfb ecc : st %l [ %l + x ] ; e
xfb ecc : clrb [ %l + x ] ; c c
xfb ecc c: membar #StoreLoad ; e
xfb ecca : sethi %hi( xff fc ) %l ; fcff
xfb ecca : ld [ %l ] %g ; c
xfb ecca : ret ; c e
xfb eccac: restore ; e
我们看到在第五 六行存在两次volatile写 *** 作 第二次写 *** 作后面是一个membar指令 显式的双向内存屏障 x 和SPARC的指令流与Itanium的指令流存在一个重要区别 JVM在x 和SPARC上通过内存屏障跟踪连续写 *** 作 但是在两次写 *** 作之间没有放置内存屏障
另一方面 Itanium的指令流在两次写 *** 作之间存在内存屏障 为何JVM在不同的硬件架构之间表现不一?因为硬件架构都有自己的内 存模型 每一个内存模型有一套一致性保障 某些内存模型 如x 和SPARC等 拥有强大的一致性保障 另一些内存模型 如Itanium PowerPC和Alpha 是一种弱保障
例如 x 和SPARC不会重新排序连续写 *** 作 也就没有必要放置内存屏障 Itanium PowerPC和Alpha将重新排序连续写 *** 作 因此JVM必须在两者之间放置内存屏障 JVM使用内存屏障减少Java内存模型和硬件内存模型之间的距离
隐式内存屏障
显式屏障指令不是序列化内存 *** 作的唯一方式 让我们再看一看Counter类这个例子
class Counter{
static int counter = ;
public static void main(String[] _){
for(int i = ; i < ; i++)
inc();
}
static synchronized void inc(){ counter += ; }
}
Counter类执行了一个典型的读 修改 写的 *** 作 静态counter字段不是volatile的 因为所有三个 *** 作必须要原子可见的 因此 inc 方法是synchronized修饰的 我们可以采用下面的命令编译Counter类并查看生成的汇编指令 Java内存模型确保了synchronized区域的退出和volatile内存 *** 作都是相同的可见性 因此我们应该预料到会有另一个内存屏障
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX: UseBiasedLocking XX:CompileCommand=print Counter inc Counter
x d eda : push %ebp ;
x d eda : mov %esp %ebp ; bec
x d edaa: sub $ x %esp ; ec
x d edad: mov $ x ba %esi ; be ba
x d edb : lea x (%esp) %edi ; d c
x d edb : mov %esi x (%edi) ;
x d edb : mov (%esi) %eax ; b
x d edbb: or $ x %eax ; c
x d edbe: mov %eax (%edi) ;
x d edc : lock cmpxchg %edi (%esi) ; f fb e
x d edc : je x d edda ; f
x d edca: sub %esp %eax ; bc
x d edcc: and $ xfffff %eax ; e f ffff
x d edd : mov %eax (%edi) ;
x d edd : jne x d ee ; f
x d edda: mov $ x ba b %eax ; b b ba
x d eddf: mov x (%eax) %esi ; bb
x d ede : inc %esi ;
x d ede : mov %esi x (%eax) ; b
x d edec: lea x (%esp) %eax ; d
x d edf : mov (%eax) %esi ; b
x d edf : test %esi %esi ; f
x d edf : je x d ee ; f d
x d edfa: mov x (%eax) %edi ; b
x d edfd: lock cmpxchg %esi (%edi) ; f fb
x d ee : jne x d ee f ; f
x d ee : mov %ebp %esp ; be
x d ee : pop %ebp ; d
不出意外 synchronized生成的指令数量比volatile多 第 行做了一次增 *** 作 但是JVM没有显式插入内存屏障 相反 JVM通过在 第 行和第 行cmpxchg的lock前缀一石二鸟 cmpxchg的语义超越了本文的范畴
lock cmpxchg不仅原子性执行写 *** 作 也会刷新等待的读写 *** 作 写 *** 作现在将在所有后续内存 *** 作之前完成 如果我们通过ncurrent atomic AtomicInteger 重构和运行Counter 将看到同样的手段
import ncurrent atomic AtomicInteger;
class Counter{
static AtomicInteger counter = new AtomicInteger( );
public static void main(String[] args){
for(int i = ; i < ; i++)
counter incrementAndGet();
}
}
$ java XX:+UnlockDiagnosticVMOptions XX:PrintAssemblyOptions=hsdis print bytes
XX:CompileCommand=print AtomicInteger incrementAndGet Counter
x f : push %ebp ;
x f : mov %esp %ebp ; bec
x fa: sub $ x %esp ; ec
x fd: jmp x a ; e
x : xchg %ax %ax ;
x : test %eax xb e ; e b
x a: mov x (%ecx) %eax ; b
x d: mov %eax %esi ; bf
x f: inc %esi ;
x : mov $ x a f d %edi ; bfd f a
x : mov x (%edi) %edi ; bbf
x b: mov %ecx %edi ; bf
x d: add $ x %edi ; c
x : lock cmpxchg %esi (%edi) ; f fb
x : mov $ x %eax ; b
x : je x ; f
x f: mov $ x %eax ; b
x : cmp $ x %eax ; f
x : je x ; cb
x : mov %esi %eax ; bc
x b: mov %ebp %esp ; be
x d: pop %ebp ; d
我们又一次在第 行看到了带有lock前缀的写 *** 作 这确保了变量的新值(写 *** 作)会在其他所有后续内存 *** 作之前完成
内存屏障能够避免
JVM非常擅于消除不必要的内存屏障 通常JVM很幸运 因为硬件内存模型的一致性保障强于或者等于Java内存模型 在这种情况下 JVM只是简单地插 入一个no op语句 而不是真实的内存屏障
例如 x 和SPARC内存模型的一致性保障足够强壮以消除读volatile变量时所需的内存屏障 还记得在 Itanium上两次读 *** 作之间的显式单向内存屏障吗?x 上的Dekker算法中连续volatile读 *** 作的汇编指令之间没有任何内存屏障 x 平台上共享内存的连续读 *** 作
x f : mov $ x %ebp ; bd
x f : mov $ x d %edx ; ba d
x f c: mov l x a f (%edx) %ebx ; fbe a da af
x f : test %ebx %ebx ; db
x f : jne x f ;
x f : movl $ x x a f (%ebp) ; c d a af
x f : movb $ x x a f (%edi) ; c d a af
x f : mfence ; faef
x f b: add $ x %esp ; c
x f e: pop %ebp ; d
x f f: test %eax xb ec ; c eb
x f : ret ; c
x f : nopw x (%eax %eax ) ; f f
x f : mov x a f (%ebp) %ebx ; b d d a af
x f : test %edi xb ec ; d c eb
第三行和第十四行存在volatile读 *** 作 而且都没有伴随内存屏障 也就是说 x 和SPARC上的volatile读 *** 作的性能下降对于代码的优 化影响很小 指令本身和常规读 *** 作一样
单向内存屏障本质上比双向屏障性能要好一些 JVM在确保单向屏障即可的情况下会避免使用双向屏障 本文的第一个例子展示了这点 Itanium平台上的 连续两次读 *** 作 入单向内存屏障 如果读 *** 作插入显式双向内存屏障 程序仍然正确 但是延迟比较长
动态编译
静态编译器在构建阶段决定的一切事情 在动态编译器那里都可以在运行时决定 甚至更多 更多信息意味着存在更多机会可以优化 例如 让我们看看JVM在单 处理器运行时如何对待内存屏障 以下指令流来自于通过Dekker算法实现两次连续volatile写 *** 作的运行时编译 程序运行于 x 硬件上的单处理器模式中的VMWare工作站镜像
x b c: push %ebp ;
x b d: sub $ x %esp ; ec
x b : mov $ x c %edi ; bf c
x b : movb $ x x f (%edi) ; c d aaf
x b f: mov $ x %ebp ; bd
x b : mov $ x d %edx ; ba d
x b : mov l x f (%edx) %ebx ; fbe a d aaf
x b : test %ebx %ebx ; db
x b : jne x b ; c
x b : movl $ x x f (%ebp) ; c d aaf
x b : add $ x %esp ; c
x b : pop %ebp ; d
在单处理器系统上 JVM为所有内存屏障插入了一个no op指令 因为内存 *** 作已经序列化了 每一个写 *** 作(第 行)后面都跟着一个屏障 JVM针对原子条件式做了类似的优化 下面的指令流来自于同一 个VMWare镜像的AtomicInteger incrementAndGet动态编译结果
x f : push %ebp ;
x f : mov %esp %ebp ; bec
x fa: sub $ x %esp ; ec
x fd: jmp x a ; e
x : xchg %ax %ax ;
x : test %eax xb b ; bb
x a: mov x (%ecx) %eax ; b
x d: mov %eax %esi ; bf
x f: inc %esi ;
x : mov $ x a f d %edi ; bfd f a
x : mov x (%edi) %edi ; bbf
x b: mov %ecx %edi ; bf
x d: add $ x %edi ; c
x : cmpxchg %esi (%edi) ; fb
x : mov $ x %eax ; b
x : je x ; f
x e: mov $ x %eax ; b
x : cmp $ x %eax ; f
x : je x ; cc
x : mov %esi %eax ; bc
x a: mov %ebp %esp ; be
x c: pop %ebp ; d
注意第 行的cmpxchg指令 之前我们看到编译器通过lock前缀把该指令提供给处理器 由于缺少SMP JVM决定避免这种成本 与静态编译有些不同
结束语
lishixinzhi/Article/program/Java/hx/201311/25723
不是文件,这个是JVM启动时的参数列表。
Properties props=SystemgetProperties(); //系统属性
propsgetProperty("javaversion");具体有那些属性,参照下边。
javaversion Java 运行时环境版本
javavendor Java 运行时环境供应商
javavendorurl Java 供应商的 URL
javahome Java 安装目录
javavmspecificationversion Java 虚拟机规范版本
javavmspecificationvendor Java 虚拟机规范供应
javavmspecificationname Java 虚拟机规范名称
javavmversion Java 虚拟机实现版本
javavmvendor Java 虚拟机实现供应商
javavmname Java 虚拟机实现名称
javaspecificationversion Java 运行时环境规范版本
javaspecificationvendor Java 运行时环境规范供应商
javaspecificationname Java 运行时环境规范名称
javaclassversion Java 类格式版本号
javaclasspath Java 类路径
javalibrarypath 加载库时搜索的路径列表
javaiotmpdir 默认的临时文件路径
javacompiler 要使用的 JIT 编译器的名称
javaextdirs 一个或多个扩展目录的路径
osname *** 作系统的名称
osarch *** 作系统的架构
osversion *** 作系统的版本
fileseparator 文件分隔符(在 UNIX 系统中是“/”)
pathseparator 路径分隔符(在 UNIX 系统中是“:”)
lineseparator 行分隔符(在 UNIX 系统中是“/n”)
username 用户的账户名称
userhome 用户的主目录
userdir 用户的当前工作目录
以上就是关于谁能简单阐述下java编译执行的过程全部的内容,包括:谁能简单阐述下java编译执行的过程、java强制类型转换的时候JVM是如何工作的、深入Java底层:内存屏障与JVM并发详解等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)