这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战
前言
JVM(Java Virtual Machine):爪哇虚拟机
JVM内存结构:运行时数据区域
事实上,对于JVM上层,还存在一个由Oracle定制的Java虚拟机规范,我们常说的JVM一般是指JVM规范的某个具体实现。比如我们经常使用的Java虚拟机HotSpot,JDK1.8使用的就是HotSpot
> java -version
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)
比如我们经常搞混的,JDK1.8之前的永久代、JDK1.8之后的元空间,这篇文章是永久代,到了那篇文章又是元空间的,实际上他们两个都可以看作是JVM规范中方法区的实现
本文针对JVM的内存区域,尽可能详细的解剖
事实上,JVM的知识多且杂,容易混淆,对此,我强烈建议你点赞收藏加关注,逛完
PB站,趁着精神状态良好,来复习一遍JVM想必也是极好
JVM整体架构
JVM内存区域,又称运行时数据区域,就是下图红框框起来的部分:
JVM整体架构图:
JVM内存结构
主要分为以下区域:
- 线程私有的:程序计数器,本地方法栈,虚拟机栈
- 线程公有的:方法区,堆
程序计数器(线程私有)
程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的**行号指示器;**它是唯一一个在 JVM 规范中没有规定任何
OutOfMemoryError情况的区域
程序计数器的作用
当多个线程在特定的时间内同时执行的时候,一个线程不可能一直占用着CPU 的资源,这样就会造成频繁的上下文切换,为了线程切换回来后,能恢复到正确的位置继续执行,每个线程都有自己的程序计数器
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理等
程序计数器里存储的是什么?
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。如果当前线程正在执行的是 Java 方法,程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值(undefined)
虚拟机栈(线程私有)
每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个函数调用结束后,都会有一个栈帧被弹出,遵循“先进后出/后入先出”的原则
如下图,方法1调用了方法2,于是方法2把方法1压在了身下,这个过程就是压栈;当前方法执行完毕后(return或者异常),就会进行出栈操作
栈里面存什么
虚拟机栈里面保存栈帧,每个栈帧中保存了方法的局部变量表、操作数栈、动态链接、方法出口信息
在这里大概说一下栈里面的几个东西是干嘛的,详细内容下回分解
- 局部变量表:一组变量值存储空间,主要用于存储方法参数和定义在方法体内的局部变量,比如方法内的
String abc = "abc";对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置) - 操作数栈:主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
- 动态链接:每个栈帧都保存了一个可以指向当前方法所在类的运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接
- 方法返回地址:方法正常
return退出时,调用者的 PC 计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址;通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息
虚拟机栈会抛出什么异常
虚拟机栈的内存大小可以是动态的,也可以是固定大小的
StackOverFlowError:当设置为固定大小时,若是压栈超出了最大的允许值就会抛出该异常OutOfMemoryError:当设置为动态扩展时,若是扩展时无法向系统申请到足够的内存时会报该异常
本地方法栈(线程私有)
本地方法栈的功能与虚拟机栈类似,会抛出的异常也一样,两者的区别在于,虚拟机栈是为Java方法服务的,而本地方法栈是为Native方法服务的
并不是所有 JVM 都支持本地方法。因为 Java 虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果 JVM 产品不打算支持 native 方法,也可以无需实现本地方法栈
在 Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一
堆(线程共享)
Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存,正因如此,Java 堆也是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)
堆内存的划分
为了高效的进行垃圾回收,可以发现,堆被划分成了三个区域:
- 新生代(Young Generation):新对象和没达到一定年龄的对象都在新生代,新生代又被分为Eden、Survivor0,Survivor1
- 老年代(Old Generation):被长时间使用的对象,年龄到了以后会进入老年代,老年代的内存空间应该要比年轻代更大
- 元空间(图中的Perm区):JDK1.8以前不叫元空间,叫永久代,不管是元空间还是永久代,都可以看作是JVM规范中方法区的实现。方法区有个别名叫非堆(Non-Heap),目的应该是与 Java 堆区分开
如何配置堆内存
在启动Java程序的时候,可以用参数指定堆的大小:
-Xms:设定堆的起始内存,默认情况下,初始堆内存大小为:电脑内存大小/64-Xmx:设定堆的最大内存,默认情况下,最大堆内存大小为:电脑内存大小/4
分享一个小zi si,我们通常会将
-Xmx和-Xms两个参数配置为相同的值,其目的是为了能够在垃圾回收机制清理完堆区后不再需要重新分隔计算堆的大小,从而提高性能
例:
java -Xmx1024m -Xms1024m -jar test.jar
堆最容易出现 OutOfMemoryError错误,有两个比较常见的错误信息:
OutOfMemoryError: GC Overhead Limit Exceeded:这是JVM在垃圾回收时用了太多时间并且回收的空间很少的时候会报出的错误OutOfMemoryError: Java heap space:这是创建对象是,发现堆中的内存不足以存放新的对象就会出现这个报错
通过对象在堆中的生命周期进一步了解堆
- 当创建了一个新的对象时,首先会在新生代的Eden区分配内存,此时JVM会给这个对象一个年龄
- 当新来的对象很大,而Eden空间不足时,就会触发Minor GC,把不需要的干掉,再将新对象放入Eden区
- Eden经历一次垃圾回收后的对象会被放入Survivor0中,并且给它的年龄+1
- 如果再次触发垃圾回收,Survivor0 中的对象会被移入 Survivor1,年龄再次+1
- 如果对象的年龄超过了我们配置的进入老年代的年龄阈值(可以通过
-XX:MaxTenuringThreshold参数设置),对象就会被分配到老年代 - 老年代很少进行GC,当老年代内存不足时,会触发 Full GC,若到了这一步依然无法对新来的对象进行保存,就会出现OOM(内存溢出)
方法区
方法区,是JVM规范定义的一个概念,无论是JDK1.8之前的永久代,还是JDK1.8之后的元空间,都属于规范的一种是实现。可以把方法区看作Java代码中的接口,而永久代和元空间则是它的具体实现
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等
如何配置元空间的大小
-XX:MetaspaceSize:设置元空间的初始大小-XX:MaxMetaspaceSize:设置元空间的最大大小
由于元空间是直接使用系统的物理内存的,如果不指定元空间额度大小,随着类的创建,虚拟机可能会耗尽所有的可用系统内存
同样的,由于元空间使用的是系统内存,虽然任有可能溢出,但是出现的概率会小很多,当元空间溢出时会报错 OutOfMemoryError: MetaSpace
需要注意的是:
- 对于一个 64 位的服务器端 JVM 来说,其默认的
-XX:MetaspaceSize的值为20.75MB,这就是初始的高水位线,一旦触及这个水位线,Full GC 将会被触发并卸载没用的类(即这些类对应的类加载器不再存活),然后这个高水位线将会重置,新的高水位线的值取决于 GC 后释放了多少元空间。如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值 - 如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次,通过垃圾回收的日志可观察到 Full GC 多次调用。为了避免频繁 GC,可以将
-XX:MetaspaceSize设置为一个相对较高的值。