JVM的内存区域
程序计数器:
字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
⚠️ 注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
虚拟机栈:
它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。栈是 JVM 运行时数据区域的核心,除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的。方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,都是先进后出的数据结构,只支持出栈和入栈两种操作。
与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
局部变量表
局部变量表是一组变量值的存储空间,用于存储方法参数和局部变量。
局部变量表在编译期间分配内存空间,可以存放编译期的各种变量类型:
基本数据类型 :boolean, byte, char, short, int, float, long, double等8种;
对象引用类型 :reference,指向对象起始地址的引用指针;
返回地址类型 :returnAddress,返回地址的类型。
操作数栈:
主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接:主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化成在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接 。
栈空间不是无限的,但正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
简单总结一下程序运行中栈可能会出现两种错误:
StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
新生代何时晋升到老年代?
空间分配担保是为了确定在MinorGC前确保老年代本身还有容纳新生代所有对象的剩余空间
类加载过程
(下图为类的生命周期)
类加载器
作用:所有的类都由类加载器加载,作用是将class文件加载到内存。
双亲委派模型
每当一个类加载器接收到加载请求时,它会先将请求转发给父类加载器。在父类加载器没有找到所请求的类的情况下,该类加载器才会尝试去加载。使用委派模型的目的为了避免类的重复加载。
应用程序类加载器->拓展类加载器->启动类加载器
自定义类加载器?
自定义类加载器需要继承ClassLoader()方法,如果要打破双亲委派模型重写loadClass(),
如果不想打破重写就重写ClassLoader()类中的findClass()方法。
加载即获取类的二进制字节流,是可控性最强的阶段
死亡对象的判断方法
垃圾回收分类
垃圾收集算法
jvm调优
(对于系统的优化思路一般是,先排查是否是数据库的问题,包括,索引是否合理,是否需要引进分布式缓存,是否需要分库分表),然后考虑是否是硬件能力不足导致,然后再是在应用层代码上进行排查并优化,
最后考虑jvm调优。
在理解JVM内存结构和各种垃圾收集器前提下,结合业务,调整参数来使应用正常运行。
指标:吞吐量,停顿时间,垃圾回收频率
基于这三个指标,我们可能需要调整:
内存区域的大小以及相关策略
堆内存大小、新生代占多少、老年代占多少、Survivor占多少、晋升老年代的条件等
参数(-Xmx:设置堆的最大值、-Xms:设置堆的初始值、-Xmn:表示年轻代的大小、-XX:SurvivorRatio:伊甸区和幸存区的比例等等)
(按经验来说:IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。
内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些)
选择合适的垃圾回收器,以及设置合适的参数
一、针对新生代的垃圾收集器:Serial New,Parallel Scavenge 和 Parallel New
Serial(siry) New:应用复制算法,简单高效,但会暂停程序导致停顿。
ParNew:是Serial的多线程版本,可以配合老年代的CMS工作
Parallel(拍落) Scavenge(死凯位置)应用复制算法,并行收集器,追求高吞吐量,高效利用CPU。
(吞吐量就是运行用户代码时间/(运行用户代码时间+gc时间)
二针对老年代的垃圾收集器:Serial Old 和 Parallel Old,以及CMS
Serial Old:Serial GC的老年代版本,采用标记整理算法。Parallel Old:Parallel Scavenge的老年代版本,可配合Parallel Scavenge收集器达成在整体应用上吞吐量最大化CMS是基于标记清除算法实现的。优点:并发收集、低停顿。缺点:CMS对cpu资源敏感,在cpu数量较少时,可能因为占用一部分cpu资源导致程序变慢。cms无法处理浮动垃圾,可能出现”Concurrent Mode Failure“失败而导致Full GC基于清除算法,会产生内存碎片。
三横跨新生代和老年代的垃圾收集器G1
四、ZGC(可伸缩低延迟垃圾收集器)
特点:
一、并发收集:吞吐量不会下降超过15%。(与G1相比)
二、低延迟:GC的停顿时间不会超过10ms。
三、大内存支持:既能处理几百MB的小堆,也能处理几个TB的堆。
四、动态空间压缩:会对堆进行动态的空间压缩,避免堆内存碎片化的问题,并减少内存浪费。
比如(-XX:+UseG1GC:指定 JVM 使用的垃圾回收器为 G1、-XX:MaxGCPauseMillis:设置目标停顿时间、-XX:InitiatingHeapOccupancyPercent:当整个堆内存使用达到一定比例,全局并发标记阶段 就会被启动等等)
遇到问题进行调优,可使用工具
通过jps命令查看Java进程基础信息(进程号、主类)。
通过jstat命令查看Java进程相关的信息,如类加载、编译相关信息,各个区域的GC情况。
通过jinfo命令来查看和调整Java进程的运行参数。
通过jmap查看进程的内存信息,并且将信息保存到文件,可利用MAT进行分析。
通过jstack命令来查看JVM线程信息,可用来排查死锁相关问题。
Arthas(阿里开源的诊断工具)涵盖命令并有可视化界面。
OOM的原因和解决
定义:
当JVM内存不足,没有空闲内存,并且垃圾收集器也不能提供更多内存,就会发生OOM。
原因
一、堆空间不足。存在内存泄漏问题;堆大小设置不合理;JVM处理引用不及时,导致内存无法回收。
二、对于虚拟机栈和本地方法栈,类似于不断递归且没有返回条件,不断压栈就会导致StackOverFlowError。
如果JVM试图拓展堆空间的时候失败,就会抛出OOM。
三、在老版JDK中,由于JVM堆永久代垃圾回收不积极,容易出现OOM。元数据区引入有所改善。
四、直接内存不足,会导致OOM。
解决
一、使用jps,jmp,MAT,等工具分析出频繁full gc的原因并定位到代码。
频繁full gc
原因:
一、高并发,数据量过大,每次Young GC过后存活对象过多,内存分配不合理,Survivor区过小,导致对象频繁进入老年代,触发Full gc。
二、系统一次性加载大量数据到内存,导致大对象过多,大对象进入老年代,触发FULL GC。
三、系统内存泄漏,大量对象无法回收,一直占用老年代,触发FULL GC。
四、永久代加载类过多触发FUll GC。
五、代码或者第三方依赖包中有system.gc()操作,可设置JVM禁止执行该方法。
解决:
一、通过命令查看进程、线程情况、dump出内存快照,用MAT工具进行分析,确定原因。
二、调整JVM的参数,适当增大新生代的大小。
三、控制并发数量。
CPU打满?
原因:
一、代码中某个位置读取数据量较大,内存耗尽,频繁full gc,系统缓慢。
二、代码中有比较耗CPU的操作,导致CPU过高,系统运行缓慢。可通过命令查看当前CPU消耗高的进程和线程是哪一个,再查看线程的堆栈信息。
系统缓慢的情况
一、FULL GC次数过多
二、CPU打满
三、不定期出现的接口耗时情况。
eg:接口访问需要2、3秒才会返回,一般来说消耗的cpu和占用的内存也不高。思路是首先找到该接口,通过压测工具不断加大访问力度,由于访问力度大,大多数线程都会阻塞在该阻塞点,可以定位到接口中比较耗时的代码位置。
四、某个线程处于waiting状态。
比如CountDownLatch的不合理使用。
五、出现死锁。这个可以直接通过jstack日志分析得到。
三色标记法
作用:提高标记对象的效率
初始标记时STW时间较短,但并发标记时时间较长,因此应提高标记效率。
在三色标记中,从GC ROOT标记为以下三种颜色
白:在开始遍历时,所有对象都为白
灰:被垃圾回收器扫描过,但还有引用没有被扫描,为灰
黑:被垃圾回收器扫描过,并且这个对象的引用也全部被扫描,可存活对象,为黑。
全部扫描过后,回收为白的对象。
强软弱虚引用?
一个对象创建的过程
一个对象的流程图就如上图所示,大致上可以分为以下五步:
类检查机制(检查是否加载过类,没有加载过执行类加载过程)
分配内存
初始化
设置对象头
执行类加载的初始化步骤
Step1:类加载检查
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
对象的内存布局
在 Hotspot 虚拟机中,对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充。
Hotspot 虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据(哈希码、GC 分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。
对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为 Hotspot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍,换句话说就是对象的大小必须是 8 字节的整数倍。而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
建立对象就是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。
对象的访问方式由虚拟机实现而定,目前主流的访问方式有:使用句柄、直接指针。
如果使用句柄的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息。
如果使用直接指针访问,reference 中存储的直接就是对象的地址。对象的访问定位-直接指针这两种对象访问方式各有优势。
使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。HotSpot 虚拟机主要使用的就是这种方式来进行对象访问。