Java仮想マシンの原理

Java はクロスプラットフォーム言語であり、Java によって開発されたプログラムはコンパイル後に Linux または Windows 上で実行でき、PC、サーバー、または携帯電話上で実行でき、X86 CPU 上で実行でき、ARM CPU 上でも実行できます。

オペレーティング システム、特に CPU アーキテクチャが異なると、同じ命令を実行することはできません。Java にこのような魔法の機能がある理由は、Java によってコンパイルされたバイトコード ファイルが基盤となるシステム プラットフォーム上で直接実行されず、Java 仮想マシン JVM 上で実行されるためです。JVM は基盤となるシステムの差異を保護します。Java バイトコード ファイルは、統一された動作環境。JVM は本質的にアプリケーション プログラムであり、起動後に Java バイトコード ファイルをロードして実行します。JVM の正式名称は Java Virtual Machine ですが、なぜこのようなプログラムがマシンと呼ばれるのか疑問に思ったことはありますか?

実際、この質問に答えると、JVM の基礎となる構造が理解できるようになります。このように、Javaを開発する際に、さまざまな問題に遭遇した場合、JVMレベルではどうなっているのかを考えることができます。次に、実際の問題が解決されるまで、さらに情報を検索し、問題を分析します。

JVMの構成

この質問に対する答えを知るには、まず JVM の構造を理解する必要があります。JVM は主に、クラス ローダー、ランタイム データ領域、実行エンジンの 3 つの部分で構成されます。

代替

ランタイムデータ領域には、主にメソッド領域、ヒープ、Java スタック、プログラムカウントレジスタが含まれます。

メソッド領域には主にディスクからロードされたクラスのバイトコードが格納され、プログラムの実行中に作成されたクラスのインスタンスはヒープに格納されます。プログラムが実行されるとき、実際にはスレッド単位で実行されます。JVM がスタートアップ クラスの main メソッドに入ると、アプリケーションのメイン スレッドが作成され、main メソッド内のコードがメイン スレッドによって実行されます。各スレッドには独自の Java スタックがあり、メソッド ランタイムのローカル変数はスタックに格納されます。現在のスレッドがバイトコード命令のどの行を実行するか、この情報はプログラム カウント レジスタに保存されます。

一般的な Java プログラムの実行プロセスは次のとおりです。

Java コマンドを使用して JVM を起動すると、JVM のクラス ローダーは、Java コマンドのパラメータに従って、指定されたパスから .class クラス ファイルをロードします。クラス ファイルはメモリにロードされた後、特別な場所に保存されます。メソッド領域。次に、JVM はこのクラス ファイルの main メソッドを実行するメイン スレッドを作成し、main メソッドの入力パラメータとメソッド内で定義された変数が Java スタックにプッシュされます。メソッド内でオブジェクトインスタンスが作成されると、オブジェクトインスタンス情報がヒープに格納され、オブジェクトインスタンスの参照、つまりヒープ上のオブジェクトインスタンスのアドレス情報がスタックに記録されます。クラスメソッド内の実行コードはメソッド領域に格納され、メソッド内のローカル変数はスレッドスタックに格納されるため、ヒープに記録されるオブジェクトインスタンス情報は主にメンバ変数情報になります。

プログラム カウント レジスタには、最初に main メソッドのコードの最初の行の位置が格納され、JVM の実行エンジンは、この位置に従ってメソッド領域の対応する位置からコード命令のこの行をロードし、CPU 命令として解釈します。独自のプラットフォームを実装し、それを CPU に渡します。メイン メソッドで他のメソッドが呼び出されると、他のメソッドに入ると、Java スタックにこのメソッド用の新しいスタック フレームが作成されます。スレッドがこのメソッドで実行されると、メソッドのローカル変数がこのスタックに格納されます。フレーム。メソッドの実行が終了して終了すると、スタック フレームが Java スタックからポップされ、現在のスタック フレーム、つまりスタックの最上位が、このスタック内の変数を使用してメイン メソッドのスタック フレームに戻ります。フレーム、メインメソッドの実行を継続します。そうすれば、main メソッドと f メソッドの両方で同じ変数が定義されている場合でも、JVM がそれを間違えることはありません。この部分については、最初の記事ですでに説明しましたが、JVM はマシンとして、オペレーティング システムと同じ方法でスレッド スタックを処理します。

代替

