JVMのアーキテクチャ

仮想マシン

シミュレートされた仮想マシンは、特定の命令セットアーキテクチャ(ISA)ソフトウェアを実行するオペレーティング・システムおよびハードウェアの抽象化です。

 

抽象オブジェクト指向プログラミング(OOP)と同様、そのようなコンピュータ・システムは、用の汎用プログラミング・インターフェースである(又は依存性逆転原理)、CCPの一部を達成するために、抽象化の下層を抽出することにより、基礎となる実装は、抽象的であり、彼らの人格を完成セクション。すなわち、抽象化の異なるレベルを介して基底実装を分離することです。仮想マシン仕様は、この仮想マシンが終了した独自の仮想マシンが提供する基礎となるオペレーティング・システムおよびハードウェアの使用機能を機能(すなわちインターフェース)を定義(達成する)機能を実行する必要があります。仮想マシン上で実行することにより、Javaは唯一の良いクロスプラットフォームの機能を備えています。

Java仮想マシン

Java仮想マシン(JVM)は、バイトコード命令セットで実行されているJava仮想マシン仕様で定義されています。そのようなバイトコード命令セットは、バイトのオペレーションコード(オペコード)、ゼロ(oprand)へのオペランドの複数、完了された各バイトコード命令に対して定義された仮想マシンの仕様とどのくらいどの関数オペランドのを含みます。バイトコード命令ストリームとクラス定義情報が含まれているクラスファイル上で実行しているJava仮想マシンは、Java仮想マシンの仕様は、クラスファイル形式(各バイトの精度)を定義します。したがって、2つの要素のJava仮想マシンのバイトコードクラスファイル形式と命令セットが達成され、Java仮想マシンであれば、ファイルは、各クラスのバイトコード命令を読み出すように正しい方法で実現され、バイトコードを達成するために必要とされます機能命令は、JVMを達成することができます。

現在、主に商業用JVMで使用される:日のHotSpot、BEA JRocketおよびIBM J9を。ホットスポットは、解釈のバイトコードではなく解釈よりも、直接ホットスポット(ホットスポット)コード、および、コードのこの部分は、ネイティブコードにコンパイルされ、後のネイティブコードを検出したときに、ホットスポット+ -timeコンパイル実行コードを説明し、そしてう効果的に仮想マシンのパフォーマンスが向上します。主にサーバアプリケーションに位置JRocket、それは仮想マシンの起動スピードを懸念していない、それはネイティブコード実行にすべてのコードインタイムコンパイルされます、JRocketのガベージコレクタは、高い捕集効率を有します。J9のHotSpot主にIBMのJava製品のさまざまなデスクトップおよびサーバーアプリケーションで同様の位置やフォーカス、。

 

Java言語とJava仮想マシン

私たちは、.javaファイルをJavaソースコードは、javacが.classファイルにコンパイルすることを知っています。.classファイルは、.classファイル内のバイトコード命令は、基礎となるJVMバイトコードインタプリタや時間コンパイラ(JITコンパイラ)によって実行される、JVM上で実行することができます。JVMは、オペレーティングシステム、オペレーティングシステムとその様々なソフトウェアでサービスを実行するための命令セットを呼び出すことにより、基盤となるハードウェア上で実行されています。 

 

Java言語だけではない限り、適切なコンパイラは、Java言語は、(例えばJ ++など)任意のプラットフォーム上で実行することができます実装して、JVM上で動作する、それはまたのような、オペレーティングシステムの上で直接実行ネイティブコードにコンパイルすることができます、GCJ(Java用のGNUコンパイラ)は、Linux上で、あなたは直接実行ネイティブコードにJava言語コンパイラを置くことができます。同様に、JVMは限り適切なコンパイラを達成するためには、JVMバイトコードに他の言語にコンパイルされます、それはJVM上で実行できるだけでなく、Java言語の実装です。そのため、以降の.classファイルスクリーンの違いなどのJava、JRubyと他のトップの言語なので、Javaの、Groovyは、お互いを呼び出すことができます。

