JVM 内存结构划分
从java代码精力编译生成对应字节码文件,再经由类加载器加载,经历加载,验证,解析,初始化,阶段,整个过程我们称之为类加载阶段,也就是我们JVM 第一部分重要的开端
JVM内存模型图
JVM各个区域说明
- 程序计数器:用于记录将要执行的JVM指令地址
- 虚拟机栈:每个线程运行时所需要的内存,称为虚拟机栈;每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 本地方法栈:保存navtive方法进入区域的地址
- 堆:通过new关键字创建对象都会使用堆内存
- 方法区:存储类的信息以及运算时常量池
程序计数器
程序计数器(Program Counter Register) 同时也叫PC寄存器
作用:是记住下一条Jvm指令的执行地址
特点:
- 是线程私有的
- 不会存在内存溢出
JVM是不识别我们写的代码的,我们的java代码会被编译为.class字节码文件,而字节码文件中的代码才是jvm能试别和执行的,这些代码我们也叫做【
字节码指令
】,他对应了一条一条的机器指令,JVM通过将这些指令在解释翻译成为机器指令,来操作我们的计算器进行执行,例如以下的代码public class Demo1{ public static void main(String[] args){ int num1 = 1; System.out.println(num1); int num2 = 2; System.out.pringln(num2); } }
上述代码的字节码指令为
0 iconst_1 1 istore_1 2 getstatic #2 <java/lang/System.out> 5 iload_1 6 invokevirtual #3 <java/io/PrintStream.println> 9 iconst_2 10 istore_2 11 getstatic #2 <java/lang/System.out> 14 iload_2 15 invokevirtual #3 <java/io/PrintStream.println> 18 return
注意:
这些字节码指令就是由字节码执行引擎来执行的JAVA程序编译后的.class文件生成的字节码指令是由JVM基于线程执行的,那么在执行的时候会涉及到上下文切换的问题,这个时候就涉及到一个专门负责记录每个线程当前执行字节码指令语句坐标的空间,叫做程序计数器
由于JAVA多线程的的执行,我们的程序可能会开启多个线程并发执行不同的代码,所以会对应着有多个线程并发执行不同的代码指令,而每个线程底层是通过CPU分配给她时间片的方式,以此轮流来执行的,可能A线程执行一段时间后就切换成B线程执行了,B线程执行时间结束后,再切换回A线程执行,此时A线程需要知道自己上一次执行到什么地方了,才能再上次的位置继续执行下去
所以程序计数器扮演了一个这样的角色:记录每个线程执行字节码指令位置,并且程序计数器每个线程都是私有的,专门为各自线程记录每次执行字节码指令的位置,方便下次线程切换回来时还能找到上次执行的位置继续执行
虚拟机栈
概念
定义
- java Virtual Machine Stacks (java虚拟机栈)
- 每个线程运行时所需要的内存,成为虚拟机栈
- 每个栈有多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
解释
- 每个线程运行时所需要的内存,称为虚拟机栈 --> 每个线程都有自己的JAVA虚拟机栈
java虚拟机一方法作为基本的执行单元,"栈帧"(Stack Frame)则是用于支持虚拟机进行方法调用和方法执行背后的数据结构,它也是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素
- java代码的执行一定是有线程来执行某个方法中的代码,哪怕是我们的main()方法也是有一个主线程来执行的,再main线程执行main()方法的代码指令的时候,就会通过main线程队医你给的程序计数器来记录执行自己执行的指令位置
- main()方法本质上也是一个方法,再main()中也可以带哦用其他的方法,而每个方法中也有自己的局部变量数据,因此JVM提供了一块内存区域用来保存每个方法内部变量等数据,这个区域就是
java虚拟机栈
每个栈有多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个战阵主要存放:局部变量表,操作数栈,动态连接和方法返回地址等信息,每一个方法从调用开始至执行结束的过程,都对应着一个栈帧再虚拟机栈里面从入栈到出战的过程
当我们在一个线程中调用了一个方法,就会对该方法创建一个对应的栈帧,比如说我们下面的代码
public class Demo1{ public static void main(String[] args){ int num1 = 1; System.out.pringln(num1); int num2 = 2; System.out.pringln(num2); } }
当我们运行上面的代码的时候,再JVM虚拟机栈内存中就会先创建一个main方法的栈帧,同时记录保存对应的局部变量,此时的内存模型图为
而此时如果我们再main方法中调用另一个方法的话,如
```java
public class Demo1{public static void main(String[] args){ int num1 = 1; System.out.println(num1); int num2 = 2; System.out.println(num2); method1(); }
public static void method1(){
int num3 = 20;
System.out.println(num3);
}
}
```
此时的虚拟机栈内存模型
- 并且当Method1方法执行完毕之后,弹出该队列,最后弹出来main()方法栈,代表整个main方法代码执行完毕,这也对应了栈数据的特点:先进后出
每个线程都只能有一个活动栈帧,对应着当前正在执行的那个方法
一个线程中的方法调用链可能会很长,以java程序的角度来看,同一时刻,同一条线程里面,在调用堆栈的所有方法都同时处于执行状态,而对于执行引擎来讲,在活动线程中,只有位于栈顶的方法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为"当前栈帧"(Current Stack Frame),与这个栈帧所关联的方法被成为“当前方法”(Current Method),执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作、
注意 虚拟机栈只是一份管理数据的内存区域,只负责存储,真正的执行还是由JVM字节码执行引擎来执行
流程图小结
栈内存面试案例
垃圾回收是否涉及栈内存?
栈帧每次执行结束后会自动弹栈,所以不会涉及到垃圾的产生,也就不会对栈内存进行垃圾回收
栈内存分配越大越好吗?
并不是,假设分配好的物理内存是100MB,每个线程大小是1MB,那么可以分配100个线程,但是如果提升了线程栈的大小,那可以分配的对应的线程数就变少了
官网给出的每个虚拟机栈默认大小分配:
可以看到Linux系统上默认的就是1MB,当然我们可以通过-Xss进行大小的更改
对于不同版本的Java虚拟机喝不同的操作系统,站容量最小值可能会有所限制,这主要取决于操作系统内存分页的大小,譬如上述方法中的参数-Xss128k可以张常使用32位Windows系统下的JDK6,大那是如果用于64位Windows系统下的JDK 11,则会提示栈容量最小不能低于180k,而在Linux下,这个值可能是228K,如果低于这个最小限制,HotSpot虚拟机启动时就会提示:The Java thread Stack size specified is too small, Specify at least 228K
方法内部的局部变量是否线程安全
如果方法内的局部变量没有逃离方法的作用访问,他是线程安全的
如果局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全
栈内存溢出
- 什么原因导致占内存溢出
- 栈帧过多导致栈内存溢出,将抛出StackOverflowError异常
- 什么原因导致占内存溢出
- 栈帧过大也会造成栈内存溢出
- 我们这次可以尝试将每一个栈帧的局部变量多占用一点空间,这样每个栈帧的大小就会变大,我们还是设定每个线程栈空间为128K
- 小结:
- 无论是栈帧太大还是虚拟机栈容量太小,当新的栈帧内存无法分配的时候,HotSpot虚拟机抛出的都是StackOverflowError异常
本地方法栈
本地方法栈(Native Method Stacks)与虚拟机所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到本地(Native)方法服务
说明:
- 对本地方法栈中方法使用的语言,使用方式与数据结构并没有任何强制规定,因此具体的虚拟机可以根据需要自由实现它,甚至有的Java虚拟机(譬如Hot-Spot虚拟机)直接就把本地方法栈和虚拟机栈合二为一
- 与虚拟机栈一样,本地方法栈也会在栈深度溢出或者栈扩展失败是分别抛出StackOverflowError和OutOfMemoryError异常
为什么要使用本地方法?
java使用起来很方便,然而Java代码有一定的局限性,有时候不能和系统底层交互,或是追求程序的效率时,这时候就需要更加底层的语言和更快的运行效率
方便与java之外的环境交互,如与操作系统或者某些硬件交换信息,本地方法为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节
虚拟机本身就是由C++写的,一些操作系统特性JVM没有封装提供出来,那我们就可以自行的使用C语言来实现它,并通过本地方法来调用
追求更快的运行效率
这幅图展示了JAVA虚拟机内部线程运行的全景图,一个线程可能在整个生命周期中都执行java方法,操作他的java栈,或者他可能毫无障碍的在java栈和本地方法栈之间跳转
- 该线程首先调用了两个java方法,而第二个java方法又调用了一个被你的方法,这样导致虚拟机使用了一个本地方法栈,假设这是一个C语言栈,期间有两个C函数,第一个C函数被第二个java方法当作本地方法调用,而这个C函数又调用了第二个C函数,之后第二个C函数又通过本地方法接口毁掉了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个java方法(它成为图中的当前方法)