Java のスレッド セーフはしばしば混乱を招きます。Java スタックの観点から理解してみるとよいでしょう。メソッドで定義されたすべての基本型変数は、このメソッドを実行する各スレッドによって独自のスタックに置かれます。スレッドのスタックは互いに分離されています。したがって、これらの変数はスレッドセーフである必要があります。オブジェクト インスタンスがメソッド内で作成された場合、オブジェクト インスタンスがメソッドによって返されない場合、または外部オブジェクト コンテナに配置された場合、つまり、オブジェクトは配置されていても、オブジェクトの参照はメソッドから出ません。 heap ですが、このオブジェクトは他のスレッドからアクセスされず、スレッドセーフでもあります。

相反,像Servlet这样的类,在Web容器中创建以后,会被传递给每个访问Web应用的用户线程执行,这个类就不是线程安全的。但这并不意味着一定会引发线程安全问题,如果Servlet类里没有成员变量,即使多线程同时执行这个Servlet实例的方法,也不会造成成员变量冲突。这种对象被称作无状态对象,也就是说对象不记录状态,执行这个对象的任何方法都不会改变对象的状态,也就不会有线程安全问题了。事实上,Web开发实践中,常见的Service类、DAO类,都被设计成无状态对象,所以虽然我们开发的Web应用都是多线程的应用,因为Web容器一定会创建多线程来执行我们的代码,但是我们开发中却可以很少考虑线程安全的问题。

我们再回过头看JVM,它封装了一组自定义的字节码指令集,有自己的程序计数器和执行引擎,像CPU一样,可以执行运算指令。它还像操作系统一样有自己的程序装载与运行机制,内存管理机制,线程及栈管理机制,看起来就像是一台完整的计算机,这就是JVM被称作machine(机器)的原因。

JVM的垃圾回收

事实上,JVM比操作系统更进一步,它不但可以管理内存,还可以对内存进行自动垃圾回收。所谓自动垃圾回收就是将JVM堆中的已经不再被使用的对象清理掉,释放宝贵的内存资源。那么要想进行垃圾回收,首先一个问题就是如何知道哪些对象是不再被使用的,可以清理的呢?

JVM通过一种可达性分析算法进行垃圾对象的识别,具体过程是:从线程栈帧中的局部变量,或者是方法区的静态变量出发,将这些变量引用的对象进行标记,然后看这些被标记的对象是否引用了其他对象,继续进行标记,所有被标记过的对象都是被使用的对象,而那些没有被标记的对象就是可回收的垃圾对象了。所以你可以看出来,可达性分析算法其实是一个引用标记算法。

进行完标记以后,JVM就会对垃圾对象占用的内存进行回收,回收主要有三种方法。

第一种方式是清理:将垃圾对象占据的内存清理掉,其实JVM并不会真的将这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲,记录在一个空闲列表里,当应用程序需要创建新对象的时候,就从空闲列表中找一段空闲内存分配给这个新对象。

但这样做有一个很明显的缺陷,由于垃圾对象是散落在内存空间各处的,所以标记出来的空闲空间也是不连续的,当应用程序创建一个数组需要申请一段连续的大内存空间时,即使堆空间中有足够的空闲空间,也无法为应用程序分配内存。

第二种方式是压缩:从堆空间的头部开始,将存活的对象拷贝放在一段连续的内存空间中,那么其余的空间就是连续的空闲空间。

第三种方法是复制:将堆空间分成两部分,只在其中一部分创建对象,当这个部分空间用完的时候,将标记过的可用对象复制到另一个空间中。JVM将这两个空间分别命名为from区域和to区域。当对象从from区域复制到to区域后,两个区域交换名称引用,继续在from区域创建对象,直到from区域满。

下面这系列图可以让你直观地了解JVM三种不同的垃圾回收机制。

回收前:

代替

清理:

代替

压缩:

代替

复制:

代替

JVM在具体进行垃圾回收的时候,会进行分代回收。绝大多数的Java对象存活时间都非常短,很多时候就是在一个方法内创建对象,对象引用放在栈中,当方法调用结束,栈帧出栈的时候,这个对象就失去引用了,成为垃圾。针对这种情况,JVM将堆空间分成新生代(young)和老年代(old)两个区域,创建对象的时候,只在新生代创建,当新生代空间不足的时候,只对新生代进行垃圾回收,这样需要处理的内存空间就比较小,垃圾回收速度就比较快。

