58、内存分区
前面我们讲了编译执行的整个过程,编译执行的结果会存放在内存中,这其中就包括代码和数据
为了区分存储和管理不同的内容,JVM 将内存划分为不同的分区,其中包括:方法区、程序计数器、堆、虚拟机栈、本地方法栈
本节我们就先从整体上简单介绍一下这几个分区,在接下来的几节中,我们再单独对其中比较复杂的堆进行更加详细的讲解
1、方法区
在第 57 节中我们讲到类加载,类字节码加载到内存中的具体存储位置就是方法区
除此之外方法区中还会存储 JIT 编译得到的机器码,以及专门服务于字符串的字符串常量池,具体主要包含以下这几部分内容
- 类信息:类的全限定名和修饰符(访问标志,如 public、static、final 等),以及父类、接口列表等相关信息
- 方法信息:方法的名称和修饰符(入参、返回值、访问标志等),以及方法的字节码
- 静态变量:静态变量隶属于类,因此存储在方法区中而非堆上
- 运行时常量池:此分区对应类字节码中的常量池,存储字面量和符号引用,每一个类会对应一个运行时常量池
- 字符串常量池:此分区专门服务于字符串,避免 String 对象的重复创建,减少内存开销,跟运行时常量池不同,字符串常量池是类之间共享的
- JIT 编译代码缓存:此分区存储的是 JIT 编译之后的机器码
方法区是 JVM 规范中的抽象分区,落实到具体的 JVM 实现,不同的 JVM 对方法区有不同的实现方式,我们拿常用的 HotSpot JVM 举例说明
- 在 Java 7 以前的版本中,方法区在 HotSpot JVM 中实现为永久代(Permanent Generation)
- 在 Java 7 中,方法区中的字符串常量池和静态变量从永久代中移除,放入到堆中
- 在 Java 7 以后的版本中,永久代被元空间取代(MetaSpace),字符串常量池和静态变量仍然存储在堆中,其他方法区的内容存储在元空间中
在专栏的第 8 节中,我们讲到 String 类型,其中提到的常量池就是这里的字符串常量池
当我们使用字面量或者 intern() 方法对 String 变量赋值时,会在字符串常量池中查找是否存在字符串相同的 String 对象
- 如果存在:直接将对象的引用赋值给 String 变量
- 如果不存在:在字符串常量池中创建 String 对象,再将对象的引用赋值给 String 变量
String s1 = "abc"; String s2 = s1.intern();
很多读者会将 "字符串字面量" 跟 "字符串" 对象混淆
"字符串字面量" 是一个值,是在编译期间通过解析代码得到的,存储在运行时常量池中,用于给其他变量赋值
例如当我们通过 String s = "abc" 来创建对象时,编译器会将运行时常量池中的字面量 "abc",拷贝一份存储在 String 对象的 char value[] 成员变量中
2、程序计数器
在专栏的第一节,当我们讲到 CPU 的各类寄存器时,讲到过程序计数寄存器(Program Counter Register,简称为 PC 寄存器),它用来存储 CPU 下一条要执行的指令的地址
虚拟机实际上就相当于一个抽象的计算机,也有自己的指令集,那就是字节码集
虚拟机在对字节码进行解释执行的过程中,也需要有这样一个存储单元,存储下一条要执行的字节码的地址,虚拟机将这个存储单元称为程序计数器
程序计数器跟 PC 寄存器的不同之处在于
- PC 寄存器是线程共享的,PC 寄存器会随着线程的切换而进行保存和恢复
- 程序计数器是线程私有的,每个线程都会分配一个独立的程序计数器,记录当前线程执行到哪一行字节码
之所有这样的区别是因为
- PC 寄存器作为一个支持 CPU 高速访问的存储器,比较稀缺
- 程序计数器位于内存中,相对寄存器来说非常富裕,线程独享能减少线程上下文切换的信息量,有利于提高线程切换的速度
3、堆
堆用来存储 Java 对象,在 Java 中,对象的回收是由虚拟机中的垃圾收集器自动完成的,堆是垃圾收集器的主要工作分区
为了配合垃圾收集算法,堆又被划分为更加细粒度的分区
- 年轻代(Young Generation)和老年代(Old Generation)
- 年轻代又分为 Eden 区和 Survivor 区
- Survivor 区又分为 From Survivor 区和 To Survivor 区
对于堆我们会在后面的章节中,结合分代垃圾回收算法进行更加详细的讲解
4、虚拟机栈
在本专栏的第 2 节中,我们详细讲解了函数调用的实现原理,其中所涉及到的重要的内存结构便是栈
- 用来存储函数调用的局部变量、参数、返回地址等信息,因此栈也叫做函数调用栈
- 栈为线程私有的,每个线程会有一个栈,因此栈也叫做线程栈
Java 也不例外,我们把 Java 中的栈叫做虚拟机栈
5、本地方法栈
在前面章节中我们讲到,Java 提供了很多使用 C / C++ 语言实现的 native 方法,很多 Java 函数最终是通过调用 native 方法来实现的
在 JVM 规范中,Java 将服务于 Java 方法调用的栈,跟服务于 native 方法调用的栈做了区分
- 服务于 Java 方法调用的栈称为虚拟机栈
- 服务于 native 方法调用的栈称为本地方法栈
这两个栈的功能基本是相同的,因此在具体的虚拟机实现中,比如 HotSpot JVM,这两栈被合并为一个栈,同时存储 Java 方法调用的栈帧和 native 方法调用的栈帧
不同平台下,JVM 的默认线程栈的大小有些许差别
在 64 位 Linux 操作系统下,HotSpot JVM 默认的每个线程的栈大小为 1 MB,我们可以通过 JVM 参数 -Xss 来自定义线程栈的大小
JVM 可创建的线程个数:由进程可用内存大小(一般就是计算机可用内存)除以线程栈大小决定
如果需要创建的线程个数超过可创建线程个数,那么我们可以通过减小线程栈大小来支持创建更多的线程
如果因为函数调用深度太深导致栈溢出(也就是栈内存不够用,JVM 抛出 StackOverflowError),那么我们可以通过增大线程栈大小来避免栈溢出
6、课后思考题
我们知道类中 static 类型的变量存储在方法区,那么类中 final 类型的变量存储在哪里?
final 类型的变量隶属于对象而非类,因此存储在堆中
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17497735.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步