ホームページ >Java >&#&ベース >JVM 学習 Java メモリ構造

JVM 学習 Java メモリ構造

coldplay.xixi
coldplay.xixi転載
2021-02-01 17:41:302544ブラウズ

JVM 学習 Java メモリ構造

Java メモリ構造

  • 1.JVM の概要
  • 2.プログラム カウンター
    • 2.1.定義
    • 2.2.機能と特徴の説明
  • 3.仮想マシンスタック
    • 3.1. スタックの特性
    • 3.2. スタックのデモンストレーション
    • 3.3. スタックの問題の分析
    • 3.4. スタックのスレッド セーフティの問題
    • 3.5.スタック メモリ オーバーフロー (StackOverflowError)
    • 3.6. スレッド実行診断
      • 3.6.1. ケース 1: CPU 使用率が高すぎる (例として Linux システム)
      • 3.6.2. ケース 2: スレッド診断_長期間結果が得られない
  • 4.ローカル メソッド スタック
  • 5.Heap
    • #5.1.定義
    • ##5.2.ヒープメモリオーバーフロー(OutOfMemoryError:Javaヒープスペース)
    • ##5.3.ヒープメモリ診断
    • ##6.メソッド領域
  • 6.1.定義
  • 6.2.定義
    • 6.3.メソッド領域のメモリオーバーフロー(OutOfMemoryError:メタスペース)
    • 6.4. 定数プール
    1.JVM の概要

定義:JVM正式名は Java 仮想マシンです - Java プログラムの実行環境 (Java バイナリ バイトコードの実行環境)


#利点:

一度書いたらどこでも実行できます (クロス) -platform)

自動メモリ管理、ガベージコレクション機能
  • 配列添字範囲外チェック
  • ポリモーフィズム
  • Compare JVM、JRE、JDK
  • の接続と相違点を図で説明します。

JVM アーキテクチャ
図に示すように、JVM 学習 Java メモリ構造
a クラスが Java ソース コード (.java ファイル) から Java バイナリ バイトコードにコンパイルされた後、クラスは、実行する前に JVM にロードされる前にクラス ローダーを通過する必要があります。 通常、クラスはメソッド領域に配置します。今後クラスによって作成されるオブジェクトはヒープに配置され、ヒープ内のオブジェクトは、メソッドを呼び出すときにローカル開発だけでなく、仮想マシンのスタックやプログラム カウンターも使用します。
メソッドが実行されると、コードの各行が実行エンジンのインタープリターによって 1 行ずつ実行されます。メソッド内のホット コードは、頻繁に呼び出され、ジャストインタイム コンパイラによってコンパイルおよび実行されるコードです。 GC はガベージを収集します。 JVM 学習 Java メモリ構造 ローカル メソッド インターフェイスを通じて、オペレーティング システムが提供する関数を呼び出すことができます。


JVM のメモリ構造には次のものが含まれます。
1. メソッド領域

2. プログラム カウンタ

3. 仮想マシン スタック 4. ローカル メソッド スタック
5 .Heap



2.Program Counter

2.1.Definition

Program Counter RegisterProgram Counter( Register) 関数:

次の jvm 命令の実行アドレスを記憶するためです

機能 スレッドに対してプライベートです
メモリ オーバーフローはありません
(メモリ構造の中で唯一メモリオーバーフローしない構造)

2.2ではプログラムカウンタの機能と特徴について説明します。

2.2. 関数と機能の説明

 二进制字节码			JVM指令					Java源代码 0: getstatic     #20                 // PrintStream out = System.out; 
 3: astore_1                          // - 
 4: aload_1                           // out.println(1); 
 5: iconst_1                          // - 
 6: invokevirtual #26                 // - 
 9: aload_1                           // out.println(2); 
 10: iconst_2                          // -
 11: invokevirtual #26                 // -
 14: aload_1                           // out.println(3); 
 15: iconst_3                          // -
 16: invokevirtual #26                 // -
 19: aload_1                           // out.println(4); 
 20: iconst_4                          // -
 21: invokevirtual #26                 // -
 24: aload_1                           // out.println(5); 
 25: iconst_5                          // -
 26: invokevirtual #26                 // -
 29: return

