【后端面经-Java】JVM内存分区详解

Posted on 2023-07-17 09:00  CrazyPixel  阅读(93)  评论(0编辑  收藏  举报

@

1. JVM内存分区简介

JVM内存分区如图所示:

主要有如下几个区域:

  • 栈(Stack)
  • 堆(Heap)
  • 方法区(Method Area)
  • 程序计数器(PC)
  • 本地方法栈(Native Method Stack)

其中,程序计数器用于存储线程当前执行的指令地址(记录进度),程序计数器是线程私有的;
本地方法栈并不是每个JVM都必须实现,而是针对支持native本地方法调用的JVM。

  • 本地方法:使用非Java语言定义的方法;
  • 本地方法栈同一般的JVM栈一样,可分配固定或者动态内存。

剩余的三个区域:栈、堆、方法区,在下文中会详细介绍。

2. JVM栈

Java栈的基本存储单元为栈帧,每个线程对应一个栈帧,不同栈帧之间不会共享数据。
栈帧主要包括:

  • 局部变量:在方法内部定义的变量
  • 操作数栈:函数形参
  • 指向运行时常量池的引用
  • 方法返回地址
  • 附加信息

JVM栈的内存大小可以固定设置也可以动态分配,当出现栈溢出的时候,固定内存的栈会抛出StackOverFlowError,而动态分配的栈则会抛出OutOfMemoryError

3. JVM堆

JVM堆中存放的是对象实例(说人话就是用new创建的对象,因为数组也是new int[]这种形式创建的,所以JVM将数组看作一种特殊的对象)。
JVM堆是线程共享的,可动态分配内存。
需要注意的是,当一个方法结束之后,JVM栈中的相关数据会出栈,释放内存,但是对于JVM堆,虽然创建对象的方法已经消失,但是对象本身并不会在堆中被销毁,而是会等待垃圾回收机制进行内存回收。

4. JVM方法区

JVM方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
方法区是线程共享的。
方法区中有个区域叫做常量池,包括静态常量池运行时常量池
静态常量池存放的是编译时就能够确定的常量数据,包括一些常数、类信息、方法信息等等;
运行时常量池存放的是运行时才能够确定的常量数据。

需要注意的是,运行时常量池在JDK1.7之后不再放在方法区中,而是放在堆中。

因此,对于常见的字符串常量池,如果一个字符串在编译时可以确定值,那么就放在方法区静态常量池中,如果在运行时遇到字符串先查找常量池,常量池中没有该字符串,则该字符串被创建,创建的对象会放在运行时常量池中,放在方法区还是堆中就需要根据版本来确定。

5. JVM内存分配实例

public class Demo {
	String username;
	public void method() {
        int i=1;
		System.out.println("执行类方法");
	}
	public static void main(String[] args) {
		int i=1;
		String str="hello java";
		Demo demo=new Demo();
		demo.username="123";
		demo.method();
		
	}
 
}
//出处:https://blog.csdn.net/m0_57816620/article/details/127332057?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-127332057-blog-125451232.235%5Ev38%5Epc_relevant_anti_t3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~CTRLIST~Rate-1-127332057-blog-125451232.235%5Ev38%5Epc_relevant_anti_t3&utm_relevant_index=2

对于上面的Java代码,各位可以思考一下内存分配的具体情况。

  1. Demo类编译的时候产生.class文件,放在方法区的静态常量池中,存储类信息;
  2. 从Demo类的main方法进入,方法main的内存地址入栈,
  3. int i=1;创建一个局部变量i,值为1,入栈;
  4. String str="hello java";创建一个字符串变量str,入栈;对于hello java这个字符串常量,JVM查找静态常量池运行时常量池中是否存在该字符串,如果存在,字符串变量str直接指向该字符串常量,如果不存在,则创建一个字符串常量hello java,并将该字符串常量放入运行时常量池中,然后字符串变量str指向该字符串常量;
  5. Demo demo=new Demo()创建一个对象变量demo,入栈;对于对象实例本身,则会创建之后存于堆中,然后demo变量指向该对象实例;
  6. demo.username="123"修改类中的属性值,在堆中设置username变量,值为123;
  7. demo.method()调用类方法,方法定义存于JVM方法区中,取出该方法信息,然后进入method方法,开启新一个线程,JVM栈帧加1,存放method方法的内存地址;
  8. 执行method方法,int i直接将局部变量入栈;
  9. System.out.println("执行类方法");则是调用System.out.println方法(库方法);
  10. method方法执行完毕,栈帧出栈;
  11. main方法执行完毕,栈帧出栈。
  12. 栈中无栈帧,结束进程;而此时堆中依然保留Demo对象实例,等待垃圾回收机制回收。

面试模拟

Q:JVM堆、栈、方法区的作用
A:JVM栈存放方法调用时的局部变量、形参、返回地址等信息,每个方法调用的时候都会产生一个栈帧,出入栈操作实现方法的嵌套调用;
堆中存放的是对象实例或者数组,是线程共享的,可动态分配内存;
方法区存放的是一些静态变量和常量数据,包括类编译的方法和属性信息,静态常量池和运行时常量池在JDK1.6均属于此类。

参考资料

  1. Java JVM 中 堆,栈,方法区 详解
  2. Java堆、栈、方法区、常量池
  3. java内存分配(堆,栈,方法区,常量池)图解
  4. Java内存模型(JMM)总结