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 取出局部变量1的int值,压入操作数栈
5: iload_2 取出局部变量2的int值,压入操作数栈
6: iadd 2弹出栈后1弹出栈,执行加法后结果入栈(此时操作数栈中仅存计算结果3)
7: iconst_2 将int型常量2压入该栈帧中的操作数栈
8: imul 2弹出栈后3弹出栈,执行乘法后结果入栈(此时操作数栈中剩了6)
9: 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 栈顶两个数 3 和 3 出栈相乘结果9在入栈
22: iadd 9和2出栈相加结果11入栈
23: istore_3 把11出栈存入k中 此时 i= 4 ,j=1 , k=11;
24: 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