System.out の最初の行では、変数に値を代入し、4 でそれを呼び出しています。 : println() メソッド。次に、1、2、3、4、5 を順番に出力します。これらの命令は実行のために CPU に直接渡すことができず、インタプリタを経由する必要があります。バイトコード命令を 1 つずつマシン コードに解釈し、そのマシン コードを CPU に渡して実行することができます。 つまり、

バイナリ バイトコード -> インタプリタ -> マシン コード -> CPU


実際、プログラム カウンターの機能は、命令の実行中に記録することです。 . 次の JVM 命令の実行アドレスをライブします。
上記のバイナリ バイトコードの前にある数字 0、3、4 は、アドレスとして理解できます。このアドレス情報に基づいて、実行するコマンドを見つけることができます。 命令が実行のために CPU に与えられるたびに、プログラム カウンタは次の命令のアドレスをプログラム カウンタに入れます。命令の実行が完了すると、インタプリタはプログラムに移動します。カウンタ 次の命令のアドレスを取得します。次に、それはインタプリタによってマシンコードに解釈され、実行のために CPU に渡されます。その後、このプロセスを繰り返してください。

物理的には、プログラム カウンタはレジスタを通じて実装されます。レジスタは、CPU コンポーネントの中で最も高速に読み取るストレージ ユニットです。

程序计数器是线程私有的
假如说上述代码都在线程1中运行,同时运行的还有线程2和线程3,多个线程运行的时候,CPU会给每个线程分配时间片,给线程1分配时间片,如果线程1在指定的时间没有运行完,它就会把状态暂存,切换到线程2,线程2执行自己的代码。线程2执行完了,再继续执行线程1的代码,在线程切换的过程中,我们要记住下一条指令的执行地址。就需要用到程序计数器。假如说线程1刚开始执行到第9行代码,恰好这个时候时间片用完,CPU切换到线程2去执行,这时它就会把下一条指令的地址10记录到程序计数器里面,而且程序计数器是线程私有的,它是属于线程1的,等线程2代码执行完了,线程1抢到了时间片,它就会从自己的程序计数器里面取出下一行代码。每个线程都有自己的程序计数器

3.虚拟机栈

3.1.栈的特点

栈类似现实生活中的子弹夹。栈最重要的特点是后进先出。
JVM 学習 Java メモリ構造
如图,1是最先进入栈中的,3是最后进入栈中的,但是在出栈的时候,3最先出栈,1最后出栈。即他们按照1,2,3的顺序入栈,按照3,2,1的顺序出栈

虚拟机栈就是我们线程运行时需要的内存空间,一个线程运行时需要一个栈。如果将来有多个线程的话,它就会有多个虚拟机栈。
每个栈可以看成是由多个栈帧组成,例如上图中每个元素1,2,3都可以看成是栈帧。
一个栈帧就对应着Java中一个方法的调用,即栈帧就是每个方法运行时需要的内存。每个方法运行时需要的内存一般有参数,局部变量,返回地址,这些都需要占用内存,所以每个方法执行时,都要预先把这些内存分配好。
当我们调用第一个方法栈帧时,它就会给第一个方法分配栈帧空间,并且压入栈内,当这个方法执行完了,就会把这个方法栈帧出栈,释放这个方法所占用的内存
一个栈内可能有多个栈帧存在。

