一、JVM虚拟机数据区
· 虚拟机栈
1、 线程私有
2、 每个方法被执行的时候都会创建一个栈帧用于存储局部变量表,操作栈,动态链接,方法出口等信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。
3、栈帧: 是用来存储数据和部分过程结果的数据结构
4、 栈帧大小确定时间: 编译期确定,不受运行期数据影响
5、虚拟机栈的生命周期和线程是相同
6、虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)
7、线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素:如图->
8、栈帧细节如图:
9、局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量
10、操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。栈容量的单位为“字宽”,对于32位虚拟机来说,一个“字宽”占4个字节,64位虚拟机来说,一个“字宽”占8个字节。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作
11、每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接
12、当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。 无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。 方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等
· 本地方法栈
1、本地方法本质上时依赖于实现的,虚拟机实现的设计者们可以自由地决定使用怎样的机制来让Java程序调用本地方法
2、任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法
3、很可能本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈
4、如下图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转
· 堆(划重点)
1、堆的内存结构如图:
2、堆内存分为年轻代(Young Generation)、老年代(Old Generation)
3、年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1
4、堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收
5、JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存
6、元空间有注意有两个参数:
MetaspaceSize :初始化元空间大小,控制发生GC阈值
MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
7、移除永久代原因:为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。
有了元空间就不再会出现永久代OOM问题了
8、各代关系:
新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。
老年代存储长期存活的对象,占满时会触发Major GC=Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。
Minor GC : 清理年轻代
Major GC : 清理老年代
Full GC : 清理整个堆空间,包括年轻代和永久代
所有GC都会停止应用所有线程
9、QA
为什么survivor分为两块相等大小的幸存空间?
主要为了解决碎片化。如果内存碎片化严重,也就是两个对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC
为什么会堆内存溢出?
在年轻代中经过GC后还存活的对象会被复制到老年代中。当老年代空间不足时,JVM会对老年代进行完全的垃圾回收(Full GC)。如果GC后,还是无法存放从Survivor区复制过来的对象,就会出现OOM(Out of Memory)
OOM常见原因分析:
老年代内存不足:java.lang.OutOfMemoryError:Javaheapspace
解决办法:java 堆内存溢出, 此种情况最常见, 一般由于内存泄露或者堆的大小设置不当引起。对于内存泄露, 需要通过内存监控软件查找程序中的泄露代码, 而堆大小可以通过虚拟机参数 - Xms,-Xmx 等修改
永久代内存不足:java.lang.OutOfMemoryError:PermGenspac
解决办法:java 永久代溢出, 即方法区溢出了, 一般出现于大量 Class 或者 jsp 页面, 或者采用 cglib 等反射机制的情况, 因为上述情况会产生大量的 Class 信息存储于方法区。当出现此种情况时可以通过更改方法区的大小来解决, 使用类似 - XX:PermSize=64m -XX:MaxPermSize=256m 的形式修改。注意, 过多的常量尤其是字符串也会导致方法区溢出
java.lang.StackOverflowError
解决方法: JAVA 虚拟机栈溢出, 一般是由于程序中存在死循环或者深度递归调用造成的, 栈大小设置太小也会出现此种溢出。可以通过虚拟机参数 - Xss 来设置栈的大小
重点来了:
(1)、Java heap space报错的原因及如何解救:
(1.1)、请求创建一个超大对象,通常是一个大数组,举例子:从文件或者db中读取了100w条数据,申明了一个List或者Map,一条数据的大小哪怕1kb大小,这100w数据就要占用1Gb内存
这里给个样例代码,后面不会再上复现代码了,各位自己下去动手去验证:
package com.feixiang.platform.stu.jvm.oom.stack;
import com.feixiang.platform.stu.jvm.reference.strong.User;
import java.util.ArrayList;
import java.util.List;
/**
* Created with IntelliJ IDEA.
* Description: 创造一个OOM来实验:
* 一定要够大的数组对象
* 运行参数一定要修改,不然无法如愿
*
* @PackageName com.feixiang.platform.stu.jvm.oom.stack
* @Author: ldwtxwhspring
* @Date: 2023-12-02 下午6:01
* @Version: 1.0.0
*/
public class StackHeapOomExample {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
for(int i=0;i<1000000;i++){
User user = new User();
user.setId(i);
user.setName("feixiang_"+i);
user.setAddress("xxxx省—xxx市-xxx路-xxx小区-xxx楼"+i+"号房间");
user.setMobile("133xxxx5680");
userList.add(user);
}
}
}
运行参数修改如图:
运行结果如图:
(1.2)、超出预期的访问量/数据量,通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。例如:双十一几万件商品快速被推向订单池,连续操作有不断锁库存加事务完整性逻辑,这些内存肯定不会被回收,而且会存在于完整的订单周期内。
(1.3)、过度使用(Finalizer),该对象没有立即被 GC
(1.4)、内存泄漏(Memory Leak),大量对象引用没有释放,JVM 无法对其自动回收,常见于使用了 File 等资源没有回收,例如:读写文件完成没有刷新流或者没有关闭管道。
解决方法:
针对大部分情况,通常只需要通过 -Xmx 参数调高 JVM 堆内存空间即可。如果仍然没有解决,可以参考以下情况做进一步处理:
- 如果是超大对象,可以检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制。
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级。
- 如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接。
- 对于外部网络请求,可以设置超时时间,请求回调改造
(2)、PermGen space报错原因分析及解救:
(2.1)、 该错误表示永久代 (Permanent Generation) 已用满, 通常是因为加载的 class 数目太多或体积太大
(2.2)、原因分析,永久代存储对象主要包括以下几类:
- 加载/缓存到内存中的 class 定义,包括类的名称,字段,方法和字节码;
- 常量池;
- 对象数组/类型数组所关联的 class;
- JIT 编译器优化后的 class 信息。
- PermGen 的使用量与加载到内存的 class 的数量/大小正相关。
(2.2)、解决方案,根据 Permgen space 报错的时机,可以采用不同的解决方案,如下所示:
- 程序启动报错,修改 -XX:MaxPermSize 启动参数,调大永久代空间。
- 应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份 class 信息,只需重启 JVM 即可解决。
- 运行时报错,应用程序可能会动态创建大量 class,而这些 class 的生命周期很短暂,但是 JVM 默认不会卸载 class,可以设置 -XX:+CMSClassUnloadingEnabled 和 -XX:+UseConcMarkSweepGC这两个参数允许 JVM 卸载 class。
- 如果上述方法无法解决,可以通过 jmap 命令 dump 内存对象 jmap-dump:format=b,file=dump.hprof ,然后利用 Eclipse MAT Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation 功能逐一分析开销最大的 classloader 和重复 class
(3)、java.lang.OutOfMemoryError:GC overhead limit exceeded 错误原因及解决:
当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,就会抛出 java.lang.OutOfMemoryError:GC overhead limit exceeded 错误。简单地说,就是应用程序已经基本耗尽了所有可用内存, GC 也无法回收。
此类问题的原因与解决方案跟 Javaheap space 非常类似,请回看上文
(4)、Metaspace错误原因及解救:
JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。
此类问题的原因与解决方法跟 Permgenspace 非常类似,可以参考上文。需要特别注意的是调整 Metaspace 空间大小的启动参数为 -XX:MaxMetaspaceSize。
(5)、Unable to create new native thread错误原因及解救:
(5.1)、每个 Java 线程都需要占用一定的内存空间,当 JVM 向底层操作系统请求创建一个新的 native 线程时,如果没有足够的资源分配就会报此类错误。
(5.2)、原因分析,JVM 向 OS 请求创建 native 线程失败,就会抛出 Unableto createnewnativethread,常见的原因包括以下几类:
- 线程数超过操作系统最大线程数 ulimit 限制;
- 线程数超过 kernel.pid_max(只能重启);
- native 内存不足;
(5.3)、该问题发生的常见过程主要包括以下几步:
- JVM 内部的应用程序请求创建一个新的 Java 线程;
- JVM native 方法代理了该次请求,并向操作系统请求创建一个 native 线程;
- 操作系统尝试创建一个新的 native 线程,并为其分配内存;
- 如果操作系统的虚拟内存已耗尽,或是受到 32 位进程的地址空间限制,操作系统就会拒绝本次 native 内存分配;
- JVM 将抛出 java.lang.OutOfMemoryError:Unableto createnewnativethread 错误。
(5.4)、解决方案
- 升级配置,为机器提供更多的内存;
- 降低 Java Heap Space 大小;
- 修复应用程序的线程泄漏问题;
- 限制线程池大小;
- 使用 -Xss 参数减少线程栈的大小;
- 调高 OS 层面的线程最大数:执行 ulimia-a 查看最大线程数限制,使用 ulimit-u xxx 调整最大线程数限制ulimit -a … 省略部分内容 … max user processes (-u) 16384
(6)、Out of swap space原因分析及解救:
(6.1)、该错误表示所有可用的虚拟内存已被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical Memory)和交换空间(Swap Space)两部分组成。当运行时程序请求的虚拟内存溢出时就会报 Outof swap space? 错误。
(6.2)、原因分析,该错误出现的常见原因包括以下几类:
- 地址空间不足;
- 物理内存已耗光;
- 应用程序的本地内存泄漏(native leak),例如不断申请本地内存,却不释放。
- 执行 jmap-histo:live 命令,强制执行 Full GC;如果几次执行后内存明显下降,则基本确认为 Direct ByteBuffer 问题。
(6.3)、解决方案,根据错误原因可以采取如下解决方案:
- 升级地址空间为 64 bit;
- 使用 Arthas 检查是否为 Inflater/Deflater 解压缩问题,如果是,则显式调用 end 方法。
- Direct ByteBuffer 问题可以通过启动参数 -XX:MaxDirectMemorySize 调低阈值。
- 升级服务器配置/隔离部署,避免争用。
(7)、Kill process or sacrifice child分析及解决:
(7.1)、有一种内核作业(Kernel Job)名为 Out of Memory Killer,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考 Surviving the Linux OOM Killer。不同于其他的 OOM 错误, Killprocessorsacrifice child 错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
(7.2)、原因分析
默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。
然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。
(7.3)、解决方案
- 升级服务器配置/隔离部署,避免争用。
- OOM Killer 调优。
(8)、Direct buffer memory原因及解救:
(8.1)、java 允许应用程序通过 Direct ByteBuffer 直接访问堆外内存,许多高性能程序通过 Direct ByteBuffer 结合内存映射文件(Memory Mapped File)实现高速 IO。
(8.2)、原因分析
- Direct ByteBuffer 的默认大小为 64 MB,一旦使用超出限制,就会抛出 Directbuffer memory 错误。
(8.3)、解决方案
- Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查。
- 检查是否直接或间接使用了 NIO,如 netty,jetty 等。
- 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值。
- 检查 JVM 参数是否有 -XX:+DisableExplicitGC 选项,如果有就去掉,因为该参数会使 System.gc() 失效。
- 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间。
- 内存容量确实不足,升级配置。
· 程序计数器
1、线程私有
2、每个线程一块内存,指向当前正在执行的字节码的行号。如果当前线程是native方法,则其值为null
3、如果执行的是java方法,那么记录的是正在执行的虚拟机字节码指令的地址的地址,如果是native方法,计数器的值为空(undefined)
4、这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域
二、本地内存
· 元数据区
1、主要用来保存被虚拟机加载的类信息、常量、静态变量以及即时编译器编译后的代码等数据
2、存在OOM的区:一个元数据区的大小决定了Java虚拟机可以装载的类的多少
3、包括final修饰的变量、字面量、类和接口全限定名、字段、方法名称以及修饰符等永恒不变的东西
4、并不是所有的字面量都会存储在类文件常量池中,比如对于方法内(注意是方法)整数字面量,如果值在-32768~32767之间则会被直接嵌入JVM指令中去,不会保存在常量池中
· 直接内存
1、java通过native方法去调用
2、在调用这种native方法的时候,就会有线程对应的本地方法栈,这个其实类似于java虚拟机栈。也是存放各种native方法的局部变量表之类的信息
3、还有一块区域,不是jvm的,通过NIO中的allocateDirect这种API,可以在jvm堆外分配内存空间,然后通过java虚拟机栈里的DirectByteBuffer来引用和操作堆外内存空间