这是「死磕P7」系列第 002 篇文章,欢迎大家来跟我一起 死磕 100 天,争取在 2025 年来临之际,给自己一个交代。
今天我们继续来探究 JVM 内存划分,第一篇主要搞清楚 JVM 的内存划分都有哪些,本篇主要介绍各个区域到底是干嘛的,也是一篇死记硬背的内容。
来巩固一下昨天的内存区域划分图,以后就以 1.8 为准吧,毕竟占有率高,1.8 中JVM 内存区域划分为:方法区(元空间),堆,程序计数器,虚拟机栈,本地方法栈。
整理了一份思维导图帮助您快速记忆(不过看完也会忘,多看几遍,自己按自己的理解画出来会好点):
下面来具体罗列一下各区域的作用,内容比较多,随便拎出来一项都能深入挖掘不少东西,说真的,今天一天时间净在那研究虚拟机栈了,其实发现完全不需要研究太深,研究的深反而考不高分,毕竟80% 的题目都不需要那么深的造诣,把时间花在刀刃上,赶紧抓后面更重要的才对。
就好像考试做题一样,肯定先易后难,后面有空再探究难的题目会更好,先用简单的练练自信心,说不定后面的做完,前面的难题就迎刃而解了也有可能。
记不住没关系,哪可能一遍就记住啊,必须得来回看,来回边用边记,光靠背是分分钟忘记的,你也可以尝试自己写下来,画一画,左右脑并用。
JVM 区域划分分为了线程共享和线程独享两大类,方法区(元空间,以后统称为方法区吧)和堆是线程共享的,也就相当于一个大水缸,大家(线程)都来大水缸里取水喝。
虚拟机栈,本地方法栈和程序计数器是线程独享的,相当于大家(线程)自己的水杯,自己喝自己的。
程序计数器(Program Counter Register)是JVM中一块较小的内存区域,可以看做当前线程所执行的字节码的行号指示器(其实就是记录代码执行到了哪里)。
因为一个处理器(CPU)在同一个时刻只会执行一个线程的指令,但一个线程中有多个指令(可以理解为多行代码),为了在线程切换时可以恢复到正确的执行位置,会为每个线程设置一个独立的程序计数器,等到下次轮到这个线程执行的时候就接着上次执行的位置继续执行。
补充一点线程切换的概念,因为CPU只负责计算,并且计算速度很快,但是程序要干的事情可能不仅仅需要计算,可能还有读写文件(IO操作),这部分是不需要计算的,CPU会闲着,这时操作系统会将CPU计算资源分配给其他线程进行使用,就是CPU是这个线程使用一会,那个线程使用一会,这就叫线程切换。
记住一点就行:程序计数器是记录每个线程字节码执行位置的,并且是线程独享的且不会发生OOM等异常的区域。
额外一点:如果执行的是 Native 方法,程序计数器为空。
当启动一个新线程的时候,Java虚拟机都会为它分配一个Java虚拟机栈。
一个程序至少会有一个线程,大多数情况下可能是有一个主线程,我们以物业公司的物业经理做类比吧,当业主需要服务的时候,物业经理可以安排保安或保洁人员去干活,这个保安保洁人员就可以称为一个子线程,多个保安保洁人员也就是多个子线程了呗,统一听从物业经理的安排。
每个线程会干一个或多个活,对应多个方法,线程启动就会创建对应的虚拟机栈,栈是一种数据结构,具有先进后出(后进先出)的特点,好比你往桶里放衣服(哈哈,谁把衣服放桶里啊),最后放进去的先被拿出来,我说我怎么一个夏天总是穿两件衣服呢,哈哈,洗完入栈,穿的时候出栈(后进先出),下面的衣服轮不到穿啊!
Java 虚拟机栈处理的对象是方法,每个方法在执行的时候,虚拟机栈都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
每个方法从被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
我们以打扫房屋为例,看看是怎么入栈出栈的:我这个屋子是一室一厨一主卧带独立卫生间的。
// 打扫房屋
public void cleanHouse() {
// 打扫厨房
cleanKitchen()
// 打扫卧室
cleanBedRoom()
}
// 打扫厨房
public void cleanKitchen() {
}
// 打扫卧室
public void cleanBedRoom() {
// 打扫套卫
cleanBashRoom()
}
// 打扫套卫
public void cleanBashRoom() {
}
// main 方法
public void main() {
// 打扫房屋
cleanHouse()
}
上面的伪代码涉及 5 个方法,还有方法的嵌套,算是比较常见的一个线程任务了,线程从 main 方法开始执行
示意图如下:
虚拟机栈会为每一个方法创建一个栈帧,栈帧即对应上图中栈中的一个个方块,如 cleanBashRoom 方块,栈帧里又包含 局部变量表,操作数栈,动态链接,返回地址,方法出口等等。
好了,兄弟,别再深入了,什么是局部变量表?什么是操作数栈?什么是动态链接?都别管了,先跳过去吧!我看了一天了,也没敢往文章里写,有点浪费时间,还打击自信心。
方法执行完成后,方法就会出栈,方法退出有 2 种:
1,正常的退出,不报异常时的退出
2,异常的退出,出现异常了还没有捕获处理
虚拟机栈中会遇到 2 种异常:StackOverflowError, OutOfMemoryError
1,StackOverflowError,采用固定大小的虚拟机栈,如果线程请求的栈太深虚拟机将会抛出一个StackOverflowError 异常,比如无限递归调用
2,OutOfMemoryError,如果虚拟机栈可以动态扩展,并且在扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError 异常
这个可以跳过,跟虚拟机栈基本一样,区别是调用的是本地方法,在 HotSpot VM 中合并到了虚拟机栈。
也别关心什么是本地方法了,99% 的情况下用不到,先略过。
在 1.8 中好像没啥说的了,只有类信息了,知道现在叫元空间,并且放置到了直接内存(JVM内存之外)就可以了吧。
方法区也会出现 OOM 异常。
JVM 中最重要的就数堆了,堆是 JVM 中最大的一块内存区域,因为几乎大部分的对象实例和数组是在堆中创建,还包括新移入进来的字符串常量池和静态变量
堆也是 垃圾回收(GC)的最主要的区域,后面还会介绍到,这里先知道堆为了垃圾回收的方便,又分成了如下不同的区域即可。
注意,细分的原因是为了 GC 的方便,因为 JVM GC 是采用的分代垃圾回收机制,垃圾回收也是常考点,下一遍介绍垃圾回收吧。
本文主要梳理了 JVM 的内存区域划分及简单介绍了其特点,由于 JVM 内容比较多,对于时间紧任务重的兄弟们来说记住最主要的就行了,先考 80 分再说吧!
本次的分享到此结束,希望对你有所帮助。
如果你对我分享的内容感兴趣,欢迎扫码关注公众号:新质程序猿,并设置星标,推送更实时哟!