总结
Java Virtual Machine Stacks(Java虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈
  • 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法(位于栈顶)

活动栈帧表示线程正在执行的方法。

3.2.栈的演示

public class teststacks {
	public static void main(String[] args) throws InterruptedException{
		method1();
	}
	public static void method1(){
		method2(1,2);
	}
	public static int method2(int a,int b){
		int c=a+b;
		return c;
	}}

可以自行调试以上代码来观察栈中的变化情况。
入栈顺序:main->method1->method2
出栈顺序:method2->method1->main

3.3.栈的问题辨析

  1. 垃圾回收是否涉及栈内存?
    不涉及,垃圾回收只是回收堆内存中的无用对象,栈内存不需要对它执行垃圾回收,随着方法的调用结束,栈内存就释放了。
  2. 栈内存分配越大越好吗?
    首先栈内存可以指定:-Xss size(如果不指定栈内存大小,不同系统会有一个不同的默认值)
    其次由于电脑内存一定,假如有100Mb,如果给栈内存指定为2Mb,则最多只能存在50个线程,所以并不是越大越好,栈内存较大一般是可以进行较多次的方法递归调用,而不会增强线程效率,反而会使线程数量减少,一般使用默认大小

3.4.栈的线程安全问题

看一个变量是否线程安全,首先就是看这个变量对多个线程是共享的还是私有的,共享的变量需要考虑线程安全。
其次局部变量也不能保证是线程安全的,需要看此变量是否逃离了方法的作用范围(作为参数和返回值逃出方法作用范围时需要考虑线程安全问题)
例如:
以下代码中局部变量是私有的,是线程安全的

	//多个线程同时执行该方法,会不会造成x值混乱呢?
	//不会,因为x是方法内的局部变量,是线程私有的,互不干扰
	static void m1(){
		int x=0;
		for(int i=0;i<p>但是如果我们把变量的类型改为static,此时就大不一样了,x是静态变量,线程1和线程2同时拥有同一个x,static变量针对多个线程是一个共享的,不加安全保护的话,就会出现线程安全问题。</p><pre class="brush:php;toolbar:false">	static void m1(){
		static int x=0;
		for(int i=0;i<p>我们再看几个方法</p><pre class="brush:php;toolbar:false">public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        sb.append(4);
        sb.append(5);
        sb.append(6);
        new Thread(()->{
            m2(sb);
        }).start();
    }
    public static void m1() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }
    public static void m2(StringBuilder sb) {
        sb.append(1);
        sb.append(2);
        sb.append(3);
        System.out.println(sb.toString());
    }
    public static StringBuilder m3() {
        StringBuilder sb = new StringBuilder();
        sb.append(1);
        sb.append(2);
        sb.append(3);
        return sb;
    }

m1是线程安全的:m1中的sb是线程中的局部变量,它是属于线程私有的
m2线程不安全:sb它是方法的参数,有可能有其它的线程访问到它,它就不再是线程私有的了,它对多个线程是共享的。
m3不是线程安全的:它被当成返回结果返回了,返回了有可能其它的线程拿到这个对象,从而并发的修改。

3.5.栈内存溢出(StackOverflowError)

什么情况下会导致栈内存溢出呐?
1.栈帧过多导致栈内存溢出(一般递归调用次数太多,进栈太多导致溢出)
这里最容易出现的场景是函数的递归调用。
2.栈帧过大导致栈内存溢出(不太容易出现)

栈内存溢出代码演示1(自己开发):
测试以下的程序,其中递归函数没有递归边界

public class Demo1_2 {
	private static int count;
    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }
    private static void method1() {
        count++;
        method1();
    }}

运行结果如下
JVM 学習 Java メモリ構造

JVM 学習 Java メモリ構造
这里报了错误StackOverflowError
总共进行了22846次递归调用

idea中设置栈内存大小:
JVM 学習 Java メモリ構造
将栈内存设置的小一点,发现5000多次递归调用就溢出了。

栈内存溢出代码演示2(第三方依赖库出现):
JVM 学習 Java メモリ構造
本案例可以使用JsonIgnore注解解决循环依赖,数据转换时,只让部门类去关联员工类,员工类不再关联部门类,在员工类的部门属性(dept)上加@JsonIgnore注解。具体使用详情可以点击此处查看

3.6.线程运行诊断

3.6.1.案例1:cpu占用过多(linux系统为例)

排查步骤:

1.在linux中使用top命令,去查看后台进程对cpu的占用情况
注意,在这之前我们运行了一道Java程序
JVM 学習 Java メモリ構造
Java代码占用了CPU的99.3%.top命令只能定位到进程,而无法定位到线程。

2.查看线程对cpu的占用情况:ps H -eo pid,tid,%cpu
如果显示过多,可使用ps H -eo pid,tid,%cpu | grep 进程id,过滤掉不想看的部分进程

注意:ps不仅可以查看进程,也可以查看线程对CPU的占用情况。H把进程中的线程所有信息都展示出来。-eo规定输出感兴趣的内容,这里我们想看看pid,tid和CPU的占用情况%cpu
JVM 学習 Java メモリ構造
当线程数太多,排查不方便的话,我们可以用grep pid来进行筛选,过滤掉不感兴趣的进程
ps H -eo pid,tid,%cpu |grep 32655

3.定位到是哪个线程占用内存过高后,再使用Jdk提供的命令(jstack+进程id)去查看进程中各线程的运行信息,需要把第二步中查到的线程id(十进制)转为十六进制,然后进行比较查询到位置后判断异常信息
JVM 学習 Java メモリ構造
thread1,thread2,thread3是我们自己定义的线程。
可以根据线程id,找到有问题的线程,进一步定位到问题代码的源码行号
JVM 学習 Java メモリ構造

3.6.2.案例2:线程诊断_迟迟得不到结果

仍然通过jdk提供的 jstack+进程id的方式,去查看进程中各个线程的运行信息
JVM 学習 Java メモリ構造
JVM 学習 Java メモリ構造

4.本地方法栈

含义:Java虚拟机调用本地方法时,需要给本地方法提供的一些内存空间
本地方法不是由Java编写的代码,由于Java有时不能直接和操作系统打交道,所以需要用C/C++语言来与操作系统打交道,那么Java就可以通过调用本地方法来获得这些功能。本地方法非常的多,如Object类的clone(),hashCode方法,wait方法,notify方法等

public native int hashCode();

5.堆

5.1.定义

1.虚拟机栈,程序计数器,本地方法栈,这些都是线程私有的,而堆和方法区,是线程公用的一块内存区域
2.通过new关键字创建的对象都会使用堆内存
3.由于堆是线程共享的,堆内的对象都要考虑线程安全问题(也有一些例外)
4.堆有垃圾回收机制,不再被引用的对象会被回收

5.2.堆内存溢出(OutOfMemoryError:Java heap space)

对象一直存在于堆中未被回收,且占用内存越来越大,最终导致堆内存溢出(虽然堆中有垃圾回收机制,但垃圾回收机制不是回收所有的对象)
我们可以看看下面的代码

public static void main(String[] args) {
        int i = 0;
        try {
            List<string> list = new ArrayList();
            String a = "hello";
            while (true) {
                list.add(a); // hello, hellohello, hellohellohellohello ...
                a = a + a;  // hellohellohellohello
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }}</string>

JVM 学習 Java メモリ構造
报了错误java.lang.OutOfMemoryError
代码中每次都拼接一个hello,由于定义的list集合创建在try语句里面,所以在for循环不断执行过程中,list集合是不会被回收的,只要程序还没到catch之前,它就一直有效。而字符串对象都被追加到了集合内部,字符串对象由于一直被使用,所以不会被回收。
我们可以通过-Xmx来设置堆空间大小。
JVM 学習 Java メモリ構造
我们把堆内存改成8M(之前内存是4G),此时只运行了17次。

5.3.堆内存诊断

1.jps工具:jps,查看当前进程中有哪些Java进程,并将进程id显示出来(idea中通过terminal命令行输入命令)
2.jmap工具:jmap -heap 进程id 查询某一个时刻堆内存的占用情况
3.jconsole工具:图形界面的,多功能监测工具,可连续监测,使用流程图如下(1-2-3):

6.方法区

6.1.定义

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。(与类有关的信息)。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是他却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来方法区在虚拟机启动时创建
对于习惯在HotSpot虚拟机上开发、部署程序的开发者来说,很多都更愿意把方法取称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区,或者说使用永久代来实现方法区而已,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。原则上,如何实现方法区属于虚拟机实现细节,不受虚拟机规范约束,但使用永久代来实现方法区,现在看来并不是一个好主意,因为这样更容易遇到内存溢出问题(永久代有-XX:MaxPermSize的上限,J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB,就不会出现问题),而且有极少数方法(例如String.intern())会因这个原因导致不同虚拟机下有不同的表现。因此,对于HotSpot虚拟机,根据官方发布的路线图信息,现在也已放弃永久代并逐步改为采用Navtive Memory来实现方法区的规划,在JDK1.7的HostSpot中,已经把原本放在永久代的字符串常量池移出,jdk1.8中后称作元空间,用的操作系统内存。
Java虚拟机规范对方法区的限制非常宽松,除了和Java堆一样不需要连续的内存和可以喧嚣而固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样“永久”存在了。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说,这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确实是必要的。在Sun公司的BUG列表中,曾出现过的若干个严重的BUG就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

文原文关于虚拟机的定义:

JVM 学習 Java メモリ構造

6.2.定义

jdk1.8之前,方法区是用的堆内存,1.8之后,方法区用的操作系统内存。
这块不是太清晰,可以参考下此篇博客点击查看
常量池分为静态常量池和动态常量池,下图中的常量池指的是动态常量池,因为它们已经被读入内存中去,而静态常量池存在于class文件中
JVM 学習 Java メモリ構造

6.3.方法区内存溢出(OutOfMemoryError: Metaspace)

1.8以前会导致永久代内存溢出

1.8以后会导致元空间内存溢出

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            
            for (int i = 0; i <p><strong>jdk1.8以后, 默认情况下,方法区用的是系统内存,所以加大还是不会导致内存溢出,循环很多次都运行成功。</strong><br><strong>当设置了-XX:MaxMetaspaceSize=8m,到了5411次就溢出了。报的是java.lang.OutOfMemoryError: Metaspace错误</strong></p><p>而1.8以前永久代溢出报的错误是java.lang.OutOfMemoryError:PermGen space</p><p><strong>6.4.常量池</strong></p><p><img src="https://img.php.cn/upload/article/000/000/052/b93dd11973d3f8094228c146b3131c12-17.png" alt="JVM 学習 Java メモリ構造"></p><p><strong>常量池,就是一张表,虚拟机指令根据这站常量表找到要执行的类名、方法名、参数类型、字面量信息(如字符串常量、true和false)</strong>。<br><em><em>运行时常量池,常量池是</em>.class文件中的,当该类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址</em>*。</p><pre class="brush:php;toolbar:false">public class HelloWorld {
	public static void main(String[] args) {
		System.out.println("hello,world");
	}}

以上是一个helloworld程序,helloworld要运行,肯定要先编译成一个二进制字节码。
二进制字节码由类的基本信息、常量池、类方法定义(包含了虚拟机指令)
反编译HelloWorld(之前需要运行将.java文件编译成.class文件)
使用idea工具
JVM 学習 Java メモリ構造

F:\IDEA\projects\jvm>javap -v F:\IDEA\projects\jvm\out\production\untitled\HelloWorld.class

F:\IDEA\projects\jvm\out\production\untitled\是HelloWorld.class所在的路径

显示类的详细信息

Classfile /F:/IDEA/projects/jvm/out/production/untitled/HelloWorld.class
  Last modified 2021-1-30; size 533 bytes
  MD5 checksum 82d075eb7217b4d23706f6cfbd44f8f1
  Compiled from "HelloWorld.java"public class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

可以看到类的文件,最后修改时间,签名。以及版本等等。有的还有访问修饰符、父类和接口等详细信息。

显示常量池

Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // hello,world
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LHelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               hello,world
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V</init></init></init>

显示方法定义

{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature            0       5     0  this   LHelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello,world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature            0       9     0  args   [Ljava/lang/String;}</init>

第一个方法是public HelloWorld();它是编译器自动为我们构造的无参构造方法。
第二个是public static void main(java.lang.String[]);即main方法
方噶里面就包括了虚拟机的指令了。
getstatic获取一个静态变量,即获取System.out静态变量
ldc是加载一个参数,参数是字符串hello,world
invokevirtual虚方法调用,println方法
return执行结束。
我们getstatic、ldc、invokevirtual后面都有一个#2,#3,#4。在解释器翻译这些虚拟机指令的时候,它会把这些#2,#3,#4进行一个查表翻译。比如getstatic #2,就去查常量池的表。在常量池中
#2 = Fieldref #21.#22 引用的是成员变量#21,#22.
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
然后再去找#28.29,30
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
所以现在我就知道了,我是要找到java.lang.system类下叫out的成员变量,类型是java/io。
同理,ldc是找#3 = String #23 Utf8 hello,world,它是虚拟机常量池的一个字符串。把helloworld常量变成字符串对象加载进来。
invokevirtual #4 Methodref #24.#25 等等
所以常量池的作用就是给我们指令提供一些常量符号,根据这些常量符号,我们就可以根据查表的方式去找到它,这样虚拟机才能成功的执行它。

相关免费学习推荐:java基础教程

以上がJVM 学習 Java メモリ構造の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。