はじめに
過去 2 日間、ある同僚が、Java を値渡しすべきか参照渡しすべきか、頭を悩ませていました。彼は妹のことで混乱し、議論するためにその問題をみんなに提起しました。したがって、どちらの当事者が自分たちの理解が正しいと感じているかに関係なく、値による受け渡しを主張する人もいれば、参照による受け渡しを主張する人もいます。この質問に答えるには、この質問を脇に置いて、最初にこの質問の上流にある Java メモリ割り当てに進むほうがよいでしょう。メモリ割り当てに関しては、多くの人が次の文を思い浮かべると思います。参照はスタックに配置され、オブジェクトはヒープに配置され、スタックはヒープを指します。うーん、この文は正しいように思えますが、続けて「このスタックは何ですか?」と尋ねましょう。龍門旅館ですか?いいえ!これは実際には Java 仮想マシン スタックです。さて、この時点で、勉強熱心な子供たちは「Java 仮想マシン スタックとは何ですか?」と尋ねずにはいられません。心配しないで、一緒に見てみましょう。
私たちは次のことを知っています: すべての Java プログラムは Java 仮想マシン上で実行されます。つまり、ランタイム Java 仮想マシンは Java プログラムの実行を担当します。 Java プログラムが開始されると、仮想マシン インスタンスが生成され、プログラムが実行されると、仮想マシン インスタンスは終了します。たとえば、5 つの Java プログラムがコンピュータ上で同時に実行される場合、システムは 5 つの Java 仮想マシン インスタンスを提供し、各 Java プログラムは対応する独自の Java 仮想マシン インスタンス内で独立して実行されます。
Java 仮想マシンには、デーモン スレッドと非デーモン スレッドという 2 種類のスレッドがあります。デーモン スレッドは通常、ガベージ コレクションを実行するスレッドなど、仮想マシン自体によって使用されます。非デーモン スレッドは通常、独自のスレッドを参照します。プログラム内のすべての非デーモン スレッドが終了すると、仮想マシン インスタンスは自動的に終了します。
Java 仮想マシンは Java プログラムの実行を担当するため、まず Java 仮想マシンのアーキテクチャを見てみましょう。下の図を参照してください。ここを参照してください。 目的: クラス ファイルが JVM にロードされ、クラス ローダーによって実行されます。ここでは、青いワイヤーフレーム内の JVM の実行時データ領域に焦点を当てます。これは、実行時の JVM によるメモリ空間の分割と割り当てを表します。このデータ領域は次の主要領域に分割されます: メソッド領域 (メソッド領域)、ヒープ (ヒープ)、Java スタック (Java スタック)、プログラム カウンター レジスタ (プログラム カウンター)、ネイティブ メソッド スタック (ローカル メソッド スタック)。各地域の機能や特徴を詳しく紹介します。
フレームスタックのローカル変数領域のサイズ
この部分は、クラス内で静的に変更された変数を格納するために使用されます。
クラスはクラスローダーによってロードされ、JVMはメソッド領域にクラスローダーへの参照を保持します。
ローダーによってクラスがロードされるプロセス中に、仮想マシンはクラスを表す Class オブジェクトを作成します。同時に、JVM はクラスへの参照を保持します。メソッド領域で。
プログラム カウンター レジスタ (プログラム カウンター) は、ランタイム データ領域 (ランタイム データ領域) 内の非常に小さなメモリ空間のみを占有し、次に実行されるバイトコードを格納するために使用されます。命令のアドレス。
Javaスタック(Javaスタック)は仮想マシンスタック(VMスタック)とも呼ばれ、通常スタックと呼ばれるものです。 Java メソッド実行のメモリ モデルを記述するために使用されます。各メソッドが実行されると、ローカル変数テーブル、操作スタック、ダイナミック リンク、メソッド出口、等各メソッドの呼び出しから実行が完了するまでの過程は、仮想マシンスタックにおいてスタックフレームがスタックからプッシュされてスタックからポップアウトされるまでの過程に相当します。 Java スタックのライフサイクルはスレッドのライフサイクルと同じです。スレッドが実行を完了すると、スタックもクリアされます。
ネイティブ メソッド スタック (Native Method Stack) は Java スタック (Java Stack) に非常に似ており、ネイティブ メソッド (C/C++) の呼び出しに関与するローカル変数を格納するために使用されます。テーブル、操作スタック、その他の情報。
Heap (ヒープ) は仮想マシンの起動時に作成され、ほぼすべてのオブジェクト インスタンスがここにメモリを割り当てるために使用されます。したがって、ヒープは Java 仮想マシンによって管理される最大のメモリ部分であり、ガベージ コレクターによって管理されるキー領域でもあります。
JVMランタイムデータ領域の概要は次のとおりです:
メソッド領域(メソッド領域)とヒープ(ヒープ)は、すべてのスレッドで共有されるメモリ領域です。
Java Stacks (Java スタック)、Program Counter Register (プログラム カウンタ)、Native Method Stack (ネイティブ メソッド スタック) は、スレッドごとのプライベートなメモリ領域です。
オブジェクトを作成すると、オブジェクトの参照は Java Stacks (Java スタック) に保存され、実際のオブジェクトのインスタンスは Heap (ヒープ) に保存されます。これは誰もがよく言うことです。「スタックはヒープを指している」ということです。
今述べた JVM ランタイム データ領域に関係するメモリに加えて、ダイレクト メモリ (ダイレクト メモリ) にも注意を払う必要があります。注: ダイレクト メモリ (ダイレクト メモリ) は、仮想マシンのランタイム データ領域の一部ではなく、Java 仮想マシンの仕様で定義されているメモリ領域でもありませんが、メモリのこの部分も頻繁に使用されるため、OutOfMemoryError (OOM) が発生する可能性があります。 ) 例外が発生します。たとえば、NIO を使用する場合、ネイティブ関数ライブラリを使用してオフヒープ メモリを直接割り当て、このメモリへの参照として Java ヒープに格納されている DirectByteBuffer オブジェクトを通じて動作できます。同様の操作により、Java ヒープとネイティブ ヒープの間でのデータの往復コピーを回避できるため、パフォーマンスが向上します。
Javaメソッドを呼び出してパラメータを渡すとき、値渡しですか、それとも参照渡しですか?多くの議論がありますが、コードを見てみましょう。結局のところ、コードは嘘をつきません。まず、非常に単純な例を見てみましょう。2 つの int 型データを交換するコードは次のとおりです。
package cn.com;/** */public class TestMemory { public static void main(String[] args) { TestMemory testMemory=new TestMemory(); int number1=9527; int number2=1314; System.out.println("main方法中,数据交换前:number1="+number1+" , number2="+number2); testMemory.swapData(number1, number2); System.out.println("main方法中,数据交换后:number1="+number1+" , number2="+number2); } private void swapData(int a,int b) { System.out.println("swapData方法中,数据交换前:a="+a+" , b="+b); int temp=a; a=b; b=temp; System.out.println("swapData方法中,数据交换后:a="+a+" , b="+b); } }
main メソッドで宣言した 2 つの変数、number1=9527、number2=1314 をパラメータとして使用します。メソッド swapData(int a, int b) であり、データはメソッド内で交換されます。コード自体については特に説明する必要はありませんが、出力結果について考えてみましょう。それについて考えた後、次の出力情報を参照してください:
データ交換前の main メソッド内:number1=9527、number2=1314
swapData メソッド内、データ交換前: a=9527、b=1314
swapData メソッドでは、データ交換後: a=1314, b=9527
mainメソッドでは、データ交換後:number1=9527、number2=1314
嗯哼,这和你想的一样么?为什么会是这样呢?还记得刚才讨论Java Stacks(Java 栈)时说的么:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。结合示例的代码:main( )方法在一个栈帧中,swapData( )在另外一个栈帧中;两者彼此独立互不干扰。在main( )中调用swapData( )传入参数时它的本质是:将实际参数值的副本(复制品)传入其它方法内而参数本身不会受到任何影响。也就是说,这number1和number2这两个变量仍然存在于main( )方法所对应的栈帧中,但number1和number2这两个变量的副本(即int a和int b)存在于swapData( )方法所对应的栈帧中。故,在swapData( )中交换数据,对于main( )是没有任何影响的。这就是Java中调用方法时的传值机制——值传递。
嗯哼,刚才这个例子是关于基本类型的参数传递。Java对于引用类型的参数传递一样采用了值传递的方式。我们在刚才的示例中稍加改造。首先,我们创建一个类,该类有两个变量number1和number2,请看代码:
package cn.com;/** */public class DataObject { private int number1; private int number2; public int getNumber1() { return number1; } public void setNumber1(int number1) { this.number1 = number1; } public int getNumber2() { return number2; } public void setNumber2(int number2) { this.number2 = number2; } }
好了,现在我们来测试交换DataObject类对象中的两个数据:
package cn.com;/** */public class TestMemory { public static void main(String[] args) { TestMemory testMemory=new TestMemory(); DataObject dataObject=new DataObject(); dataObject.setNumber1(9527); dataObject.setNumber2(1314); System.out.println("main方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2()); testMemory.swapData(dataObject); System.out.println("main方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2()); } private void swapData(DataObject dataObject) { System.out.println("swapData方法中,数据交换前:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2()); int temp=dataObject.getNumber1(); dataObject.setNumber1(dataObject.getNumber2()); dataObject.setNumber2(temp); System.out.println("swapData方法中,数据交换后:number1="+dataObject.getNumber1()+" , number2="+dataObject.getNumber2()); } }
简单地描述一下代码:在main( )中定义一个DataObject类的对象并为其number1和number2赋值;然后调用swapData(DataObject dataObject)方法,在该方法中交换数据。请思考输出的结果是什么?在您考虑之后,请参见如下打印信息:
main方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换前:number1=9527 , number2=1314
swapData方法中,数据交换后:number1=1314 , number2=9527
main方法中,数据交换后:number1=1314 , number2=9527
嗯哼,为什么是这样呢?我们通过DataObject dataObject=new DataObject();创建一个对象;该对象的引用dataObject存放于栈中,而该对象的真正的实例存放于堆中。在main( )中调用swapData( )方法传入dataObject作为参数时仍然传递的是值,只不过稍微特殊点的是:该值指向了堆中的实例对象。好了,再结合栈帧来梳理一遍:main( )方法存在于与之对应的栈帧中,在该栈帧中有一个变量dataObject它指向了堆内存中的真正的实例对象。swapData( )收到main( )传递过来的变量dataObject时将其存放于其本身对应的栈帧中,但是该变量依然指向堆内存中的真正的实例对象。也就是说:main( )方法中的dataObject和swapData( )方法中的dataObject指向了堆中的同一个实例对象!所以,在swapData( )中交换了数据之后,在main( )会体现交换后的变化。在此,我们可以进一步的验证:在该swapData( )方法的最后一行添加一句代码dataObject=null ;我们发现打印信息并没有任何变化。因为这句代码仅仅使得swapData( )所对应的栈帧中的dataObject不再指向堆内存中的实例对象但不会影响main( )所对应的栈帧中的dataObject依然指向堆内存中的实例对象。
通过这两个示例,我们进一步验证了:Java中调用方法时的传递机制——值传递。当然,有的人说:基础类型传值,对象类型传引用。其实,这也没有什么错,只不过是表述方式不同罢了;只要明白其中的道理就行。如果,有些童鞋非纠缠着个别字眼不放,那我只好说:PHP是世界上最好的语言。
以上がJava メモリ割り当ての調査の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。