新生代又分为Eden区、From区和To区三个区域,每次垃圾回收都是扫描Eden区和From区,将存活对象复制到To区,然后交换From区和To区的名称引用,下次垃圾回收的时候继续将存活对象从From区复制到To区。当一个对象经过几次新生代垃圾回收,也就是几次从From区复制到To区以后,依然存活,那么这个对象就会被复制到老年代区域。

当老年代空间已满,也就是无法将新生代中多次复制后依然存活的对象复制进去的时候,就会对新生代和老年代的内存空间进行一次全量垃圾回收,即Full GC。所以根据应用程序的对象存活时间,合理设置老年代和新生代的空间比例对JVM垃圾回收的性能有很大影响,JVM设置老年代新生代比例的参数是-XX:NewRatio。

代替

JVM中,具体执行垃圾回收的垃圾回收器有四种。

第一种是Serial 串行垃圾回收器,这是JVM早期的垃圾回收器,只有一个线程执行垃圾回收。

第二种是Parallel 并行垃圾回收器,它启动多线程执行垃圾回收。如果JVM运行在多核CPU上,那么显然并行垃圾回收要比串行垃圾回收效率高。

在串行和并行垃圾回收过程中,当垃圾回收线程工作的时候,必须要停止用户线程的工作,否则可能会导致对象的引用标记错乱,因此垃圾回收过程也被称为stop the world,在用户视角看来,所有的程序都不再执行,整个世界都停止了。

第三种CMS 并发垃圾回收器,在垃圾回收的某些阶段,垃圾回收线程和用户线程可以并发运行,因此对用户线程的影响较小。Web应用这类对用户响应时间比较敏感的场景,适用CMS垃圾回收器。

最后一种是G1 垃圾回收器,它将整个堆空间分成多个子区域,然后在这些子区域上各自独立进行垃圾回收,在回收过程中垃圾回收线程和用户线程也是并发运行。G1综合了以前几种垃圾回收器的优势,适用于各种场景,是未来主要的垃圾回收器。

代替

总结

JVM有很多配置参数,Java开发过程中也可能会遇到各种问题,了解了JVM的基本构造,可以帮助我们从原理上去解决问题。

比如遇到OutOfMemoryError,我们就知道是堆空间不足了,可能是JVM分配的内存空间不足以让程序正常运行,这时候我们需要通过调整-Xmx参数增加内存空间。也可能是程序存在内存泄漏,比如一些对象被放入List或者Map等容器对象中,虽然这些对象程序已经不再使用了,但是这些对象依然被容器对象引用,无法进行垃圾回收,导致内存溢出,这时候可以通过jmap命令查看堆中的对象情况,分析是否有内存泄漏。

如果遇到StackOverflowError,我们就知道是线程栈空间不足,栈空间不足通常是因为方法调用的层次太多,导致栈帧太多。我们可以先通过栈异常信息观察是否存在错误的递归调用,因为每次递归都会使嵌套方法调用更深入一层。如果调用是正常的,可以尝试调整-Xss参数增加栈空间大小。

如果程序运行卡顿,部分请求响应延迟比较厉害,那么可以通过jstat命令查看垃圾回收器的运行状况,是否存在较长时间的FullGC,然后调整垃圾回收器的相关参数,使垃圾回收对程序运行的影响尽可能小。

执行引擎在执行字节码指令的时候,是解释执行的,也就是每个字节码指令都会被解释成一个底层的CPU指令,但是这样的解释执行效率比较差,JVM对此进行了优化,将频繁执行的代码编译为底层CPU指令存储起来,后面再执行的时候,直接执行编译好的指令,不再解释执行,这就是JVM的即时编译JIT。Web应用程序通常是长时间运行的,使用JIT会有很好的优化效果,可以通过-server参数打开JIT的C2编译器进行优化。

つまり、JVMの構造を理解していれば、Java開発時にさまざまな問題に遭遇したときにJVMの構造を考えることができるのですが、JVMレベルではどうなのでしょうか?そして、さらに情報を検索し、問題を分析することで、問題が本当に解決されます。そのような継続的な思考と分析を経て、Java、JVM、さらには全体の原理システムや設計コンセプトについての理解と理解が深まります。コンピューター。

この記事はmdniceマルチプラットフォームによって公開されています

おすすめ

転載: blog.csdn.net/qq_35030548/article/details/131179867
おすすめ