JVMのライフサイクル

Javaプログラムを起動すると、プログラムが終了し閉じているときに、仮想マシン・インスタンスが、誕生した、仮想マシン・インスタンスが死んでしまいます。main()メソッドを介して実行するJavaプログラムのJVMインスタンス。そして、main()メソッドは、(パブリック)、スタティック(静的)共通にする必要があり、voidを返し、パラメータとして文字列配列を受け取ります。メインJavaプログラム最初のクラス()メソッドは、改革プロセスの初期スレッドのための出発点として、他のスレッドがこのスレッドの最初の試験により開始されます。 
スレッドガードと非デーモンスレッド:2の内部JVMスレッドがあります。デーモンスレッドは、通常、ガベージコレクションのスレッドとして、自分自身の使用のための仮想マシンによって引き起こされます。プログラムは、すべての非デーモンスレッドが終了されている場合は、JVMインスタンスは自動的に終了します。

Java仮想マシンのアーキテクチャ

JVMクラスローダーサブシステムランタイムデータ領域では、ネイティブメソッドの実行エンジンインタフェース。

クラスローダサブシステム

类加载器子系统主要用于定位类定义的二进制信息,然后将这些信息解析并加载至虚拟机,转化为虚拟机内部的类型信息的数据结构。类加载器子系统还承担着安全性的责任,并且是JVM的动态链接和动态加载的基础。将二进制信息=>类型信息的数据结构,中间需要经过很多步骤。首先类加载器是JVM安全沙箱的第一道防线,能够防止非信任类破坏虚拟机。每一个被加载的class文件需要经过四次校验才能被加载。校验通过后,类加载器的命名空间和运行时包的特性能够防止非信任类伪装成信任类来破坏虚拟机。类加载器在方法区构造具有这个类的信息的数据结构后,会在堆上创建一个Class对象作为访问这个数据结构的接口。同时,类加载还需要初始化类的静态数据,也就是调用类的方法。以上就是一个类的加载、链接及初始化的过程。

运行时数据区

运行时数据区是JVM运行时的内存空间的组织,逻辑上又划分为多个区,这些区的生命周期和它是否线程共享有关,它们分别是:

用于存放对象或数组实例,也就是运行期间new出来的对象。堆的生命周期与JVM相同,并且在线程之间共享访问。由于多线程并发访问,所以需要考虑线程安全的问题,有两种方法。第一种是,加锁进行互斥访问。第二种是线程本地分配缓冲(Thread Local Allocate Buffer, TLAB),在线程创建时预先给每个线程分配一块区域,这块区域是线程私有的,对其他线程是不可见,也就不会被共享。JVM规范规定在申请不到足够的内存时,堆会抛出OutOfMemoryException。

方法区

存放类型信息和运行时常量池(Runtime Constant Pool)。每个被类加载器加载的类都会在方法区中形成一个与子对应的类型信息的数据结构,包括:这个类的类名、直接超类、实现的接口列表、字段列表、方法列表等。运行时常量池是class文件中的常量池列表(Constant Pool List)在运行时的一种体现,其中存储各种基本数据类型及String类型的常量以及其他类、方法、字段的符号引用。方法区的生命周期与JVM相同,被多个线程共享,所以要考虑并发访问的安全性的问题。JVM规范规定在需要的内存得不到满足的情况下,方法区会抛出OutOfMemoryException。

PC(Program Counter)

线程私有的,生命周期与线程相同,是对CPU中PC的一种模拟。如果线程正在执行的是Java方法,则该线程的PC中存放的下一条字节码指令的地址。在进行Java方法的调用和返回时,需要更新PC以保存当前方法(Current Method)正在执行的字节码指令的地址。PC是JVM规范中唯一没有规定会抛出异常的存储区。

JVM栈

