Jvm 学习笔记
Jvm的内存结构
参考博客:https://blog.csdn.net/weixin_40114067/article/details/105444317
参考博客:https://blog.csdn.net/a616413086/article/details/51272309
Jvm结构图
Jvm内存结构划分
- 程序计数器
- 虚拟机栈
- 本地方法栈
- 堆
- 方法区
一.程序计数器(寄存器)
在java虚拟机中,每一个线程都会有自己的程序计数器,也就是说程序计数器是线程私有的。我的理解是它就是一个指针,指向方法区中的方法字节码。
上图右侧是java源代码,左侧是二进制字节码,也就jvm能够执行的指令。我们写的代码都是通过编译之后,变成字节码后,再通过解释器,解析成机器码,再交给CPU来执行。
当jvm加载所有左侧的字节码后,每一条字节码在虚拟机内存中都会有一个地址,执行第一条字节码时,会把下一条字节码的地址存入程序计数器中,字节码解释器会通过改变程序计数器的值来获取下一条即将执行的字节码指令。在物理层面实现程序计数器功能的就是cpu中的寄存器。
程序计数器的主要2个作用:
① 帮助字节码解释器能够依次读取字节码指令,从而实现代码按照流程来运行,也可以帮助实现对流程的控制,比如:顺序执行,循环执行,选择执行等,都依赖程序计数器来实现。
② 在多线程的环境下,程序技术器可以帮助实现记录当前线程的执行位置,这样就可以在下次线程继续执行的时候,从上次的执行位置继续执行
注:程序计数器有一个特点,就是它是唯一一个不会出现OutOfMemory现象的内存区域,它的生命周期跟随线程,随线程创建而被创建,随线程死亡而死亡。
二.JAVA虚拟机栈
java虚拟机栈也是线程所私有的,随着线程的创建而被创建,随线程消亡而消亡。它描述的是java方法的内存模型,常说的java内存中的栈内存就是指的虚拟机栈,它好比数据结构中的栈,而每次调用方法就是往这个栈里压入一个栈帧,每个栈帧中都存放了方法调用的数据。虚拟机栈遵循先进后出的原则,所以main方法一定会在最底层。
栈帧:每个栈帧包含了局部变量表、操作数栈、动态链接、方法出口信息
局部变量表:主要存放了各种数据类型,如基本数据类型、对象引用
操作数栈:主要保存计算过程的中间结果,同时作为变量的临时存储空间,如果被调用方法有返回值的话会将返回值放入当前栈帧的操作数栈中,并且更新pc寄存器中下一条需要执行的字节码指令
动态链接:符号引用和直接引用在运行时进行解析和链接的过程
方法出口:存放调用该方法的pc寄存器的值。一个方法结束有两种方式,一种是正常结束,一种是异常结束,不管哪一种都会使当前栈帧出栈,本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常出口和异常出口的区别在于,通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。
Java 虚拟机栈会出现两种错误:StackOverFlowError 和 OutOfMemoryError。
StackOverFlowError:如果虚拟机内存大小不允许动态扩展的话,那么当前线程请求栈的大小超过虚拟机栈设定的大小的时候,就会抛出这个错误
OutOfMemoryError:如果虚拟机栈内存大小允许动态扩展,并且当线程请求栈时内存用完了,就会抛出这个错误
三.本地方法栈
本地方法栈主要管理本地方法,本地方法是通过c语言或者c++来实现的,与虚拟机栈相同的是不受垃圾回收机制管理,不同线程间是隔离的,是线程私有的
什么是本地方法?
因为java方法无法直接对操作系统直接进行操作,所以需要通过调用c或者c++语言编写的方法来对操作系统进行直接操作。
当调用了本地方法之后,就可以不受java虚拟机的限制,直接使用本地处理的寄存器,可以直接使用本地内存
四.堆内存
堆内存是所有线程都共享的一部分内存,也是垃圾回收管理的重要区域,在虚拟机启动的时候堆内存就会创建,堆的主要作用就是存放对象实例。
Java堆也被称为GC堆,由于现在收集器基本采用分代垃圾收集算法,所以java堆还可以细分为:新生代和老年代
jdk1.7之前,堆内存分为:
新生代:Eden空间、From Survivor、To Survivor 空间
新生代满了,jvm会进行minorGC,如果空间依然不够,则会将对象移动到老年代
老年代:Old空间,满了会进行FullGC,如果依然内存不够,会出现OOM
永生代(方法区)
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
对象首先会在eden空间分配,每次新生代垃圾回收后,如果对象继续存活,则会使对象的年龄加1,当年龄达到一定程度(默认15岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
当JVM堆内存设置不够,或者代码中创建了大量大对象,并且不能被垃圾收集器收集(存在被引用)的时候,会报java.lang.OutOfMemoryError: Java heap space错误
五.方法区
方法区与堆一样,是被各个线程共享的内存区域,用于储存已经被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。在此区中的数据不会被垃圾回收,只有关闭jvm才会释放占有的内存空间。方法区也被称为永久代。
方法区和永久代的关系很像 Java 中接口和类的关系,类实现了接口,而永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。 也就是说,永久代是 HotSpot 的概念,方法区是 Java 虚拟机规范中的定义,是一种规范,而永久代是一种实现,一个是标准一个是实现,其他的虚拟机实现并没有永久代这一说法。
jdk1.8中将永久代替换成了元空间,元空间是1.8以后jdk对方法区的实现,元空间使用的是直接内存,直接和本机内存相关,虽然也有可能会溢出,但是相较于受jvm本身固定大小上限的永久代来说,溢出的概率更小。可以通过以下参数设置元空间,
-XX:MaxMetaspaceSize:标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制
-XX:MetaspaceSize:调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小
当元空间溢出时,会得到如下错误: java.lang.OutOfMemoryError: MetaSpace
六.运行时常量池
运行时常量池是方法区的一部分, Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存放编译期生成的各种字面量和符号引用)
JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。