JVM内存模型及功能

一、JVM的概念

JVM(Java Virtual Machine)为执行java程序的虚拟计算机,是java程序编译和运行不可或缺的平台。它可以将java文件解析成字节码文件供不同操作系统的JVM来运行,实现java的跨平台特性——“一次编译,到处运行”。而其内置的解释器(或称直译器 java.exe)在运行阶段将转换成字节码文件内的高级汇编语言翻译成计算机能够识别的二进制数据后进行处理。

二、JVM的内置存储空间

2.1 JVM的存储区域

2.2 Heap堆区

2.2.1 堆的内部空间

从内存角度;堆是JVM内存中空间占比最大的存储区域,在《Java虚拟机规范》书中对Java堆的描述是:“所有 的对象实例以及数组都应当在堆上分配[1]”,这就说到它存储的数据。首先,堆由新生代、老生代(和永久代,JDK8版本称为元数据区,永久代空间在JDK9开始被移除)组成,在创建新对象(TLAB为对象分配内存)时通常先会对新生代进行存储空间判断,如果空间足够则存储,如果新生代的内存区域不足以存储该对象并且触发了15次的"GC(Minor GC/Young GC)"机制仍空间不够,则会将对象移至老生代进行存储,当老生代的内存区域存储空间不足时也会触发"GC(Major GC/Old GC 触发次数比Minor GC要少)",如果这时还无法存储就会出OOM(内存溢出的错误,JVM直接抛出的无法try...catch..)

2.2.2 堆的生命周期

java堆在JVM启动时被创建初始化(堆空间可以通过配置Xms,Xmx来让JVM分配其内存)

然后使用java对象

【以下为个人想法】

在JVM关闭前会先执行Full GC(会使当前线程stop-the-world简称STW暂停所有事件执行)将整堆的对象回收清理

销毁堆空间

最后JVM关闭

2.2.3 堆的线程共享性

堆在多线程环境中是数据共享的,也就是说每条线程所初始化的对象值都是互相能够看到并且使用的。【多线程情况需考虑线程数据安全问题】

2.3 虚拟机Stack栈区

只有在栈区所有栈帧弹栈或者抛出给JVM异常,JVM才会关闭

2.4 方法区 


方法区是JVM的一个规范。并不是具体的实现,方法区的具体实现是永久代(Perm space)
类加载器加载代码后将其放置方法区,所以最先有数据
未调用执行的方法是不会走到栈区的


2.5 本地方法栈


JVM的本地方法栈是为了虚拟机执行本地方法(Native修饰的方法)的一块内存区域,通过JNI(Java Native Interface,java提供的一种编程框架,用于实现java代码与Native方法与C或C++代码之间的交互调用)

image.png


本地方法栈用于执行Native方法的调用和执行过程,与虚拟机栈类似,每个线程在执行方法时也会在本地方法栈中创建栈帧,用来存储本地方法的形参、局部变量和返回值。但本地方法的大小要比虚拟机栈分配的内存要小,且内存空间固定,不会进行动态扩张,如果栈空间不足,就会抛出StackOverflowError栈内容空间溢出的错误

关于线程共享:本地方法栈在每个线程都会开辟出私有的内存空间,是线程私有的,各个线程不能访问和感知到其他线程的本地方法栈的数据



2.6 程序计数器


程序计数器(Program Counter Register)又名PC寄存器,类似计算机的物理寄存器,用于存储操作指令的现场信息通过电脑CPU装载到寄存器上的数据后运行,JVM的PC寄存器则为其一种抽象模拟。寄存器基本上是整个CPU组件里读取速度最快的一个单元,读取/写相关指令地址的动作是非常频繁的,而JVM基于这一点在设计时就把CPU的物理寄存器作为程序计数器来存储和读取地址来满足功能


2.6.1 程序计数器的功能


在JVM的执行引擎中的直译器将字节码文件解释成计算机能识别的机器码后交由CPU执行的,而因为CPU的执行是线程级别的,会不停的切换各个线程。CPU切换到其他线程的程序运行前程序计数器就会将下一条指令的地址存储,等切换回就可以通过程序计数器存储的指令地址来继续执行JVM的字节码指令
在一些商用JVM中(例如HotSpot,JDK1.3版本后都是这种类型的虚拟机),为了处理代码块重复多次执行(例如一个方法或者循环体的多次执行),提高程序执行效率,JVM会将经常处于线程的栈顶方法/代码块建立计数器,来统计其的执行次数,如果次数超过1万次(可以配置),就会将这些“热点方法/代码块”通过JIT一次性编译成机器码交由CPU来执行

image.png


延伸: 关于HotSpot类型的JVM

HotSpot是什么?_ddO_O的博客-CSDN博客



2.6.2 程序计数器的内部空间


程序计数器占用的内存空间很小,在JVM内存计算时,基本可以忽略不计,因为它只需要记录存储下一条需要执行的字节码指令内存地址,它不会因为方法的深层调入而新增存储而是将之前的地址值进行替换,所以数据量非常小,所需的存储空间极小,而且也不会存在内存溢出的错误。
思考:为什么在执行本地native方法时,程序计数器为undefind?
因为java在调用native方法时实际上是在调用C/C++暴露的接口,是需要在C/C++内存模型上运行的与java不是一个体系,字节码文件都不会有native方法的数据,程序计数器自然是undefind。
2.7.3 程序计数器的线程共享性
为了准确记录各个线程正在执行的字节码指令地址,避免因为CPU的线程切换执行而错误执行程序,所以设定为私有的,每个线程独立分配一个程序计数器,让各个线程执行时都能顺利正常执行,互不影响。在对应线程结束后即消失(生命周期与对应线程生命周期一致)。



[1] 《Java虚拟机规范》中的原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated。 [2] 逃逸分析与标量替换的相关内容,请参见第11章的相关内容。 [3] 指新生代(其中又包含一个Eden和两个Survivor)、老年代这种划分,源自UC Berkeley在20世纪80
年代中期开发的Berkeley Smalltalk。历史上有多款虚拟机采用了这种设计,包括HotSpot和它的前身Self 和Strongtalk虚拟机(见第1章),原始论文是:https://dl.acm.org/citation.cfmid=808261
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值