【JVM】运行时内存数据区(main完整流程实例分析)

1.区域划分:

在这里插入图片描述

Java虚拟机栈

1.Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)

2.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常; 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)
3. Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。


栈帧(Stack Frame)  
栈帧(StackFrame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。

栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。

因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

注意:  在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。

执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

堆里存放的是对象的实例
是Java虚拟机管理内存中最大的一块
GC主要的工作区域,为了高效的GC,会把堆细分更多的子区域

线程共享

方法区域

存放了每个Class的结构信息,包括常量池、字段描述、
方法描述
GC的非主要工作区域

本地方法栈

本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java
虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java
虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

摘自链接:https://www.jianshu.com/p/8a775d747c47

程序计数器

程序计数器(Program Counter
Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
                                             ------ 摘自《深入理解JAVA虚拟机》

实例分析1:

通过一个实例来彻底了解每部分区域的作用,并结合之前类加载器和字节码指令的知识点做相关分析

public class JText7 {
		public static void main(String[] args) {
			Son son = new Son();
			son.Math();
		}
}

class Son{
	private static int a = 1;
	private String str = "Hello";
	private static final int b = 5;
	
	public Son() {
		
	}
	
	public void Math() {
		int a = 1;
		int b = 2;
		int c = (a + b)*2;
		
		
	}
}

首先随着该线程的启动,JVM给该线程分配好虚拟机栈,本地方法栈,程序计数器等线程私有物,因为是对该类的主动使用(执行main方法),该类位于classpath下会先被加载,在Appclassloader 的命名空间中保存该类的Class对象,然后在内存的方法区中保存该类的元数据信息,并初始化该类(执行静态方法视为主动使用,必然会导致该类被初始化) 。
开始执行mian方法,main方法对应的栈帧入栈。

在这里插入图片描述
开始执行该方法对应的字节码指令,该方法字节码指令对应如下

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=2, locals=2, args_size=1
       0: new           #16                 // class com/mec/JVM/Son
       3: dup
       4: invokespecial #18                 // Method com/mec/JVM/Son."<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #19                 // Method com/mec/JVM/Son.Math:()V
      12: return
    LineNumberTable:
      line 5: 0
      line 6: 8
      line 7: 12
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      13     0  args   [Ljava/lang/String;
          8       5     1   son   Lcom/mec/JVM/Son;

程序中先new了一个Son实例,这里再次对new的过程做出说明:
Son son = new Son();
通过上面字节码指令可以看出, newSon实例包含以下几步 :
new :在堆内存中创建一个对象的实例
dup:复制该对象的空间首地址值并压栈
invokespecial :调用构造方法,为实例成员赋初值
astore 1: 将操作数栈顶的值弹出并存入局部变量表索引1的位置,通过局部变量表可以看到1的位置就是son,也就是返回空间首地址存给son引用。
也就是说,保存在main方法栈帧对应的局部变量表里的 son 引用存入的是该空间首地址占4字节!所以说引用在栈里,new在堆里。

顺便一提,这里因为由于实例化对Son类主动使用了,势必会引起该类的初始化,那该类肯定会先被加载,在内存方法区中保存该类的元数据,并在加载该类的Appclassloader中保留他的Class对象,然后初始化,给该类的静态成员a赋值,初始化完了后对该类实例化,就是上面的步骤,分配空间,调用构造方法给实例成员赋值,b是常量,其值在编译时期已经确定直接就在常量池中。直接从常量池去取指推送就行,故也在构造时赋值,整个流程就完毕了(在反编译那篇博文做了更为详细的分析,有疑惑的可以在看看)。

然后执行son.Math();
先把第一个引用也就是son从main方法栈帧中的局部变量表中取出,invokevirtual 调用Math方法,Math方法对应栈帧入栈,
在这里插入图片描述
开始执行Math方法对应字节码指令:图中做了具体分析

public void Math();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=4, args_size=1
       0: iconst_1          将int型常量1压入该栈帧中的操作数栈
       1: istore_1	        将栈顶int型值存入局部变量表索引为1的位置处,从下面局部变量可以看到1     a   I,就是放入a处
       2: iconst_2	        将int型常量2压入该栈帧中的操作数栈
       3: istore_2		    将栈顶int型值存存入局部变量表索引为2的位置处(b)
       4: iload_1		 	取出局部变量1int值,压入操作数栈
       5: iload_2			取出局部变量2int值,压入操作数栈
       6: iadd			    2弹出栈后1弹出栈,执行加法后结果入栈(此时操作数栈中仅存计算结果37: iconst_2		    将int型常量2压入该栈帧中的操作数栈
       8: imul			    2弹出栈后3弹出栈,执行乘法后结果入栈(此时操作数栈中剩了69: istore_3		    将栈顶int型值存入局部变量表索引为3的位置处(c)
      10: return			方法执行完毕返回
    LineNumberTable:
      line 21: 0
      line 22: 2
      line 23: 4
      line 26: 10
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      11     0  this   Lcom/mec/JVM/Son;
          2       9     1     a   I
          4       7     2     b   I
         10       1     3     c   I
}

执行完Math方法后,根据Math方法的栈帧中对应的方法返回地址找到main方法的执行位置。该方法彻底执行完毕,然后Math栈帧出栈。

顺带一提程序计数器的修改和方法区中元数据指令都是由字节码执行引擎来操作的,mian方法执行完了对应栈帧出栈,整个程序执行完毕(只有一个主线程main)JVM也就退出了。
在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
Java支持多线程,当Java程序执行main方法的时候,就是在执行一个名字叫做main的线程,可以在main方法执行时,开启多个线程A,B,C,多个线程 main,A,B,C同时执行(都在就绪态),相互抢夺CPU,所以main线程和他的子线程都会被分配到线程的私有物:程序计数器,栈,本地方法栈,然后他们互相抢夺cpu并在自己的栈中执行对应方法。至于用时启动多个main,那就是开了多个进程对应多个JVM,他们所属类都会被加载,他们都有自己各自的堆,还有自己各自线程被分配到资源然后都去争抢cpu,因为windos是抢占式调度,所以每个线程都会根据自己的优先级去抢cpu以此来执行自己的工作!

面试题分析

问输出结果是是什么,可以先思考下

public class XText2 {
	public static void main(String[] args) {
		int i = 1;
		i = i++;
		int j = i++;
		int k = i+ ++i*i++;
		
		System.out.println(i);
		System.out.println(j);
		System.out.println(k);
	}
}

结果(自己试试加深印象):
在这里插入图片描述
原理,还是通过字节码文件解释:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: iconst_1      int型常量1入操作数栈
         1: istore_1	      1给i
         2: iload_1		   取出i的值1入栈
         3: iinc          1, 1     把局部变量表i的值自增1变为2
         6: istore_1      把操作数栈顶的值1写入局部变量表的i中,i又被覆盖从2变回1
         7: iload_1		    在取出i的值1入栈
         8: iinc          1, 1     把局部变量表i的值自增1变为2
        11: istore_2	   把栈顶1存入j  此时i=2 j=1
        12: iload_1       取出i的值2入栈
        13: iinc          1, 1    把局部变量表i的值自增2变为3
        16: iload_1       取出i的值为3压栈
        17: iload_1		  取出i的值为3在压栈    此时操作数栈中从上往下是3  3  2
        18: iinc          1, 1     把局部变量表i的值自增3变为4   此时i=4,j=1
        21: imul          栈顶两个数 33 出栈相乘结果9在入栈
        22: iadd		  92出栈相加结果11入栈
        23: istore_3      把11出栈存入k中       此时 i= 4 ,j=1 , k=1124: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        27: iload_1
        28: invokevirtual #22                 // Method java/io/PrintStream.println:(I)V
        31: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        34: iload_2
        35: invokevirtual #22                 // Method java/io/PrintStream.println:(I)V
        38: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
        41: iload_3
        42: invokevirtual #22                 // Method java/io/PrintStream.println:(I)V
        45: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 7
        line 8: 12
        line 10: 24
        line 11: 31
        line 12: 38
        line 13: 45
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      46     0  args   [Ljava/lang/String;
            2      44     1     i   I
           12      34     2     j   I
           24      22     3     k   I
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JVM内存模型是Java虚拟机在运行所使用的内存分配和管理方式。它包括了运行数据,也就是JVM内存中划分的不同域,用来存储程序的数据和指令。 JVM运行数据主要包括以下几个部分: 1. 堆(Heap):用于存储对象实例和数组。堆是线程共享的域,所有线程共同使用堆来创建和访问对象。 2. 方法(Method Area):用于存储已加载的类信息、常量、静态变量和编译后的代码等。方法也是线程共享的域,它在内存中占用一块连续的空间。 3. 虚拟机栈(VM Stack):每个线程在创建都会分配一个虚拟机栈,用来存储局部变量和方法调用信息。虚拟机栈是线程私有的,每个线程都有自己独立的虚拟机栈。 4. 本地方法栈(Native Method Stack):与虚拟机栈类似,用于存储本地方法调用的相关信息。 5. 程序计数器(Program Counter Register):用于存储当前线程执行的字节码指令的地址。 这些不同的运行数据JVM内存模型中起着不同的作用,可以提供给程序运行所需的各种资源和环境。例如,堆用于存储对象实例,方法用于存储类信息和静态变量,虚拟机栈用于存储方法的局部变量和方法调用信息等。 总的来说,JVM内存模型和运行数据是Java虚拟机在运行所使用的内存管理和分配方式。它们的不同域有不同的作用,用来存储程序的数据和指令。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [运行数据JVM内存模型](https://blog.csdn.net/weixin_45659364/article/details/124027073)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [JVM:Java内存模型与运行数据域](https://blog.csdn.net/m0_71777195/article/details/131655107)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [Java内存模型与JVM运行数据别详解](https://download.csdn.net/download/weixin_38648037/12745990)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值