内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高校稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨经典的JVM内存布局。
Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
灰色的为单独线程私有的,红色的为多个线程共享的。即:
如果你使用jconsole或者任何一个调试工具,都能看到在后台有许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己创建的线程。
这些主要的后台系统线程在Hotspot JVM里主要是一下几个:
虚拟机线程: 这种线程的操作是需要JVM达到安全点才会出现,这些操作必须在不同的线程中发生的原因是他们都需要JVM达到安全点,这样堆才不会发生变化。这种线程的执行类型包括“stop the world"的垃圾收集,线程收集,线程挂起以及偏向锁撤销。
周期任务线程: 这种线程是时间周期时间的体现(比如中断),他们一般用于周期性操作的调度执行
GC线程: 这种线程对在JVM里不同种类的垃圾收集行为提供了支持
编译线程: 这种线程在运行时会将字节码编译成本地代码
信号调度线程: 这种线程接收信号并发送给JVM,在它内u通过调用适当的方法进行处理。
JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息,CPU只有把数据装载到寄存器才能够运行,这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对无力PC寄存器的一种抽象模拟。
作用:
PC寄存器用来存储指向下一条指令的地址,即将要执行的指令代码,由执行引擎读取下一条指令。
每个线程都会有一个程序计数器,是线程私有的
1、我们先写一段测试代码
package com.jvmTest.userClassLoader;
public class Test {
public static void main(String[] args) {
int i=10;
int j = 20;
int k=i+j;
String str="abc";
System.out.println(k);
System.out.println(str);
}
}
2、运行编译后,输入命令将该类进行反编译,开始分析控制台输出的数据
cd target/classes/com/jvmTest/userClassLoader
javap -verbose Test.class
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
由于跨平台性的设计,Java的指令集都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点是跨平台,指令集小,编译器容易实现
缺点是性能下降,实现同样的功能需要更多的指令。
Java虚拟机栈,早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。是 线程私有的,生命周期和线程一致不存在垃圾回收
作用:
主管Java方法程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
优点:
关于Slot的理解
局部变量表,最基本的存储单元是Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位类型(long和double)占用两个slot
栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么其作用域之后申明新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的
重点
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
栈空间所能调用方法个数的影响因素:
局部变量表的生命周期:
栈可以使用数组或链表来实现,这里采用的数组实现的
每一个独立的栈帧中除了包含局部变量表以外,还包含一个先进后出的操作数栈,也可以称之为表达式栈
栈帧中保存了一个引用,相当于C语言中的指针,指向该方法在运行时常量池中的位置。通过运行时常量池的符号引用(指向堆),完成将符号引用转化为直接引用。
在Java源文件被编译到字节码文件中时,所有的变量和方法引用(引用可理解为调用)都作为符号引用( symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
为什么需要常量池呢?
常量池的作用就是为了提供一些符号和常量,便于指令的识别,另一方面在调用其他类的方法时,不需要将整个类装入到栈帧中,只需保存地址引用,方便了调用
栈中可能出现的异常
如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
如果Java虚拟机栈可以动态扩展。并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机栈会抛出一个OutofMemoryError异常。
1、举例栈溢出的情况?(StackOverflowError)
当方法出现互相调用,即死循环调用的时候会出现栈溢出
2、调整栈的大小,就能保证不出现栈溢出吗?
不能,只能保证方法多调用几次,但不能保证不出现栈溢出
3、分配的栈内存越大越好吗?
不是,一个栈的大小变化会影响其他的栈空间和其他的内存空间,一个栈越大,其他的栈就会变小
4、垃圾回收是否会涉及到虚拟机栈?
不会的
5、方法中定义的局部变量是否线程安全?
具体问题具体分析
到此我们就将 运行时数据区的 程序计数器和栈讲解完了,下一篇我们将讲解剩下的运行时数据区的各个部分模块,记得关注哦
推荐阅读
上一篇:三、JVM虚拟机之类加载系统详解
下一篇:五、JVM从入门到精通之运行时数据区分析(篇二)