一:jvm的五大内存区(内存结构)
五大内存区
jvm五大内存区域(即jvm运行时数据区),描述的是类被加载时,经过解析后,存储到特定的数据区。方法区和堆是所有线程共享的,而栈和计数器是线程私有的。栈处理程序运行的问题,堆处理数据的存储问题。所以才有堆栈分离。
方法区:又被称为元空间,用来存储类的信息,例如:方法,方法名,返回值,常量。当它无法满足内存分配需求时,方法区会抛出OutOfMemoryError。
堆:存放new出来的对象信息, 全局变量。
程序计数器:指向当前线程正在执行的行号,用来保证线程切换时回到程序调用的位置。(例如:在a方法里面掉用了b方法,代码从上往下执行,执行到掉用b方法的那行时,指针会记录下这个位置,然后执行b方法里面的逻辑,b方法正常执行完或异常退出,指针都会回到a方法里面。)
案例:我们这里可以写一个java文件,通过javac命令编译生成一个class文件,然后通过javap -l 命令查看程序的行号和局部变量表。
本地方法栈:和虚拟机栈类似,只是它描述的是为虚拟机是用到的Native方法出栈和入栈的过程(通常我们不需要了解这块,它底层是C语言实现的)。
虚拟机栈:描述的是线程进栈出栈的过程,线程结束内存自动释放。它用来存储当前线程运行方法所需要的数据、指令、返回地址。(即局部变量和正在调用的方法) 方法被调用时会在栈中开辟一块叫栈帧的空间,方法运行在栈帧空间中。栈帧出栈后,里面的局部变量直接就从内存里清理掉了。
先进后出
// main方法先入栈,然后程序从上往下执行,a 和 b 相继入栈,程序执行完之后出栈,可以看到main方法最后执行完,所以栈是先进后出的。 public class Test { public static void main(String[] args) { a(); System.out.println("main..."); } public static void a(){ b(); System.out.println("a..."); } public static void b(){ System.out.println("b..."); } } ========== 控制台输出 =========== b... a... main...
栈帧的逻辑内存图里面包含局部变量表,操作数栈,动态链接,出口...(注意:栈帧里面包含但不仅仅只有这些, 程序员开发主要是关注这些 )。
public static void demo(){ /** * 【局部变量表】 存放该方法的参数变量,和方法内部定义的局部变量,若该变量为引用类型,则存的该变量的引用地址。在java文件编译成.class文件的时候,这个表的容量最大值就确定下来了。 */ String a = "局部变量a"; String b = "局部变量b"; int c = 2,d = 3; // 局部变量c和d /** * 【操作数栈】 也称为表达式栈,通过字节码指定把值压入栈定,稍后另一个指令就可以弹出这个值使用。 */ int e = c*d; // 操作数栈 /** * 【动态链接】 也称为常量池。用来保存常量值和符号引用。符号引用:A方法在运行时调用B方法,就是通过B方法的符号引用去找到B的内存地址.) */ UserDao dao = new UserDao(); dao.insert(a); /** * (出口):描述的就是出栈的过程。方法的返回有两种情况。(正常退出时:根据方法的定义来决定是否要传返回值给上层调用者。异常退出时:则是需要通过异常处理来确定。无论通过哪种方式退出,都会跳到当前方法被调用的位子。 */ }
* 如果线程请求的栈深度⼤大于虚拟机所允许的深度,将抛出 StackOverflowError 异常
代码运行流程
比如现在有这么一段代码
1. 首先main线程会来执行User类的main方法。main线程会有自己的一个虚拟机栈,他会把main方法的栈帧压入虚拟机栈中。
2. 然后main()方法里面又调用了loadUserId()方法,此时就会创建一个loadUserId()方法的栈帧,并压入main线程的虚拟机栈中。
3. 此时loadUserId()方法中,有一个局部变量 order
4. 然后创建了一个 Order 类的实例对象,此时就会在java堆内存中分配这个实例对象的内存空间。并且order变量会指向Order实例对象的内存空间
5. 最后就会通过局部变量 order 引用Order的实例对象去执行它的getUserId()方法了