线程私有,生命周期与线程相同,是对传统语言(比如C)中的方法调用栈的一种模拟。JVM栈中存放栈帧(Frame)用于进行方法调用和返回、存储局部变量以及计算的中间结果。JVM规范规定栈可以抛出两种异常:(1)StackOverflowException,在栈的深度大于某个规定值的情况下抛出。(2)OutOfMemoryException,在为新栈帧分配内存或者是为线程分配栈的内存时,申请不到足够的内存的情况下抛出。 
JVM栈中存放的是栈帧,每个栈帧对应着一次方法调用。每一时刻,JVM线程只能执行一个方法(Current Method),该方法的栈帧是JVM栈的栈顶的元素(叫做当前栈帧,Current Frame),当调用一个方法时,会初始化一个栈帧压入JVM栈;当方法调用返回或者抛出异常没有被处理的情况下,JVM栈会弹出该方法对应的栈帧。每一个栈帧中存放局部变量表(Local Variable Table)、操作数栈(Oprand Stack)以及其他栈帧信息。栈帧的大小在编译时就确定了,编译器会把局部变量表和操作数栈的大小记录在class文件中method_info的属性表中。局部变量表类似于数组存放局部变量和方法参数。由于JVM采用的是基于栈的指令集体系结构,而不是基于寄存器,所以JVM上的所有计算都是在操作数栈上进行的(比如,算术运算、方法调用、内存访问等)。

本地方法栈

用于支持本地方法调用,抛出的异常与JVM栈相同。

执行引擎

执行引擎用于执行JVM字节码指令,主要由两种实现方式: 
(1)将输入的字节码指令在加载时或执行时翻译成另外一种虚拟机指令; 
(2)将输入的字节码指令在加载时或执行时翻译成宿主主机本地CPU的指令集。这两种方式对应着字节码的解释执行和即时编译。比如在HotSpot VM中执行引擎的实现是一种解释-编译的层次结构: 
(1)解释执行:解释执行字节码,并以方法为单位收集“热点(HotSpot)代码”的信息,将“热点代码”执行C0编译。 
(2)C0编译:将收集的“热点代码”编译成本地代码,并进行一些简单的优化。继续收集运行时信息,将一些频繁执行的本地代码进行C1编译。 
(3)C1编译:将C0阶段的本地代码,进行一些比较激进的优化。如果某些优化导致本地代码执行失败,此时JVM会退化到解释执行字节码阶段。

自动内存管理

自动内存管理用于管理运行时数据区的分配和释放。和C和C++相比,Java不需要程序员主动的管理内存(在new出对象后,不需要显示的delete),这样JVM就需要承担内存管理这个任务。内存管理的重点主要是在申请内存(new对象、类加载和初始化、启动线程时初始化栈等)得不到满足时,JVM可以自动回收那些不再存活的对象所占用的内存,也就是经常听到的垃圾收集。在回收过程中还要保证处理内存空间的碎片,以提高空间利用率。回收过程主要有两个关键点,标记存活对象和回收内存的算法。

 

标记存活对象主要有引用计算和根搜索法两种。

(1)引用计数,是一种很普遍的方法,在python、lua等一些脚本语言中都是使用这种算法。每个对象持有一个计数器,标记这个对象被引用的次数。进行垃圾收集时,那些引用计数为0的对象就是“死”对象,需要被收集。引用计数的一个缺点就是它没有办法处理循环引用的情况(A->B, B->A)。

(2)根搜索,HotSpot虚拟机采用这种算法标记存活对象。把方法区、JVM栈中的所有的引用组成的集合作为搜索的根,从这个集合开始遍历直到结束。其中被遍历到的对象是存活对象;那些没有被遍历到的对象需要被垃圾收集。这样可以有效的避免循环引用的情况。

