JVM
总的来说,其实JVM包含有JVM内存模型-运行时数据区(Runtime Data Area)、类加载器(ClassLoader)、执行引擎(Execution Enginee)、本地接口(Native Interface)、本地库(Native Libararies)
介绍JVM的时候,其实可以展开为:内存模型、GC回收器、性能调优、双亲委派模型、内部机制 [ 公共表达式处理、JMM AOV - 指令重排 - 内存屏障、逃逸分析、内联、栈上分配、同步消除、编译器优化 ]、执行模式 [解释模式、编译模式、混合模式] (但双亲委派模型由于比较简明不打算介绍,而编译器优化和执行模式以后再介绍)
JVM内存模型
- 程序计数器:记录当前线程执行到哪里的部分,实际存储在内核空间 (系统专用的内存空间),由OS管理。对应着 CPU 的寄存器,当出现CPU片切换的时候,会存入 TCB 之中,这部分涉及更加底层,不多介绍。本质上是 线程私有
- 虚拟机栈:线程执行的栈体,实际属于堆外内存由系统管理 (用户使用的内存空间),值得一提的是,所谓的栈帧其实就是栈内部方法调用的信息,因为往往是方法调用方法,当前处理的方法被称之为 当前栈帧 对应着当前方法。本质上是 线程私有
- 本地方法栈:等同于虚拟机栈,实际上也属于堆外空间也由系统管理 (用户使用的内存空间),一般来说属于JVM引用的第三方语言类库中方法调用时使用。标志关键字是native。从线程的角度来看属于 线程私有
- 堆:常见的方法内的局部变量、对象的成员变量等实际存储地址,栈中会存有映射地址。在Jdk8之中原本会存储在永久代之中的基本数据类型、字符串常量池也被更改进入堆之中 线程共享
- 方法区:所谓的永久代*(说是属于非堆,但实际上内存地址上紧贴堆空间,本质上属于堆空间)* / 元空间*(属于堆外空间,彻底属于堆外空间),实际上该区域一般存储则JVM的一些运行必要变量、被加载的第三方类库、class字节码对象、基本数据类型、字符串常量池等(1.8移除),FullGC回收时会回收方法区(不管是永久代还是元空间都有回收动作)* 从线程的角度来看属于 线程共享
内存溢出
从报错的角度来看,分为StackOverFlowError和OutOfMemoryError。但从概念角度来看其实都属于内存溢出。我们可以直接从内存模型直接来分析有哪几种可能会出现内存溢出
-
程序计数器不可能会出现内存溢出
-
栈空间 (包括虚拟机栈、本地方法栈):会出现两种Error
-
StackOverFlowError
一般是出现递归调用写错了 / 循环调用的时候才会出现,如果真不是,建议-XSS改大 -
OOMError:unable to create new native thread
严格意义来说,出现该OOM其实有两种可能,第一种是堆外内存不足,无法创建新的线程了;第二种则是达到了系统配置的默认最大线程数了。这里只讨论第一种情况,第二种情况主要是需要修改系统配置文件。对于第一种堆外内存不足以创建新线程我们可以大概按照以下公式来进行理解:
(当然实际可以创建的数量比这要小,需要考虑其他使用到堆外内存的部分,这部分简化了)(MaxProcessMemory 系统内存最大空间 - JVMMemory 堆空间大小 - ReservedOsMemory 操作系统内存空间 又叫内核空间) / (ThreadStackSize 单个线程的栈空间大小) = Number of threads 理论上可以存在的线程总数
需要明确一点,-XSS修改的不是栈空间大小,而是单个线程栈的大小。
-
-
堆空间:会有两种常见的Error:
OOMError:java heap space
:常见于大量挂着引用不给回收的情况。如果不是建议:改大-Xms 、-XmxOOMError:GC Overhead limit exceeded
GC多次回收但是还是没有回收到足够的空间,建议开启GC日志-XX:PrintGC
、-XX:PrintGCDetails
、-XX:DumpOnOutOfMemoryError
,可以查看GC回收日志的内容做分析,也可以把GC日志用Jvisualvm / 用EasyGC网站查看分析,按照建议修改GC策略等。
-
方法区 (Permenant / MetaSpace):只会出现一种 PermSpace Size / MetaSpace Size,建议正常导入包,或者改大
-XX: PermSize 、 -XX: MaxPermSize
/-XX: MetaSpaceSize 、-XX: MaxMetaSpaceSize
JVM内部机制
公共表达式处理 (公共子表达式消除)
公共表达式处理其实是指代JVM通过类加载器对类进行加载之中对类做解析的步骤的工作,JVM为了避免没必要的重复表达式的计算导致性能上的消耗,JVM会将识别到相同表达式部分做抽取优化。
例如:
// 优化前
int a = b * c + 100;
int d = b * c + 200;
// 优化后 - JVM会识别到b*c是重复计算
int temp = b * c;
int a = temp + 100;
int d = temp + 200;
JMM、指令重排
JMM在并发一文介绍原子级别的线程安全时有所介绍:
指令重排也有过介绍:指令重排 - LeticiaFENG Note
栈上分配
栈上分配很好理解,只要系统学习过任何一款语言(从原理上学习过程序的执行),都能理解。本质上单纯的就是作用范围局限在方法代码块上的对象为了避免频繁的堆内存分配和反复回收,换一种说法就是降低GC的频率,而将这些局部变量 (在不发生逃逸的前提下) 分配到栈上。其实这个机制本质上不算是JVM的特殊机制,这种设计从C语言就存在了。
(没明白为什么会成为一个比较热门的JVM)
逃逸分析
要理解逃逸分析,我们需要先看一下上面的栈上分配,逃逸分析其实就是指代上文的局部变量的逃逸行为,所谓逃逸其实就是原本属于方法内部的局部变量,被外部的对象引用或者作为返回值返回,导致它的生命周期脱离了方法的代码块区域。这一行为就被称为“逃逸”。此时再将这部分的“局部变量”的对象分配到栈上,显然是不合适的,往往这部分特殊的局部变量会被分配到堆上。
而JVM的逃逸分析其实就是针对对象是否存在这种行为做分析,这个分析和处理的过程就被称为逃逸分析。
// 对象未逃逸 - 可能在栈上分配
public void noEscape() {
User user = new User("test"); // 仅在方法内使用
}
// 对象逃逸 - 必须在堆上分配
public User doEscape() {
User user = new User("test");
return user; // 通过返回值逃逸
}
内联
内联的解释也是相当简单,其实就是JVM在做编译的时候,会将一些简单的调用方法,直接嵌入到调用方法之中。
这样做的理由也很简单,我们都知道线程都会有一个栈体来实际负责他的方法调用和执行处理,而如果涉及到不同的方法之间的调用,例如A调用B,在实际调用B时,会产生一个新的栈帧来记录B方法调用,而B方法执行完毕,又会将B栈帧弹出。这个过程又被成为弹栈。
而如果将一些简单的方法直接嵌入到调用方法之中,就可以减少栈帧的数量,减少资源的损耗。
// 优化前
private int add(int a, int b) {
return a + b;
}
int result = add(1, 2);
// 优化后 - JVM可能直接内联代码
int result = 1 + 2;
同步消除
同步消除的存在其实是针对一些程序开发人员看起来像是存在并发可能,并针对使用了锁,但JVM明确的知道这部分根本不可能出现并发问题。针对这种情况,JVM会将同步锁去掉,避免没必要的锁造成性能损耗。
// 优化前
public synchronized void method() {
// 方法体
}
// 如果JVM检测到该对象只在单线程中使用
// 优化后会消除不必要的同步
public void method() {
// 方法体
}
编译器优化
JVM还会针对一些比较特殊的代码情况做一些优化
-
循环优化
// 循环展开 for(int i = 0; i < 2; i++) { doSomething(); } // 可能优化为 doSomething(); doSomething(); // 循环无关代码外提 for(int i = 0; i < n; i++) { int temp = expensive(); // 循环无关 use(temp); } // 优化为 int temp = expensive(); for(int i = 0; i < n; i++) { use(temp); }
-
NULL值判断消除
// 优化前 if(obj != null) { obj.method(); } // 如果JVM能确定obj一定不为null // 优化后 obj.method();
GC回收概念解释
GC回收是为了回收当前栈帧之中不被直接或者间接引用,且不是方法区/永久代/元空间之中第三方类库静态变量、JVM运行基本对象等对象,更简单的说法就是回收目前已不活跃的对象。在GC回收上面,经常会出现概念混淆的问题,特别是各种GC模式的部分
各种GC模式
YoungGC(MirrorGC)
分代回收模型下的一种概念,也就是年轻代GC,触发条件只有一个:Eden区满了。常见的回收器(Serial、Parallel、ParNew)
在以G1 / ZGC为代表的新生代回收器之中,YoungGC的回收触发条件也是类似的,是Eden Region分配达到最大阈值时就会触发YoungGC
MajorGC
分代回收模型下的一种概念,也就是老年代GC,实际上在大部分的GC回收器之中,MajorGC除非不被触发,否则一定是FullGC触发。(特殊的GC回收器:CMS [碎片化空间,触发整理算法]、G1 / ZGC [直接MixedGC])
FullGC
全局回收策略,其实就是指代年轻代、老年代都进行GC回收的一种情况。当出现FullGC时,往往代表着堆空间资源紧张,需要更多的STW时间做回收,一般来说应当尽量减少FullGC的次数。触发FullGC的几种常见场景:
- 调用
System.gc()
(虽然说是程序建议JVM堆进行回收,但其实调了大概率还是会调用FullGC除非刚GC过的特殊情况) - 老年代空间不足 一般分为两种情况:
- 新生对象无法存入Eden区,试图存入老年代,但老年代空间不足
Allocation Failure
- Survivor晋升老年代,但老年代空间不足 GC 日志之中会出现
Promotion Failure
- 新生对象无法存入Eden区,试图存入老年代,但老年代空间不足
- Pemgen Space size / Meta Space size 永久代/元空间不足
往往会出现大量引入第三方jar包,而jvm内存又不够的时候。一般来说修改-XX: PermSize 、 -XX: MaxPermSize
/-XX: MetaSpaceSize 、-XX: MaxMetaSpaceSize
大小来实现修改。MetaSpace大小是弹性的,所以其实1.8后很少会出现元空间导致的Full GC - CMS特有的空间大量碎片化出现 GC日志出现
Concurrent Mode Failure
,碎片化太多了需要调用FullGC做碎片化空间整理
MixedGC
MixedGC其实是类似于G1一样的,老年代和年轻代通过同一种回收策略进行回收的回收器特有的回收概念,本质上其实是针对Young所属的Region和Olg所属的Region做回收的动作。本质上等同于FullGC。但从回收策略的角度来看肯定是有所不同的,详情可以参考另外一篇笔记:GC回收器介绍
值得一体的是,G1有的是:YoungGC和MixedGC
GC回收器介绍
该部分的介绍,我已经在其他的笔记之中做了较为完整的介绍在这里就不再重复介绍了,有需要请跳转 (GC回收器介绍)