😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本专栏《八股消消乐》旨在记录个人所背的八股文,包括Java/Go开发、Vue开发、系统架构、大模型开发、机器学习、深度学习、力扣算法
等相关知识点,期待与你一同探索、学习、进步,一起卷起来叭!
目录
- 题目
- 答案
- JVM
- 作用
- 内存模型
- 堆
- 程序计数器
- 方法区
- 虚拟机栈
- 本地方法栈
- 运行原理
题目
💬技术栈:JVM
🔍简历内容:熟悉JVM内存模型(内存结构)
🚩面试问:你了解JVM内存模型吗?
💡建议暂停思考10s,你有答案了嘛?如果你有不同题解,欢迎评论区留言、打卡。
答案
JVM
作用
JVM 不仅承担了 Java 字节码的分析
(JIT compiler)和执行
(Runtime),同时也内置了自动内存分配管理机制
。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险
,使 Java 开发人员不需要关注每个对象的内存分配以及回收,从而更专注于业务本身。
内存模型
在 Java 中,JVM 内存模型主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈
。
堆
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享
,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代
,新生代又被进一步划分为 Eden 和 Survivor 区
,最后 Survivor 由 From Survivor 和 To Survivor
组成。
(1)Java6:永久代在非堆内存区
;
(2)Java7:永久代的静态变量和运行时常量池
被合并到了堆中;
(3)Java8:永久代移除
,引入元空间,元空间使用的是本地内存;
(1)移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为
JRockit 没有永久代
,所以不需要配置永久代。
(2)永久代内存经常不够用或发生内存溢出
,爆出异常 java.lang.OutOfMemoryError: PermGen。这是因为在 JDK1.7 版本中,指定的PermGen 区大小为 8M
,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有,为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。
程序计数器
程序计数器是一块很小的内存空间
,主要用来记录各个线程执行的字节码的地址
,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
由于 Java 是多线程语言,当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令
。
方法区
方法区不等价于“永久代”
HotSpot 虚拟机:永久代来实现方法区;
Oracle 的 JRockit、IBM 的 J9:不存在永久代;
方法区:方法区与堆空间类似,也是一个共享内存区
,所以方法区是线程共享的。主要存放已被虚拟机加载的类相关信息
,包括类信息、运行时常量池、字符串常量池。类信息又包括了类的版本、字段、方法、接口和父类
等信息。
JVM执行类的过程:
(1)加载
(2)连接:验证、准备、解析
(3)初始化
JVM 会先加载 class 文件
,而在 class 文件中除了有类的版本、字段、方法和接口等描述信息外,还有一项信息是常量池
(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
而当类加载到内存中后
,JVM 就会将 class 文件常量池中的内容存放到运行时的常量池中。
在解析阶段,JVM 会把符号引用替换为直接引用(对象的索引值)
。
虚拟机栈
Java 虚拟机栈是线程私有
的内存空间,它和 Java 线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个线程栈
,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息
,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。
本地方法栈
本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用
,而本地方法栈则用于管理本地方法的调用
。但本地方法并不是用 Java 实现的,而是由 C 语言实现
的。
运行原理
示例代码:
(1)JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间
,根据内存大小找到具体的内存分配表
,然后把内存段的起始地址和终止地址
分配给 JVM,接下来 JVM 就进行内部分配
。
(2)JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小
。
(3)class 文件加载、验证、准备以及解析
,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值
。
(4)完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 <clinit>
方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码
,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 <clinit>
() 方法。
(5)执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中
。
(6)再次创建一个 JVMCase 对象,调用 sayHello 非静态方法,sayHello 方法属于对象 JVMCase,此时 sayHello 方法入栈,并通过栈中的 student 引用调用堆中的 Student 对象;之后,调用静态方法 print,print 静态方法属于 JVMCase 类,是从静态方法中获取,之后放入到栈中,也是通过 student 引用调用堆中的 student 对象。