回收内存的算法主要有: 
(1)复制算法,将内存分成两个部分,每一时刻只是用其中的一个。进行回收时,将所有存活的对象依次复制到另一个部分(依次复制避免了内存碎片的产生),接下来只用这一个部分。复制算法需要在两个内存区域来回复制,有一定的复制开销和空间开销(每一时刻只使用一个区域),但是可以很好的解决内存碎片的问题,适用于对象频繁创建并且生命周期短的情况。 
(2)标记清扫,先进行存活对象标记,回收时将“死”对象占用的内存直接释放掉,会产生大量的内存碎片。 
(3)标记整理,标记阶段与标记清扫算法一样,回收阶段释放“死”对象的内存后,还需要进行对象的移动使得所有对象依次在内存中排列,避免了内存碎片的产生。标记整理与复制算法相反,适用于对象创建不频繁,生命周期长得情况。 
(4)按代收集,将内存按照对象生命周期的不同划分为多个部分,每个部分采用不同的收集算法。目前,大部分商业虚拟机都是采用这种算法。比如,在HotSpot中,内存被划分为:新生代(New)、老年代(Old)和永久代(Perm)。新生代采用复制算法,老年代和永久代采用标记整理算法。内存分配、回收的策略是,对象首先在新生代分配,如果新生代内存不满足要求,则触发一次新生代内存的垃圾收集(Young GC,或者是Minor GC)。Young GC会导致部分新生代的对象被移动至老年代,一部分是因为新生代内存不足以放下所有的对象;另一部分是因为这些对象的年龄(每个对象都保存着这个对象被垃圾收集的次数,表示它的年龄。存储在对象头的age属性中)大到足以晋升到老年代。当新生代的对象进入老年代,而老年代的内存不满足要求时,则会触发一次整个新生代和老年代的垃圾收集(Full GC, 或者是Major GC)。 
在JVM中有多个后台线程用于完成自动内存管理,对于CPU来说这些后台线程和用户线程是一样的,都需要占用系统的资源。在GC线程进行垃圾收集时必须执行“Stop the World”这一操作,也就是暂停所有的用户线程。这就导致对于实时性要求比较高的系统,JVM的垃圾收集可能是一个短板。但是在JDK1.5,Sun提供了CMS(Concurrent Mark and Sweep)垃圾收集器,通过GC线程和用户线程并发执行减少GC时间,提高了JVM的实时性。在JVM的各种应用中,gc调优是一个关键的部分,主要目标是减少GC的次数并且降低每次GC的时间。

 

JVM执行程序的流程分析

在命令行执行”java Main”就会开启一个JVM实例,我们可以通过jps,jstat等JVM工具观察JVM的运行状态,下面以运行com.ntes.money.Main这个类为例来描述一下JVM执行一个程序的流程。 
当在命令行执行”java -Xmx=12m -Xms=12m -Dname=value com.ntes.money.Main”这个命令时,JVM的执行流程是: 
1)加载JVM,主要是加载动态链接库,windows下是jvm.dll,Linux下是libjvm.so; 
2)设置JVM启动参数,比如命令中的-Xmx=12m -Xms=12m用于设置堆大小。 
3)初始化JVM。 
4)调用类加载器子系统,加载com.ntes.money.Main。这里给出的是自定义类,根据类加载器双亲委派链,最后是由系统默认类加载器(Classpath类加载器)进行加载。首先,根据全路径类型转化为文件路径com/ntes/money/Main.class,然后读取Main.class中的二进制信息、解析、加载,在方法区中形成Main类对应的数据结构。这里可能抛出ClassNotFoundException,有两种原因。一是文件路径com/ntes/money/Main.class不存在;二是com/ntes/money/Main.class文件路径存在,但是Main.class文件中存储的不是Main类的信息,比如是Main1,Main2等其他类的信息。这种情况下,会抛出NoClassDefFoundError,然后导致ClassNotFoundException。 
5)在方法区com.ntes.money.Main类对应的数据结构中,根据方法描述符及访问标志,查找main方法。这里的描述符,包括了方法的方法名、参数、返回值,也就是public static void main(String[])。如果找不到对应的main方法,会抛出NoSuchMethodError: main异常。 
6)通过本地方法(JNI)执行main方法。

 

转自:

 

おすすめ

転載: www.cnblogs.com/niwa/p/11206222.html