This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
<audio id="audio" title="21 | 磨刀不误砍柴工欲知JVM调优先了解JVM内存模型" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ba/b8/ba587aeb95138d9bba29bf06b2d42fb8.mp3"></audio>
你好,我是刘超。
从今天开始我将和你一起探讨Java虚拟机JVM的性能调优。JVM算是面试中的高频问题了通常情况下总会有人问到请你讲解下JVM的内存模型JVM的性能调优做过吗
## 为什么JVM在Java中如此重要
首先你应该知道运行一个Java应用程序我们必须要先安装JDK或者JRE包。这是因为Java应用在编译后会变成字节码然后通过字节码运行在JVM中而JVM是JRE的核心组成部分。
JVM不仅承担了Java字节码的分析JIT compiler和执行Runtime同时也内置了自动内存分配管理机制。这个机制可以大大降低手动分配回收机制可能带来的内存泄露和内存溢出风险使Java开发人员不需要关注每个对象的内存分配以及回收从而更专注于业务本身。
## 从了解内存模型开始
JVM自动内存分配管理机制的好处很多但实则是把双刃剑。这个机制在提升Java开发效率的同时也容易使Java开发人员过度依赖于自动化弱化对内存的管理能力这样系统就很容易发生JVM的堆内存异常垃圾回收GC的方式不合适以及GC次数过于频繁等问题这些都将直接影响到应用服务的性能。
因此要进行JVM层面的调优就需要深入了解JVM内存分配和回收原理这样在遇到问题时我们才能通过日志分析快速地定位问题也能在系统遇到性能瓶颈时通过分析JVM调优来优化系统性能。这也是整个模块四的重点内容今天我们就从JVM的内存模型学起为后续的学习打下一个坚实的基础。
## JVM内存模型的具体设计
我们先通过一张JVM内存模型图来熟悉下其具体设计。在Java中JVM内存模型主要分为堆、程序计数器、方法区、虚拟机栈和本地方法栈。
<img src="https://static001.geekbang.org/resource/image/df/8b/dfd02c98d495c4c4ed201ea7fe0e3f8b.jpg" alt="">
JVM的5个分区具体是怎么实现的呢我们一一分析。
### 1. 堆Heap
堆是JVM内存中最大的一块内存空间该内存被所有线程共享几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代新生代又被进一步划分为Eden和Survivor区最后Survivor由From Survivor和To Survivor组成。
在Java6版本中永久代在非堆内存区到了Java7版本永久代的静态变量和运行时常量池被合并到了堆中而到了Java8永久代被元空间取代了。 结构如下图所示:
<img src="https://static001.geekbang.org/resource/image/99/6c/9906824978c891c86524f9394102de6c.png" alt="">
### 2. 程序计数器Program Counter Register
程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。
由于Java是多线程语言当执行的线程数量超过CPU核数时线程之间会根据时间片轮询争夺CPU资源。如果一个线程的时间片用完了或者是其它原因导致这个线程的CPU资源被提前抢夺那么这个退出的线程就需要单独的一个程序计数器来记录下一条运行的指令。
### 3. 方法区Method Area
很多开发者都习惯将方法区称为“永久代”,其实这两者并不是等价的。
HotSpot虚拟机使用永久代来实现方法区但在其它虚拟机中例如Oracle的JRockit、IBM的J9就不存在永久代一说。因此方法区只是JVM中规范的一部分可以说在HotSpot虚拟机中设计人员使用了永久代来实现了JVM规范的方法区。
方法区主要是用来存放已被虚拟机加载的类相关信息,包括类信息、运行时常量池、字符串常量池。类信息又包括了类的版本、字段、方法、接口和父类等信息。
JVM在执行某个类的时候必须经过加载、连接、初始化而连接又包括验证、准备、解析三个阶段。在加载类的时候JVM会先加载class文件而在class文件中除了有类的版本、字段、方法和接口等描述信息外还有一项信息是常量池(Constant Pool Table),用于存放编译期间生成的各种字面量和符号引用。
字面量包括字符串String a=“b”、基本类型的常量final修饰的变量符号引用则包括类和方法的全限定名例如String这个类它的全限定名就是Java/lang/String、字段的名称和描述符以及方法的名称和描述符。
而当类加载到内存中后JVM就会将class文件常量池中的内容存放到运行时的常量池中在解析阶段JVM会把符号引用替换为直接引用对象的索引值
例如类中的一个字符串常量在class文件中时存放在class文件常量池中的在JVM加载完类之后JVM会将这个字符串常量放到运行时常量池中并在解析阶段指定该字符串对象的索引值。运行时常量池是全局共享的多个类共用一个运行时常量池class文件中常量池多个相同的字符串在运行时常量池只会存在一份。
方法区与堆空间类似也是一个共享内存区所以方法区是线程共享的。假如两个线程都试图访问方法区中的同一个类信息而这个类还没有装入JVM那么此时就只允许一个线程去加载它另一个线程必须等待。
在HotSpot虚拟机、Java7版本中已经将永久代的静态变量和运行时常量池转移到了堆中其余部分则存储在JVM的非堆内存中而Java8版本已经将方法区中实现的永久代去掉了并用元空间class metadata代替了之前的永久代并且元空间的存储位置是本地内存。之前永久代的类的元数据存储在了元空间永久代的静态变量class static variables以及运行时常量池runtime constant pool则跟Java7一样转移到了堆中。
**那你可能又有疑问了Java8为什么使用元空间替代永久代这样做有什么好处呢**
官方给出的解释是:
- 移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力因为JRockit没有永久代所以不需要配置永久代。
- 永久代内存经常不够用或发生内存溢出爆出异常java.lang.OutOfMemoryError: PermGen。这是因为在JDK1.7版本中指定的PermGen区大小为8M由于PermGen中类的元数据信息在每次FullGC的时候都可能被收集回收率都偏低成绩很难令人满意还有为PermGen分配多大的空间很难确定PermSize的大小依赖于很多因素比如JVM加载的class总数、常量池的大小和方法的大小等。
### 4.虚拟机栈VM stack
Java虚拟机栈是线程私有的内存空间它和Java线程一起创建。当创建一个线程时会在虚拟机栈中申请一个线程栈用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作方法的返回则是栈帧的出栈操作。
### 5.本地方法栈Native Method Stack
本地方法栈跟Java虚拟机栈的功能类似Java虚拟机栈用于管理Java函数的调用而本地方法栈则用于管理本地方法的调用。但本地方法并不是用Java实现的而是由C语言实现的。
## JVM的运行原理
看到这里相信你对JVM内存模型已经有个充分的了解了。接下来我们通过一个案例来了解下代码和对象是如何分配存储的Java代码又是如何在JVM中运行的。
```
public class JVMCase {
// 常量
public final static String MAN_SEX_TYPE = &quot;man&quot;;
// 静态变量
public static String WOMAN_SEX_TYPE = &quot;woman&quot;;
public static void main(String[] args) {
Student stu = new Student();
stu.setName(&quot;nick&quot;);
stu.setSexType(MAN_SEX_TYPE);
stu.setAge(20);
JVMCase jvmcase = new JVMCase();
// 调用静态方法
print(stu);
// 调用非静态方法
jvmcase.sayHello(stu);
}
// 常规静态方法
public static void print(Student stu) {
System.out.println(&quot;name: &quot; + stu.getName() + &quot;; sex:&quot; + stu.getSexType() + &quot;; age:&quot; + stu.getAge());
}
// 非静态方法
public void sayHello(Student stu) {
System.out.println(stu.getName() + &quot;say: hello&quot;);
}
}
class Student{
String name;
String sexType;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSexType() {
return sexType;
}
public void setSexType(String sexType) {
this.sexType = sexType;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
```
**当我们通过Java运行以上代码时JVM的整个处理过程如下**
1.JVM向操作系统申请内存JVM第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间根据内存大小找到具体的内存分配表然后把内存段的起始地址和终止地址分配给JVM接下来JVM就进行内部分配。
2.JVM获得内存空间后会根据配置参数分配堆、栈以及方法区的内存大小。
3.class文件加载、验证、准备以及解析其中准备阶段会为类的静态变量分配内存初始化为系统的初始值这部分我在第21讲还会详细介绍
<img src="https://static001.geekbang.org/resource/image/94/32/94e6ebbaa0a23d677a4ad752e3e68732.jpg" alt="">
4.完成上一个步骤后将会进行最后一个初始化阶段。在这个阶段中JVM首先会执行构造器&lt;clinit&gt;方法,编译器会在.java 文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 &lt;clinit&gt;() 方法。
<img src="https://static001.geekbang.org/resource/image/29/59/29d54f4a8e1ecf388adc6b99cd5e0159.jpg" alt="">
5.执行方法。启动main线程执行main方法开始执行第一行代码。此时堆内存中会创建一个student对象对象引用student就存放在栈中。
<img src="https://static001.geekbang.org/resource/image/c6/7e/c6702aea3f1aaca60b1cd2e38981ad7e.jpg" alt="">
6.此时再次创建一个JVMCase对象调用sayHello非静态方法sayHello方法属于对象JVMCase此时sayHello方法入栈并通过栈中的student引用调用堆中的Student对象之后调用静态方法printprint静态方法属于JVMCase类是从静态方法中获取之后放入到栈中也是通过student引用调用堆中的student对象。
<img src="https://static001.geekbang.org/resource/image/b7/23/b7d00191a1d42def9633b8ea8491cf23.jpg" alt="">
了解完实际代码在JVM中分配的内存空间以及运行原理相信你会更加清楚内存模型中各个区域的职责分工。
## 总结
这讲我们主要深入学习了最基础的内存模型设计,了解其各个分区的作用及实现原理。
如今JVM在很大程度上减轻了Java开发人员投入到对象生命周期的管理精力。在使用对象的时候JVM会自动分配内存给对象在不使用的时候垃圾回收器会自动回收对象释放占用的内存。
但在某些情况下正常的生命周期不是最优的选择有些对象按照JVM默认的方式创建成本会很高。比如我在[第03讲](https://time.geekbang.org/column/article/97215)讲到的String对象在特定的场景使用String.intern可以很大程度地节约内存成本。我们可以使用不同的引用类型改变一个对象的正常生命周期从而提高JVM的回收效率这也是JVM性能调优的一种方式。
## 思考题
这讲我只提到了堆内存中对象分配内存空间的过程那如果有一个类中定义了String a="b"和String c = new String(“b”)请问这两个对象会分别创建在JVM内存模型中的哪块区域呢
期待在留言区看到你的答案。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,398 @@
<audio id="audio" title="22 | 深入JVM即时编译器JIT优化Java编译" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/be/09/be9221f38d34da3c01daa85acfc12a09.mp3"></audio>
你好,我是刘超。
说到编译,我猜你一定会想到 .java文件被编译成 .class文件的过程这个编译我们一般称为前端编译。Java的编译和运行过程非常复杂除了前端编译还有运行时编译。由于机器无法直接运行Java生成的字节码所以在运行时JIT或解释器会将字节码转换成机器码这个过程就叫运行时编译。
类文件在运行时被进一步编译它们可以变成高度优化的机器代码由于C/C++编译器的所有优化都是在编译期间完成的运行期间的性能监控仅作为基础的优化措施则无法进行例如调用频率预测、分支频率预测、裁剪未被选择的分支等而Java在运行时的再次编译就可以进行基础的优化措施。因此JIT编译器可以说是JVM中运行时编译最重要的部分之一。
然而许多Java开发人员对JIT编译器的了解并不多不深挖其工作原理也不深究如何检测应用程序的即时编译情况线上发生问题后很难做到从容应对。今天我们就来学习运行时编译如何实现对Java代码的优化。
## 类编译加载执行过程
在这之前我们先了解下Java从编译到运行的整个过程为后面的学习打下基础。请看下图
<img src="https://static001.geekbang.org/resource/image/8d/17/8d4ec9c73ec37d69adb105aa7d052717.jpg" alt="">
### 类编译
在编写好代码之后,我们需要将 .java文件编译成 .class文件才能在虚拟机上正常运行代码。文件的编译通常是由JDK中自带的Javac工具完成一个简单的 .java文件我们可以通过javac命令来生成 .class文件。
下面我们通过javap [第12讲](https://time.geekbang.org/column/article/101244) 讲过如何使用javap反编译命令行反编译来看看一个class文件结构中主要包含了哪些信息
<img src="https://static001.geekbang.org/resource/image/60/7e/605dbbcfbbfcc09143d7d9e7fed5ac7e.png" alt="">
看似一个简单的命令执行前期编译的过程其实是非常复杂的包括词法分析、填充符号表、注解处理、语义分析以及生成class文件这个过程我们不用过多关注。只要从上图中知道编译后的字节码文件主要包括常量池和方法表集合这两部分就可以了。
常量池主要记录的是类文件中出现的字面量以及符号引用。字面常量包括字符串常量例如String str=“abc”其中"abc"就是常量声明为final的属性以及一些基本类型例如范围在-127-128之间的整型的属性。符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用例如String str=“abc”其中str就是成员变量引用等。
方法表集合中主要包含一些方法的字节码、方法访问权限public、protect、prviate等、方法名索引与常量池中的方法引用对应、描述符索引、JVM执行指令以及属性集合等。
### 类加载
当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。
不同的实现类由不同的类加载器加载JDK中的本地方法类一般由根加载器Bootstrp loader加载进来JDK中内部实现的扩展类一般由扩展加载器ExtClassLoader 实现加载而程序中的类文件则由系统加载器AppClassLoader )实现加载。
在类加载后class类文件中的常量池信息以及其它数据会被保存到JVM内存的方法区中。
### 类连接
类在加载进来之后,会进行连接、初始化,最后才会被使用。在连接过程中,又包括验证、准备和解析三个部分。
**验证:**验证类符合Java规范和JVM规范在保证符合规范的前提下避免危害虚拟机安全。
**准备:**为类的静态变量分配内存初始化为系统的初始值。对于final static修饰的变量直接赋值为用户的定义值。例如private final static int value=123会在准备阶段分配内存并初始化值为123而如果是 private static int value=123这个阶段value的值仍然为0。
**解析:**将符号引用转为直接引用的过程。我们知道在编译时Java类并不知道所引用的类的实际地址因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法就需要把它们转化为JVM可以直接获取的内存地址或指针即直接引用。
### 类初始化
类初始化阶段是类加载过程的最后阶段在这个阶段中JVM首先将执行构造器&lt;clinit&gt;方法,编译器会在将 .java 文件编译成 .class 文件时,收集所有类初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 &lt;clinit&gt;() 方法。
初始化类的静态变量和静态代码块为用户自定义的值初始化的顺序和Java源码从上到下的顺序一致。例如
```
private static int i=1
static{
i=0;
}
public static void main(String [] args){
System.out.println(i);
}
```
此时运行结果为:
```
0
```
再来看看以下代码:
```
static{
i=0;
}
private static int i=1
public static void main(String [] args){
System.out.println(i);
}
```
此时运行结果为:
```
1
```
子类初始化时会首先调用父类的 &lt;clinit&gt;() 方法,再执行子类的&lt;clinit&gt;() 方法,运行以下代码:
```
public class Parent{
public static String parentStr= &quot;parent static string&quot;;
static{
System.out.println(&quot;parent static fields&quot;);
System.out.println(parentStr);
}
public Parent(){
System.out.println(&quot;parent instance initialization&quot;);
}
}
public class Sub extends Parent{
public static String subStr= &quot;sub static string&quot;;
static{
System.out.println(&quot;sub static fields&quot;);
System.out.println(subStr);
}
public Sub(){
System.out.println(&quot;sub instance initialization&quot;);
}
public static void main(String[] args){
System.out.println(&quot;sub main&quot;);
new Sub();
}
}
```
运行结果:
```
parent static fields
parent static string
sub static fields
sub static string
sub main
parent instance initialization
sub instance initialization
```
JVM 会保证 &lt;clinit&gt;() 方法的线程安全,保证同一时间只有一个线程执行。
JVM在初始化执行代码时如果实例化一个新对象会调用&lt;init&gt;方法对实例变量进行初始化,并执行对应的构造方法内的代码。
## 即时编译
初始化完成后,类在调用执行过程中,执行引擎会把字节码转为机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译。
最初,虚拟机中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。
为了提高热点代码的执行效率在运行时即时编译器JIT会把这些代码编译成与本地平台相关的机器码并进行各层次的优化然后保存到内存中。
### 即时编译器类型
在HotSpot虚拟机中内置了两个JIT分别为C1编译器和C2编译器这两个编译器的编译过程是不一样的。
C1编译器是一个简单快速的编译器主要的关注点在于局部性的优化适用于执行时间较短或对启动性能有要求的程序例如GUI应用对界面启动速度就有一定要求。
C2编译器是为长期运行的服务器端应用程序做性能调优的编译器适用于执行时间较长或对峰值性能有要求的程序。根据各自的适配性这两种即时编译也被称为Client Compiler和Server Compiler。
在Java7之前需要根据程序的特性来选择对应的JIT虚拟机默认采用解释器和其中一个编译器配合工作。
Java7引入了分层编译这种方式综合了C1的启动性能优势和C2的峰值性能优势我们也可以通过参数 “-client”“-server” 强制指定虚拟机的即时编译模式。**分层编译将JVM的执行状态分为了5个层次**
- 第0层程序解释执行默认开启性能监控功能Profiling如果不开启可触发第二层编译
- 第1层可称为C1编译将字节码编译为本地代码进行简单、可靠的优化不开启Profiling
- 第2层也称为C1编译开启Profiling仅执行带方法调用次数和循环回边执行次数profiling的C1编译
- 第3层也称为C1编译执行所有带Profiling的C1编译
- 第4层可称为C2编译也是将字节码编译为本地代码但是会启用一些编译耗时较长的优化甚至会根据性能监控信息进行一些不可靠的激进优化。
在Java8中默认开启分层编译-client和-server的设置已经是无效的了。如果只想开启C2可以关闭分层编译-XX:-TieredCompilation如果只想用C1可以在打开分层编译的同时使用参数-XX:TieredStopAtLevel=1。
除了这种默认的混合编译模式,我们还可以使用“-Xint”参数强制虚拟机运行于只有解释器的编译模式下这时JIT完全不介入工作我们还可以使用参数“-Xcomp”强制虚拟机运行于只有JIT的编译模式下。
通过 java -version 命令行可以直接查看到当前系统使用的编译模式。如下图所示:
<img src="https://static001.geekbang.org/resource/image/6e/11/6ea0938770cccc1b17c46f7b37d20711.jpg" alt="">
### 热点探测
在HotSpot虚拟机中的热点探测是JIT优化的条件热点探测是基于计数器的热点探测采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数如果执行次数超过一定的阈值就认为它是“热点方法” 。
虚拟机为每个方法准备了两类计数器方法调用计数器Invocation Counter和回边计数器Back Edge Counter。在确定虚拟机运行参数的前提下这两个计数器都有一个确定的阈值当计数器超过阈值溢出了就会触发JIT编译。
**方法调用计数器:**用于统计方法被调用的次数方法调用计数器的默认阈值在C1模式下是1500次在C2模式在是10000次可通过-XX: CompileThreshold来设定而在分层编译的情况下-XX: CompileThreshold指定的阈值将失效此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值时就会触发JIT编译器。
**回边计数器:**用于统计一个方法中循环体代码执行的次数在字节码中遇到控制流向后跳转的指令称为“回边”Back Edge该值用于计算是否触发C1编译的阈值在不开启分层编译的情况下C1默认为13995C2默认为10700可通过-XX: OnStackReplacePercentage=N来设置而在分层编译的情况下-XX: OnStackReplacePercentage指定的阈值同样会失效此时将根据当前待编译的方法数以及编译线程数来动态调整。
建立回边计数器的主要目的是为了触发OSROn StackReplacement编译即栈上编译。在一些循环周期比较长的代码段中当循环达到回边计数器阈值时JVM会认为这段是热点代码JIT编译器就会将这段代码编译成机器语言并缓存在该循环时间段内会直接将执行代码替换执行缓存的机器语言。
### 编译优化技术
JIT编译运用了一些经典的编译优化技术来实现代码的优化即通过一些例行检查优化可以智能地编译出运行时的最优性能代码。今天我们主要来学习以下两种优化手段
**1.方法内联**
调用一个方法通常要经历压栈和出栈。调用方法是将程序执行顺序转移到存储该方法的内存地址,将方法的内容执行完后,再返回到执行该方法前的位置。
这种执行操作要求在执行前保护现场并记忆执行的地址,执行后要恢复现场,并按原来保存的地址继续执行。 因此,方法调用会产生一定的时间和空间方面的开销。
那么对于那些方法体代码不是很大,又频繁调用的方法来说,这个时间和空间的消耗会很大。方法内联的优化行为就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用。
例如以下方法:
```
private int add1(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
private int add2(int x1, int x2) {
return x1 + x2;
}
```
最终会被优化为:
```
private int add1(int x1, int x2, int x3, int x4) {
return x1 + x2+ x3 + x4;
}
```
JVM会自动识别热点方法并对它们使用方法内联进行优化。我们可以通过-XX:CompileThreshold来设置热点方法的阈值。但要强调一点热点方法不一定会被JVM做内联优化如果这个方法体太大了JVM将不执行内联操作。而方法体的大小阈值我们也可以通过参数设置来优化
- 经常执行的方法默认情况下方法体大小小于325字节的都会进行内联我们可以通过-XX:MaxFreqInlineSize=N来设置大小值
- 不是经常执行的方法默认情况下方法大小小于35字节才会进行内联我们也可以通过-XX:MaxInlineSize=N来重置大小值。
之后我们就可以通过配置JVM参数来查看到方法被内联的情况
```
-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来
```
当我们设置VM参数-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining之后运行以下代码
```
public static void main(String[] args) {
for(int i=0; i&lt;1000000; i++) {//方法调用计数器的默认阈值在C1模式下是1500次在C2模式在是10000次我们循环遍历超过需要阈值
add1(1,2,3,4);
}
}
```
我们可以看到运行结果中,显示了方法内联的日志:
<img src="https://static001.geekbang.org/resource/image/ed/80/eda343938d9fa4ebb6b3331ae7b62680.jpg" alt="">
热点方法的优化可以有效提高系统性能,一般我们可以通过以下几种方式来提高方法内联:
- 通过设置JVM参数来减小热点阈值或增加方法体阈值以便更多的方法可以进行内联但这种方法意味着需要占用更多地内存
- 在编程中,避免在一个方法中写大量代码,习惯使用小方法体;
- 尽量使用final、private、static关键字修饰方法编码方法因为继承会需要额外的类型检查。
2.逃逸分析
逃逸分析Escape Analysis是判断一个对象是否被外部方法引用或外部线程访问的分析技术编译器会根据逃逸分析的结果对代码进行优化。
**栈上分配**
我们知道在Java中默认创建一个对象是在堆中分配内存的而当堆内存中的对象不再使用时则需要通过垃圾回收机制回收这个过程相对分配在栈中的对象的创建和销毁来说更消耗时间和性能。这个时候逃逸分析如果发现一个对象只在方法中使用就会将对象分配在栈上。
以下是通过循环获取学生年龄的案例,方法中创建一个学生对象,我们现在通过案例来看看打开逃逸分析和关闭逃逸分析后,堆内存对象创建的数量对比。
```
public static void main(String[] args) {
for (int i = 0; i &lt; 200000 ; i++) {
getAge();
}
}
public static int getAge(){
Student person = new Student(&quot;小明&quot;,18,30);
return person.getAge();
}
static class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
```
然后我们分别设置VM参数Xmx1000m -Xms1000m -XX:-DoEscapeAnalysis -XX:+PrintGC以及 -Xmx1000m -Xms1000m -XX:+DoEscapeAnalysis -XX:+PrintGC通过之前讲过的VisualVM工具查看堆中创建的对象数量。
然而运行结果却没有达到我们想要的优化效果也许你怀疑是JDK版本的问题然而我分别在1.6~1.8版本都测试过了,效果还是一样的:
-server -Xmx1000m -Xms1000m -XX:-DoEscapeAnalysis -XX:+PrintGC
<img src="https://static001.geekbang.org/resource/image/83/a5/8313c788b71e4df16b29a98bb4b63ca5.jpg" alt="">
-server -Xmx1000m -Xms1000m -XX:+DoEscapeAnalysis -XX:+PrintGC
<img src="https://static001.geekbang.org/resource/image/25/63/259bd540cca1120813146cebbebef763.jpg" alt="">
这其实是因为HotSpot虚拟机目前的实现导致栈上分配实现比较复杂可以说在HotSpot中暂时没有实现这项优化。随着即时编译器的发展与逃逸分析技术的逐渐成熟相信不久的将来HotSpot也会实现这项优化功能。
** 锁消除**
在非线程安全的情况下尽量不要使用线程安全容器比如StringBuffer。由于StringBuffer中的append方法被Synchronized关键字修饰会使用到锁从而导致性能下降。
但实际上在以下代码测试中StringBuffer和StringBuilder的性能基本没什么区别。这是因为在局部方法中创建的对象只能被当前线程访问无法被其它线程访问这个变量的读写肯定不会有竞争这个时候JIT编译会对这个对象的方法锁进行锁消除。
```
public static String getString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
```
**标量替换**
逃逸分析证明一个对象不会被外部访问,如果这个对象可以被拆分的话,当程序真正执行的时候可能不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了。这种编译优化就叫做标量替换。
我们用以下代码验证:
```
public void foo() {
TestInfo info = new TestInfo();
info.id = 1;
info.count = 99;
...//to do something
}
```
逃逸分析后,代码会被优化为:
```
public void foo() {
id = 1;
count = 99;
...//to do something
}
```
我们可以通过设置JVM参数来开关逃逸分析还可以单独开关同步消除和标量替换在JDK1.8中JVM是默认开启这些操作的。
```
-XX:+DoEscapeAnalysis开启逃逸分析jdk1.8默认开启,其它版本未测试)
-XX:-DoEscapeAnalysis 关闭逃逸分析
-XX:+EliminateLocks开启锁消除jdk1.8默认开启,其它版本未测试)
-XX:-EliminateLocks 关闭锁消除
-XX:+EliminateAllocations开启标量替换jdk1.8默认开启,其它版本未测试)
-XX:-EliminateAllocations 关闭就可以了
```
## 总结
今天我们主要了解了JKD1.8以及之前的类的编译和加载过程Java源程序是通过Javac编译器编译成 .class文件其中文件中包含的代码格式我们称之为Java字节码bytecode
这种代码格式无法直接运行但可以被不同平台JVM中的Interpreter解释执行。由于Interpreter的效率低下JVM中的JIT会在运行时有选择性地将运行次数较多的方法编译成二进制代码直接运行在底层硬件上。
在Java8之前HotSpot集成了两个JIT用C1和C2来完成JVM中的即时编译。虽然JIT优化了代码但收集监控信息会消耗运行时的性能且编译过程会占用程序的运行时间。
到了Java9AOT编译器被引入。和JIT不同AOT是在程序运行前进行的静态编译这样就可以避免运行时的编译消耗和内存消耗且 .class文件通过AOT编译器是可以编译成 .so的二进制文件的。
到了Java10一个新的JIT编译器Graal被引入。Graal 是一个以 Java 为主要编程语言、面向 Java bytecode 的编译器。与用 C++ 实现的 C1 和 C2 相比它的模块化更加明显也更容易维护。Graal 既可以作为动态编译器,在运行时编译热点方法;也可以作为静态编译器,实现 AOT 编译。
## 思考题
我们知道Class.forName和ClassLoader.loadClass都能加载类你知道这两者在加载类时的区别吗
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,152 @@
<audio id="audio" title="23 | 如何优化垃圾回收机制?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bc/92/bcd855b07fef56d5995519f1505e5792.mp3"></audio>
你好,我是刘超。
我们知道在Java开发中开发人员是无需过度关注对象的回收与释放的JVM的垃圾回收机制可以减轻不少工作量。但完全交由JVM回收对象也会增加回收性能的不确定性。在一些特殊的业务场景下不合适的垃圾回收算法以及策略都有可能导致系统性能下降。
面对不同的业务场景垃圾回收的调优策略也不一样。例如在对内存要求苛刻的情况下需要提高对象的回收效率在CPU使用率高的情况下需要降低高并发时垃圾回收的频率。可以说垃圾回收的调优是一项必备技能。
这讲我们就把这项技能的学习进行拆分看看回收后面简称GC的算法有哪些体现GC算法好坏的指标有哪些又如何根据自己的业务场景对GC策略进行调优
## 垃圾回收机制
掌握GC算法之前我们需要先弄清楚3个问题。第一回收发生在哪里第二对象在什么时候可以被回收第三如何回收这些对象
### 1. 回收发生在哪里?
JVM的内存区域中程序计数器、虚拟机栈和本地方法栈这3个区域是线程私有的随着线程的创建而创建销毁而销毁栈中的栈帧随着方法的进入和退出进行入栈和出栈操作每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的因此这三个区域的内存分配和回收都具有确定性。
那么垃圾回收的重点就是关注堆和方法区中的内存了,堆中的回收主要是对象的回收,方法区的回收主要是废弃常量和无用的类的回收。
### 2. 对象在什么时候可以被回收?
那JVM又是怎样判断一个对象是可以被回收的呢一般一个对象不再被引用就代表该对象可以被回收。目前有以下两种算法可以判断该对象是否可以被回收。
**引用计数算法:**这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对象被引用引用计数器就会加1每当引用失效计数器就会减1。当对象的引用计数器的值为0时就说明该对象不再被引用可以被回收了。这里强调一点虽然引用计数算法的实现简单判断效率也很高但它存在着对象之间相互循环引用的问题。
**可达性分析算法:**GC Roots 是该算法的基础GC Roots是所有对象的根对象在JVM加载时会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点在垃圾回收时会从这些GC Roots开始向下搜索当一个对象到 GC Roots 没有任何引用链相连时就证明此对象是不可用的。目前HotSpot虚拟机采用的就是这种算法。
以上两种算法都是通过引用来判断对象是否可以被回收。在 JDK 1.2 之后Java 对引用的概念进行了扩充,将引用分为了以下四种:
<img src="https://static001.geekbang.org/resource/image/5c/0a/5c671c5ae73cbb8bc14b38d9e871530a.jpg" alt="">
### 3. 如何回收这些对象?
了解完Java程序中对象的回收条件那么垃圾回收线程又是如何回收这些对象的呢JVM垃圾回收遵循以下两个特性。
**自动性:**Java提供了一个系统级的线程来跟踪每一块分配出去的内存空间当JVM处于空闲循环时垃圾收集器线程会自动检查每一块分配出去的内存空间然后自动回收每一块空闲的内存块。
**不可预期性:**一旦一个对象没有被引用了,该对象是否立刻被回收呢?答案是不可预期的。我们很难确定一个没有被引用的对象是不是会被立刻回收掉,因为有可能当程序结束后,这个对象仍在内存中。
垃圾回收线程在JVM中是自动执行的Java程序无法强制执行。我们唯一能做的就是通过调用System.gc方法来"建议"执行垃圾收集器,但是否可执行,什么时候执行?仍然不可预期。
## GC算法
JVM提供了不同的回收算法来实现这一套回收机制通常垃圾收集器的回收算法可以分为以下几种
<img src="https://static001.geekbang.org/resource/image/3f/b9/3f4316c41d4ffb27e5a36db5f2641db9.jpg" alt="">
如果说收集算法是内存回收的方法论那么垃圾收集器就是内存回收的具体实现JDK1.7 update14 之后Hotspot虚拟机所有的回收器整理如下以下为服务端垃圾收集器
<img src="https://static001.geekbang.org/resource/image/28/74/2824581e7c94a3a94b2b0abb1d348974.jpg" alt="">
其实在JVM规范中并没有明确GC的运作方式各个厂商可以采用不同的方式实现垃圾收集器。我们可以通过JVM工具查询当前JVM使用的垃圾收集器类型首先通过ps命令查询出进程ID再通过jmap -heap ID查询出JVM的配置信息其中就包括垃圾收集器的设置类型。
<img src="https://static001.geekbang.org/resource/image/95/97/953dc139ff9035b41d06d4a400395e97.png" alt="">
## GC性能衡量指标
一个垃圾收集器在不同场景下表现出的性能也不一样,那么如何评价一个垃圾收集器的性能好坏呢?我们可以借助一些指标。
**吞吐量:**这里的吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算GC的吞吐量系统总运行时间=应用程序耗时+GC耗时。如果系统运行了100分钟GC耗时1分钟则系统吞吐量为99%。GC的吞吐量一般不能低于95%。
**停顿时间:**指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
**垃圾回收频率:**多久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。
## 查看&amp;分析GC日志
已知了性能衡量指标现在我们需要通过工具查询GC相关日志统计各项指标的信息。首先我们需要通过JVM参数预先设置GC日志通常有以下几种JVM参数设置
```
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳以基准时间的形式
-XX:+PrintGCDateStamps 输出GC的时间戳以日期的形式如 2013-05-04T21:53:59.234+0800
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
```
这里使用如下参数来打印日志:
```
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:./gclogs
```
打印后的日志为:
<img src="https://static001.geekbang.org/resource/image/58/58/58d74b6e3e68edf9b595287686b42b58.png" alt="">
上图是运行很短时间的GC日志如果是长时间的GC日志我们很难通过文本形式去查看整体的GC性能。此时我们可以通过[GCViewer](https://sourceforge.net/projects/gcviewer/)工具打开日志文件图形化界面查看整体的GC性能如下图所示
<img src="https://static001.geekbang.org/resource/image/69/79/69db951663299d342aad572d911b0279.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/f9/37/f95d5db87d20068085b6d67cd6822d37.png" alt="">
通过工具我们可以看到吞吐量、停顿时间以及GC的频率从而可以非常直观地了解到GC的性能情况。
这里我再推荐一个比较好用的GC日志分析工具[GCeasy](https://www.gceasy.io/index.jsp)是一款非常直观的GC日志分析工具我们可以将日志文件压缩之后上传到GCeasy官网即可看到非常清楚的GC日志分析结果
<img src="https://static001.geekbang.org/resource/image/ab/22/ab3119a73f313d20a4aa0cee02e84022.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/ef/27/ef85b02537b9c970d55d3bbd5a3e3427.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/71/95/71e8f8922bc7045a7b52e5a6dff82595.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/83/be/834d779d27afbee8c70219c1628f0bbe.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/83/ba/830069547013fdcbbb74c1e9b75a77ba.jpeg" alt=""><img src="https://static001.geekbang.org/resource/image/63/8e/638ede71247b855b50e04a25564f268e.jpeg" alt="">
## GC调优策略
找出问题后就可以进行调优了下面介绍几种常用的GC调优策略。
### 1. 降低Minor GC频率
通常情况下由于新生代空间较小Eden区很快被填满就会导致频繁Minor GC因此我们可以通过增大新生代空间来降低Minor GC的频率。
可能你会有这样的疑问扩容Eden区虽然可以减少Minor GC的次数但不会增加单次Minor GC的时间吗如果单次Minor GC的时间增加那也很难达到我们期待的优化效果呀。
我们知道单次Minor GC时间是由两部分组成T1扫描新生代和T2复制存活对象。假设一个对象在Eden区的存活时间为500msMinor GC的时间间隔是300ms那么正常情况下Minor GC的时间为 T1+T2。
当我们增大新生代空间Minor GC的时间间隔可能会扩大到600ms此时一个存活500ms的对象就会在Eden区中被回收掉此时就不存在复制存活对象了所以再发生Minor GC的时间为两次扫描新生代即2T1。
可见扩容后Minor GC时增加了T1但省去了T2的时间。通常在虚拟机中复制对象的成本要远高于扫描成本。
如果在堆内存中存在较多的长期存活的对象此时增加年轻代空间反而会增加Minor GC的时间。如果堆中的短期对象很多那么扩容新生代单次Minor GC时间不会显著增加。因此单次Minor GC时间更多取决于GC后存活对象的数量而非Eden区的大小。
### 2. 降低Full GC的频率
通常情况下由于堆内存空间不足或老年代对象太多会触发Full GC频繁的Full GC会带来上下文切换增加系统的性能开销。我们可以使用哪些方法来降低Full GC的频率呢
**减少创建大对象:**在平常的业务场景中我们习惯一次性从数据库中查询出一个大对象用于web端显示。例如我之前碰到过一个一次性查询出60个字段的业务操作这种大对象如果超过年轻代最大对象阈值会被直接创建在老年代即使被创建在了年轻代由于年轻代的内存空间有限通过Minor GC之后也会进入到老年代。这种大对象很容易产生较多的Full GC。
我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。
**增大堆内存空间:**在堆内存不足的情况下增大堆内存空间且设置初始化堆内存为最大堆内存也可以降低Full GC的频率。
### 选择合适的GC回收器
假设我们有这样一个需求要求每次操作的响应时间必须在500ms以内。这个时候我们一般会选择响应速度较快的GC回收器CMSConcurrent Mark Sweep回收器和G1回收器都是不错的选择。
而当我们的需求对系统吞吐量有要求时就可以选择Parallel Scavenge回收器来提高系统的吞吐量。
## 总结
今天的内容比较多,最后再强调几个重点。
垃圾收集器的种类很多我们可以将其分成两种类型一种是响应速度快一种是吞吐量高。通常情况下CMS和G1回收器的响应速度快Parallel Scavenge回收器的吞吐量高。
在JDK1.8环境下默认使用的是Parallel Scavenge年轻代+Serial Old老年代垃圾收集器你可以通过文中介绍的查询JVM的GC默认配置方法进行查看。
通常情况JVM是默认垃圾回收优化的在没有性能衡量标准的前提下尽量避免修改GC的一些性能配置参数。如果一定要改那就必须基于大量的测试结果或线上的具体性能来进行调整。
## 思考题
以上我们讲到了CMS和G1回收器你知道G1是如何实现更好的GC性能的吗
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,169 @@
<audio id="audio" title="24 | 如何优化JVM内存分配" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/c7/6dec37d734d5d11fb373b069946de5c7.mp3"></audio>
你好,我是刘超。
JVM调优是一个系统而又复杂的过程但我们知道在大多数情况下我们基本不用去调整JVM内存分配因为一些初始化的参数已经可以保证应用服务正常稳定地工作了。
但所有的调优都是有目标性的JVM内存分配调优也一样。没有性能问题的时候我们自然不会随意改变JVM内存分配的参数。那有了问题呢有了什么样的性能问题我们需要对其进行调优呢又该如何调优呢这就是我今天要分享的内容。
## JVM内存分配性能问题
谈到JVM内存表现出的性能问题时你可能会想到一些线上的JVM内存溢出事故。但这方面的事故往往是应用程序创建对象导致的内存回收对象难一般属于代码编程问题。
但其实很多时候在应用服务的特定场景下JVM内存分配不合理带来的性能表现并不会像内存溢出问题这么突出。可以说如果你没有深入到各项性能指标中去是很难发现其中隐藏的性能损耗。
JVM内存分配不合理最直接的表现就是频繁的GC这会导致上下文切换等性能问题从而降低系统的吞吐量、增加系统的响应时间。因此如果你在线上环境或性能测试时发现频繁的GC且是正常的对象创建和回收这个时候就需要考虑调整JVM内存分配了从而减少GC所带来的性能开销。
## 对象在堆中的生存周期
了解了性能问题那需要做的势必就是调优了。但先别急在了解JVM内存分配的调优过程之前我们先来看看一个新创建的对象在堆内存中的生存周期为后面的学习打下基础。
在[第20讲](https://time.geekbang.org/column/article/106203)中我讲过JVM内存模型。我们知道在JVM内存模型的堆中堆被划分为新生代和老年代新生代又被进一步划分为Eden区和Survivor区最后Survivor由From Survivor和To Survivor组成。
当我们新建一个对象时对象会被优先分配到新生代的Eden区中这时虚拟机会给对象定义一个对象年龄计数器通过参数-XX:MaxTenuringThreshold设置
同时也有另外一种情况当Eden空间不足时虚拟机将会执行一个新生代的垃圾回收Minor GC。这时JVM会把存活的对象转移到Survivor中并给对象的年龄+1。对象在Survivor中同样也会经历MinorGC每经过一次MinorGC对象的年龄将会+1。
当然了,内存空间也是有设置阈值的,可以通过参数-XX:PetenureSizeThreshold设置直接被分配到老年代的最大对象这时如果分配的对象超过了设置的阀值对象就会直接被分配到老年代这样做的好处就是可以减少新生代的垃圾回收。
## 查看JVM堆内存分配
我们知道了一个对象从创建至回收到堆中的过程接下来我们再来了解下JVM堆内存是如何分配的。在默认不配置JVM堆内存大小的情况下JVM根据默认值来配置当前内存大小。我们可以通过以下命令来查看堆内存配置的默认值
```
java -XX:+PrintFlagsFinal -version | grep HeapSize
jmap -heap 17284
```
<img src="https://static001.geekbang.org/resource/image/43/d5/436338cc5251291eeb6dbb57467443d5.png" alt="">
<img src="https://static001.geekbang.org/resource/image/59/de/59941c65600fe4a11e5bc6b8304fe0de.png" alt="">
通过命令我们可以获得在这台机器上启动的JVM默认最大堆内存为1953MB初始化大小为124MB。
在JDK1.7中默认情况下年轻代和老年代的比例是1:2我们可以通过XX:NewRatio重置该配置项。年轻代中的Eden和To Survivor、From Survivor的比例是8:1:1我们可以通过-XX:SurvivorRatio重置该配置项。
在JDK1.7中如果开启了-XX:+UseAdaptiveSizePolicy配置项JVM将会动态调整Java堆中各个区域的大小以及进入老年代的年龄XX:NewRatio和-XX:SurvivorRatio将会失效而JDK1.8是默认开启-XX:+UseAdaptiveSizePolicy配置项的。
还有在JDK1.8中不要随便关闭UseAdaptiveSizePolicy配置项除非你已经对初始化堆内存/最大堆内存、年轻代/老年代以及Eden区/Survivor区有非常明确的规划了。否则JVM将会分配最小堆内存年轻代和老年代按照默认比例1:2进行分配年轻代中的Eden和Survivor则按照默认比例8:2进行分配。这个内存分配未必是应用服务的最佳配置因此可能会给应用服务带来严重的性能问题。
## JVM内存分配的调优过程
我们先使用JVM的默认配置观察应用服务的运行情况下面我将结合一个实际案例来讲述。现模拟一个抢购接口假设需要满足一个5W的并发请求且每次请求会产生20KB对象我们可以通过千级并发创建一个1MB对象的接口来模拟万级并发请求产生大量对象的场景具体代码如下
```
@RequestMapping(value = &quot;/test1&quot;)
public String test1(HttpServletRequest request) {
List&lt;Byte[]&gt; temp = new ArrayList&lt;Byte[]&gt;();
Byte[] b = new Byte[1024*1024];
temp.add(b);
return &quot;success&quot;;
}
```
### AB压测
分别对应用服务进行压力测试,以下是请求接口的吞吐量和响应时间在不同并发用户数下的变化情况:
<img src="https://static001.geekbang.org/resource/image/8b/26/8b67579af661a666dff89d16ab2e2f26.jpg" alt="">
可以看到当并发数量到了一定值时吞吐量就上不去了响应时间也迅速增加。那么在JVM内部运行又是怎样的呢
### 分析GC日志
此时我们可以通过GC日志查看具体的回收日志。我们可以通过设置VM配置参数将运行期间的GC日志 dump下来具体配置参数如下
```
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/log/heapTest.log
```
以下是各个配置项的说明:
- -XX:PrintGCTimeStamps打印GC具体时间
- -XX:PrintGCDetails 打印出GC详细日志
- -Xloggc: pathGC日志生成路径。
收集到GC日志后我们就可以使用[第22讲](https://time.geekbang.org/column/article/107396)中介绍过的GCViewer工具打开它进而查看到具体的GC日志如下
<img src="https://static001.geekbang.org/resource/image/bf/d1/bffd496963f6bbd345092c1454524dd1.jpeg" alt="">
主页面显示FullGC发生了13次右下角显示年轻代和老年代的内存使用率几乎达到了100%。而FullGC会导致stop-the-world的发生从而严重影响到应用服务的性能。此时我们需要调整堆内存的大小来减少FullGC的发生。
### 参考指标
我们可以将某些指标的预期值作为参考指标上面的GC频率就是其中之一那么还有哪些指标可以为我们提供一些具体的调优方向呢
**GC频率**高频的FullGC会给系统带来非常大的性能消耗虽然MinorGC相对FullGC来说好了许多但过多的MinorGC仍会给系统带来压力。
**内存:**这里的内存指的是堆内存大小堆内存又分为年轻代内存和老年代内存。首先我们要分析堆内存大小是否合适其实是分析年轻代和老年代的比例是否合适。如果内存不足或分配不均匀会增加FullGC严重的将导致CPU持续爆满影响系统性能。
**吞吐量:**频繁的FullGC将会引起线程的上下文切换增加系统的性能开销从而影响每次处理的线程请求最终导致系统的吞吐量下降。
**延时:**JVM的GC持续时间也会影响到每次请求的响应时间。
### 具体调优方法
**调整堆内存空间减少FullGC**通过日志分析堆内存基本被用完了而且存在大量FullGC这意味着我们的堆内存严重不足这个时候我们需要调大堆内存空间。
```
java -jar -Xms4g -Xmx4g heapTest-0.0.1-SNAPSHOT.jar
```
以下是各个配置项的说明:
- -Xms堆初始大小
- -Xmx堆最大值。
调大堆内存之后我们再来测试下性能情况发现吞吐量提高了40%左右响应时间也降低了将近50%。
<img src="https://static001.geekbang.org/resource/image/5f/af/5fd7c3f198018cf5e789c25bd4f14caf.png" alt="">
再查看GC日志发现FullGC频率降低了老年代的使用率只有16%了。
<img src="https://static001.geekbang.org/resource/image/b9/2e/b924a13d8cb4e383b94e82d34125002e.jpeg" alt="">
**调整年轻代减少MinorGC**通过调整堆内存大小我们已经提升了整体的吞吐量降低了响应时间。那还有优化空间吗我们还可以将年轻代设置得大一些从而减少一些MinorGC[第22讲](https://time.geekbang.org/column/article/107396)有通过降低Minor GC频率来提高系统性能的详解
```
java -jar -Xms4g -Xmx4g -Xmn3g heapTest-0.0.1-SNAPSHOT.jar
```
再进行AB压测发现吞吐量上去了。
<img src="https://static001.geekbang.org/resource/image/55/04/55de34ab7eccaf9ad83bac846d0cbf04.png" alt="">
再查看GC日志发现MinorGC也明显降低了GC花费的总时间也减少了。
<img src="https://static001.geekbang.org/resource/image/75/5d/75f84993ba0c52d6d338d19dd4db1a5d.jpeg" alt="">
**设置Eden、Survivor区比例**在JVM中如果开启 AdaptiveSizePolicy则每次 GC 后都会重新计算 Eden、From Survivor和 To Survivor区的大小计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。这个时候SurvivorRatio默认设置的比例会失效。
在JDK1.8中默认是开启AdaptiveSizePolicy的我们可以通过-XX:-UseAdaptiveSizePolicy关闭该项配置或显示运行-XX:SurvivorRatio=8将Eden、Survivor的比例设置为8:2。大部分新对象都是在Eden区创建的我们可以固定Eden区的占用比例来调优JVM的内存分配性能。
再进行AB性能测试我们可以看到吞吐量提升了响应时间降低了。
<img src="https://static001.geekbang.org/resource/image/91/c2/91eb734ea45dc8a24b7938401eafb7c2.png" alt="">
<img src="https://static001.geekbang.org/resource/image/cf/fd/cfaef71dbc8ba4f149a6e134482370fd.jpeg" alt="">
## 总结
JVM内存调优通常和GC调优是互补的基于以上调优我们可以继续对年轻代和堆内存的垃圾回收算法进行调优。这里可以结合上一讲的内容一起完成JVM调优。
虽然分享了一些JVM内存分配调优的常用方法但我还是建议你在进行性能压测后如果没有发现突出的性能瓶颈就继续使用JVM默认参数起码在大部分的场景下默认配置已经可以满足我们的需求了。但满足不了也不要慌张结合今天所学的内容去实践一下相信你会有新的收获。
## 思考题
以上我们都是基于堆内存分配来优化系统性能的但在NIO的Socket通信中其实还使用到了堆外内存来减少内存拷贝实现Socket通信优化。你知道堆外内存是如何创建和回收的吗
期待在留言区看到你的见解。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,232 @@
<audio id="audio" title="25 | 内存持续上升,我该如何排查问题?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/30/fc/300f6947de14c0197145f2122c8ea7fc.mp3"></audio>
你好,我是刘超。
我想你肯定遇到过内存溢出,或是内存使用率过高的问题。碰到内存持续上升的情况,其实我们很难从业务日志中查看到具体的问题,那么面对多个进程以及大量业务线程,我们该如何精准地找到背后的原因呢?
## 常用的监控和诊断内存工具
工欲善其事必先利其器。平时排查内存性能瓶颈时我们往往需要用到一些Linux命令行或者JDK工具来辅助我们监测系统或者虚拟机内存的使用情况下面我就来介绍几种好用且常用的工具。
### Linux命令行工具之top命令
top命令是我们在Linux下最常用的命令之一它可以实时显示正在执行进程的CPU使用率、内存使用率以及系统负载等信息。其中上半部分显示的是系统的统计信息下半部分显示的是进程的使用率统计信息。
<img src="https://static001.geekbang.org/resource/image/36/49/3633095ed54d1ef22fc08310497d6b49.jpg" alt="">
除了简单的top之外我们还可以通过top -Hp pid查看具体线程使用系统资源情况
<img src="https://static001.geekbang.org/resource/image/1e/47/1e4429a9785ae4e6c0884655ee8b5747.jpg" alt="">
### Linux命令行工具之vmstat命令
vmstat是一款指定采样周期和次数的功能性监测工具我们可以看到它不仅可以统计内存的使用情况还可以观测到CPU的使用率、swap的使用情况。但vmstat一般很少用来查看内存的使用情况而是经常被用来观察进程的上下文切换。
<img src="https://static001.geekbang.org/resource/image/31/62/31a79622cdcadda4e9003b075378dc62.jpg" alt="">
- r等待运行的进程数
- b处于非中断睡眠状态的进程数
- swpd虚拟内存使用情况
- free空闲的内存
- buff用来作为缓冲的内存数
- si从磁盘交换到内存的交换页数量
- so从内存交换到磁盘的交换页数量
- bi发送到块设备的块数
- bo从块设备接收到的块数
- in每秒中断数
- cs每秒上下文切换次数
- us用户CPU使用时间
- sy内核CPU系统使用时间
- id空闲时间
- wa等待I/O时间
- st运行虚拟机窃取的时间。
### Linux命令行工具之pidstat命令
pidstat是Sysstat中的一个组件也是一款功能强大的性能监测工具我们可以通过命令yum install sysstat安装该监控组件。之前的top和vmstat两个命令都是监测进程的内存、CPU以及I/O使用情况而pidstat命令则是深入到线程级别。
通过pidstat -help命令我们可以查看到有以下几个常用的参数来监测线程的性能
<img src="https://static001.geekbang.org/resource/image/90/46/90d26ef49ad94510062ac3f36727a346.jpg" alt="">
常用参数:
- -u默认的参数显示各个进程的cpu使用情况
- -r显示各个进程的内存使用情况
- -d显示各个进程的I/O使用情况
- -w显示每个进程的上下文切换情况
- -p指定进程号
- -t显示进程中线程的统计信息。
我们可以通过相关命令例如ps或jps查询到相关进程ID再运行以下命令来监测该进程的内存使用情况
<img src="https://static001.geekbang.org/resource/image/18/61/184df3ee5ab0a920f506b3daa6250a61.jpg" alt="">
其中pidstat的参数-p用于指定进程ID-r表示监控内存的使用情况1表示每秒的意思3则表示采样次数。
其中显示的几个关键指标的含义是:
- Minflt/s任务每秒发生的次要错误不需要从磁盘中加载页
- Majflt/s任务每秒发生的主要错误需要从磁盘中加载页
- VSZ虚拟地址大小虚拟内存使用KB
- RSS常驻集合大小非交换区内存使用KB。
如果我们需要继续查看该进程下的线程内存使用率,则在后面添加-t指令即可
<img src="https://static001.geekbang.org/resource/image/3c/72/3c9072c659a91b5f83cbc1a112ddcc72.jpg" alt="">
我们知道Java是基于JVM上运行的大部分内存都是在JVM的用户内存中创建的所以除了通过以上Linux命令来监控整个服务器内存的使用情况之外我们更需要知道JVM中的内存使用情况。JDK中就自带了很多命令工具可以监测到JVM的内存分配以及使用情况。
### JDK工具之jstat命令
jstat可以监测Java应用程序的实时运行情况包括堆内存信息以及垃圾回收信息。我们可以运行jstat -help查看一些关键参数信息
<img src="https://static001.geekbang.org/resource/image/42/e8/42880a93eb63ae6854a7920e73a751e8.jpg" alt="">
再通过jstat -option查看jstat有哪些操作
<img src="https://static001.geekbang.org/resource/image/7a/7d/7af697d9cfd6002a49063ab2464d5f7d.jpg" alt="">
- -class显示ClassLoad的相关信息
- -compiler显示JIT编译的相关信息
- -gc显示和gc相关的堆信息
- -gccapacity显示各个代的容量以及使用情况
- -gcmetacapacity显示Metaspace的大小
- -gcnew显示新生代信息
- -gcnewcapacity显示新生代大小和使用情况
- -gcold显示老年代和永久代的信息
- -gcoldcapacity :显示老年代的大小;
- -gcutil显示垃圾收集信息
- -gccause显示垃圾回收的相关信息通-gcutil同时显示最后一次或当前正在发生的垃圾回收的诱因
- -printcompilation输出JIT编译的方法信息。
它的功能比较多在这里我例举一个常用功能如何使用jstat查看堆内存的使用情况。我们可以用jstat -gc pid查看
<img src="https://static001.geekbang.org/resource/image/e5/68/e59188982cf5b75243a8c333bfead068.jpg" alt="">
- S0C年轻代中To Survivor的容量单位KB
- S1C年轻代中From Survivor的容量单位KB
- S0U年轻代中To Survivor目前已使用空间单位KB
- S1U年轻代中From Survivor目前已使用空间单位KB
- EC年轻代中Eden的容量单位KB
- EU年轻代中Eden目前已使用空间单位KB
- OCOld代的容量单位KB
- OUOld代目前已使用空间单位KB
- MCMetaspace的容量单位KB
- MUMetaspace目前已使用空间单位KB
- YGC从应用程序启动到采样时年轻代中gc次数
- YGCT从应用程序启动到采样时年轻代中gc所用时间(s)
- FGC从应用程序启动到采样时old代全gcgc次数
- FGCT从应用程序启动到采样时old代全gcgc所用时间(s)
- GCT从应用程序启动到采样时gc用的总时间(s)。
### JDK工具之jstack命令
这个工具在模块三的[答疑课堂](https://time.geekbang.org/column/article/105234)中介绍过,它是一种线程堆栈分析工具,最常用的功能就是使用 jstack pid 命令查看线程的堆栈信息通常会结合top -Hp pid 或 pidstat -p pid -t一起查看具体线程的状态也经常用来排查一些死锁的异常。
<img src="https://static001.geekbang.org/resource/image/28/88/2869503e8d5460e36b3fd3e1a52a8888.jpg" alt="">
每个线程堆栈的信息中都可以查看到线程ID、线程的状态wait、sleep、running 等状态)以及是否持有锁等。
### JDK工具之jmap命令
在[第23讲](https://time.geekbang.org/column/article/108139)中我们使用过jmap查看堆内存初始化配置信息以及堆内存的使用情况。那么除了这个功能我们其实还可以使用jmap输出堆内存中的对象信息包括产生了哪些对象对象数量多少等。
我们可以用jmap来查看堆内存初始化配置信息以及堆内存的使用情况
<img src="https://static001.geekbang.org/resource/image/80/3f/808870b42f5f6525d79f70fd287a293f.jpg" alt="">
我们可以使用jmap -histo[:live] pid查看堆内存中的对象数目、大小统计直方图如果带上live则只统计活对象
<img src="https://static001.geekbang.org/resource/image/74/7b/74f42fa2b48ceaff869472f6061c1c7b.jpg" alt="">
我们可以通过jmap命令把堆内存的使用情况dump到文件中
<img src="https://static001.geekbang.org/resource/image/f3/17/f3c17fd9bb436599fb48cf151ee7ba17.jpg" alt="">
我们可以将文件下载下来,使用 [MAT](http://www.eclipse.org/mat/) 工具打开文件进行分析:
<img src="https://static001.geekbang.org/resource/image/3c/43/3cc14844625cebcc1cdb836e5ccbfc43.jpg" alt="">
下面我们用一个实战案例来综合使用下刚刚介绍的几种工具,具体操作一下如何分析一个内存泄漏问题。
## 实战演练
我们平时遇到的内存溢出问题一般分为两种,一种是由于大峰值下没有限流,瞬间创建大量对象而导致的内存溢出;另一种则是由于内存泄漏而导致的内存溢出。
使用限流我们一般就可以解决第一种内存溢出问题但其实很多时候内存溢出往往是内存泄漏导致的这种问题就是程序的BUG我们需要及时找到问题代码。
**下面我模拟了一个内存泄漏导致的内存溢出案例,我们来实践一下。**
我们知道ThreadLocal的作用是提供线程的私有变量这种变量可以在一个线程的整个生命周期中传递可以减少一个线程在多个函数或类中创建公共变量来传递信息避免了复杂度。但在使用时如果ThreadLocal使用不恰当就可能导致内存泄漏。
这个案例的场景就是ThreadLocal下面我们模拟对每个线程设置一个本地变量。运行以下代码系统一会儿就发送了内存溢出异常
```
@RequestMapping(value = &quot;/test0&quot;)
public String test0(HttpServletRequest request) {
ThreadLocal&lt;Byte[]&gt; localVariable = new ThreadLocal&lt;Byte[]&gt;();
localVariable.set(new Byte[4096*1024]);// 为线程添加变量
return &quot;success&quot;;
}
```
在启动应用程序之前我们可以通过HeapDumpOnOutOfMemoryError和HeapDumpPath这两个参数开启堆内存异常日志通过以下命令启动应用程序
```
java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:/tmp/heapTest.log heapTest-0.0.1-SNAPSHOT.jar
```
首先请求test0链接10000次这个时候我们请求test0的接口报异常了。
<img src="https://static001.geekbang.org/resource/image/60/dc/60ab8d7847a55a9bcf84d17ecd11ebdc.jpg" alt="">
通过日志我们很好分辨这是一个内存溢出异常。我们首先通过Linux系统命令查看进程在整个系统中内存的使用率是多少最简单就是top命令了。
<img src="https://static001.geekbang.org/resource/image/d2/37/d2ad570e1fff2a64a1924c2852f93e37.jpg" alt="">
从top命令查看进程的内存使用情况可以发现在机器只有8G内存且只分配了4G内存给Java进程的情况下Java进程内存使用率已经达到了55%再通过top -Hp pid查看具体线程占用系统资源情况。
<img src="https://static001.geekbang.org/resource/image/6f/a7/6fdea40b5ff4f2f0744e019c3bef79a7.jpg" alt="">
再通过jstack pid查看具体线程的堆栈信息可以发现该线程一直处于 TIMED_WAITING 状态此时CPU使用率和负载并没有出现异常我们可以排除死锁或I/O阻塞的异常问题了。
<img src="https://static001.geekbang.org/resource/image/4b/87/4bfb58d626f988260e016a2bdf0e8687.jpg" alt="">
我们再通过jmap查看堆内存的使用情况可以发现老年代的使用率几乎快占满了而且内存一直得不到释放
<img src="https://static001.geekbang.org/resource/image/fe/71/feb358259ea8b3ed2b67e868c101d271.jpg" alt="">
通过以上堆内存的情况,我们基本可以判断系统发生了内存泄漏。下面我们就需要找到具体是什么对象一直无法回收,什么原因导致了内存泄漏。
我们需要查看具体的堆内存对象看看是哪个对象占用了堆内存可以通过jmap查看存活对象的数量
<img src="https://static001.geekbang.org/resource/image/c5/d9/c5b89deb306a2c470e606fa9c49dd0d9.jpg" alt="">
Byte对象占用内存明显异常说明代码中Byte对象存在内存泄漏我们在启动时已经设置了dump文件通过MAT打开dump的内存日志文件我们可以发现MAT已经提示了byte内存异常
<img src="https://static001.geekbang.org/resource/image/4c/63/4ceb91714afa77b54d1112a0e1f0c863.jpg" alt="">
再点击进入到Histogram页面可以查看到对象数量排序我们可以看到Byte[]数组排在了第一位选中对象后右击选择with incomming reference功能可以查看到具体哪个对象引用了这个对象。
<img src="https://static001.geekbang.org/resource/image/5a/91/5a651a2f52dfed72712543f7680de091.jpg" alt="">
在这里我们就可以很明显地查看到是ThreadLocal这块的代码出现了问题。
<img src="https://static001.geekbang.org/resource/image/2b/a2/2bed3871097249d64ccf4c79d68109a2.jpg" alt="">
## 总结
在一些比较简单的业务场景下,排查系统性能问题相对来说简单,且容易找到具体原因。但在一些复杂的业务场景下,或是一些开源框架下的源码问题,相对来说就很难排查了,有时候通过工具只能猜测到可能是某些地方出现了问题,而实际排查则要结合源码做具体分析。
可以说没有捷径,排查线上的性能问题本身就不是一件很简单的事情,除了将今天介绍的这些工具融会贯通,还需要我们不断地去累积经验,真正做到性能调优。
## 思考题
除了以上我讲到的那些排查内存性能瓶颈的工具之外你知道要在代码中对JVM的内存进行监控常用的方法是什么
期待在留言区看到你的分享。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他一起讨论。

View File

@@ -0,0 +1,142 @@
<audio id="audio" title="26 | 答疑课堂:模块四热点问题解答" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/80/0cfb0354c0f56d7debb1b06a39fa2080.mp3"></audio>
你好,我是刘超。
本周我们结束了“JVM性能监测及调优”的学习这一期答疑课堂我精选了模块四中 11 位同学的留言,进行集中解答,希望也能对你有所帮助。另外,我想为坚持跟到现在的同学点个赞,期待我们能有更多的技术交流,共同成长。
## [第20讲](https://time.geekbang.org/column/article/106203)
<img src="https://static001.geekbang.org/resource/image/31/3a/31a205290c3b2391f115ee77f511a43a.jpeg" alt="">
很多同学都问到了类似“黑夜里的猫"问到的问题所以我来集中回复一下。JVM的内存模型只是一个规范方法区也是一个规范一个逻辑分区并不是一个物理空间我们这里说的字符串常量放在堆内存空间中是指实际的物理空间。
<img src="https://static001.geekbang.org/resource/image/2a/01/2ac5ee0c9a6fe67ce8f896be75d05f01.jpeg" alt="">
文灏的问题和上一个类似,一同回复一下。元空间是属于方法区的,方法区只是一个逻辑分区,而元空间是具体实现。所以类的元数据是存放在元空间,逻辑上属于方法区。
## [第21讲](https://time.geekbang.org/column/article/106953)
<img src="https://static001.geekbang.org/resource/image/f2/76/f2fa07e388f5a3dbe84bb12bfea5ee76.jpeg" alt="">
Liam同学目前Hotspot虚拟机暂时不支持栈上分配对象。W.LI同学的留言值得参考所以这里一同贴出来了。
<img src="https://static001.geekbang.org/resource/image/20/f2/20e59cb2df51bd171d41c81e074821f2.jpeg" alt="">
## [第22讲](https://time.geekbang.org/column/article/107396)
<img src="https://static001.geekbang.org/resource/image/09/25/09ada15236e8ceeef2558d6ab7505425.jpeg" alt="">
非常赞Region这块Jxin同学讲解得很到位。这里我再总结下CMS和G1的一些知识点。
CMS垃圾收集器是基于标记清除算法实现的目前主要用于老年代垃圾回收。CMS收集器的GC周期主要由7个阶段组成其中有两个阶段会发生stop-the-world其它阶段都是并发执行的。
<img src="https://static001.geekbang.org/resource/image/50/aa/500c2f0e112ced378fd49a09c61c5caa.jpg" alt="">
G1垃圾收集器是基于标记整理算法实现的是一个分代垃圾收集器既负责年轻代也负责老年代的垃圾回收。
跟之前各个分代使用连续的虚拟内存地址不一样G1使用了一种 Region 方式对堆内存进行了划分同样也分年轻代、老年代但每一代使用的是N个不连续的Region内存块每个Region占用一块连续的虚拟内存地址。
在G1中还有一种叫 Humongous 区域用于存储特别大的对象。G1内部做了一个优化一旦发现没有引用指向巨型对象则可直接在年轻代的YoungGC中被回收掉。
<img src="https://static001.geekbang.org/resource/image/f8/be/f832278afd5cdb94decd1f6826056dbe.jpg" alt="">
G1分为Young GC、Mix GC以及Full GC。
G1 Young GC主要是在Eden区进行当Eden区空间不足时则会触发一次Young GC。将Eden区数据移到Survivor空间时如果Survivor空间不足则会直接晋升到老年代。此时Survivor的数据也会晋升到老年代。Young GC的执行是并行的期间会发生STW。
当堆空间的占用率达到一定阈值后会触发G1 Mix GC阈值由命令参数-XX:InitiatingHeapOccupancyPercent设定默认值45Mix GC主要包括了四个阶段其中只有并发标记阶段不会发生STW其它阶段均会发生STW。
<img src="https://static001.geekbang.org/resource/image/b8/2f/b8090ff2c7ddf54fb5f6e3c19a36d32f.jpg" alt="">
G1和CMS主要的区别在于
- CMS主要集中在老年代的回收而G1集中在分代回收包括了年轻代的Young GC以及老年代的Mix GC
- G1使用了Region方式对堆内存进行了划分且基于标记整理算法实现整体减少了垃圾碎片的产生
- 在初始化标记阶段搜索可达对象使用到的Card Table其实现方式不一样。
这里我简单解释下Card Table在垃圾回收的时候都是从Root开始搜索这会先经过年轻代再到老年代也有可能老年代引用到年轻代对象如果发生Young GC除了从年轻代扫描根对象之外还需要再从老年代扫描根对象确认引用年轻代对象的情况。
**这种属于跨代处理,非常消耗性能。**为了避免在回收年轻代时跨代扫描整个老年代CMS和G1都用到了Card Table来记录这些引用关系。只是G1在Card Table的基础上引入了RSet每个Region初始化时都会初始化一个RSetRSet记录了其它Region中的对象引用本Region对象的关系。
除此之外CMS和G1在解决并发标记时漏标的方式也不一样CMS使用的是Incremental Update算法而G1使用的是SATB算法。
首先我们要了解在并发标记中G1和CMS都是基于三色标记算法来实现的
- 黑色:根对象,或者对象和对象中的子对象都被扫描;
- 灰色:对象本身被扫描,但还没扫描对象中的子对象;
- 白色:不可达对象。
基于这种标记有一个漏标的问题,也就是说,当一个白色标记对象,在垃圾回收被清理掉时,正好有一个对象引用了该白色标记对象,此时由于被回收掉了,就会出现对象丢失的问题。
为了避免上述问题CMS采用了Incremental Update算法只要在写屏障write barrier里发现一个白对象的引用被赋值到一个黑对象的字段里那就把这个白对象变成灰色的。而在G1中采用的是SATB算法该算法认为开始时所有能遍历到的对象都是需要标记的即认为都是活的。
G1具备Pause Prediction Model 即停顿预测模型。用户可以设定整个GC过程中期望的停顿时间用参数-XX:MaxGCPauseMillis可以指定一个G1收集过程的目标停顿时间默认值200ms。
G1会根据这个模型统计出来的历史数据来预测一次垃圾回收所需要的Region数量通过控制Region数来控制目标停顿时间的实现。
<img src="https://static001.geekbang.org/resource/image/91/b4/915e9793981a278112087f0c880b96b4.jpeg" alt="">
Liam提出的这两个问题都非常好。
不管什么GC都会发送stop-the-world区别是发生的时间长短。而这个时间跟垃圾收集器又有关系Serial、PartNew、Parallel Scavenge收集器无论是串行还是并行都会挂起用户线程而CMS和G1在并发标记时是不会挂起用户线程的但其它时候一样会挂起用户线程stop the world 的时间相对来说就小很多了。
Major Gc 在很多参考资料中是等价于 Full GC的我们也可以发现很多性能监测工具中只有Minor GC 和 Full GC。一般情况下一次Full GC将会对年轻代、老年代、元空间以及堆外内存进行垃圾回收。触发Full GC的原因有很多
- 当年轻代晋升到老年代的对象大小并比目前老年代剩余的空间大小还要大时会触发Full GC
- 当老年代的空间使用率超过某阈值时会触发Full GC
- 当元空间不足时JDK1.7永久代不足也会触发Full GC
- 当调用System.gc()也会安排一次Full GC。
<img src="https://static001.geekbang.org/resource/image/a8/24/a8a506a512922609669b4073d0dbc224.jpeg" alt="">
接下来解答 ninghtmare 的提问。我们可以通过 jstat -gc pid interval 查看每次GC之后具体每一个分区的内存使用率变化情况。我们可以通过JVM的设置参数来查看垃圾收集器的具体设置参数使用的方式有很多例如 jcmd pid VM.flags 就可以查看到相关的设置参数。
<img src="https://static001.geekbang.org/resource/image/26/2e/26d688a3af534fb00fe3b89d261e5c2e.jpg" alt="">
这里附上第22讲中我总结的各个设置参数对应的垃圾收集器图表。
<img src="https://static001.geekbang.org/resource/image/e2/56/e29c9ac3e53ffbc8a5648644a87d6256.jpeg" alt="">
## [第23讲](https://time.geekbang.org/column/article/108139)
<img src="https://static001.geekbang.org/resource/image/bb/76/bb92ec845c715f9d36a6ce48a0c7d276.jpeg" alt="">
我又不乱来同学的留言真是没有乱来,细节掌握得很好!
前提是老年代有足够接受这些对象的空间才会进行分配担保。如果老年代剩余空间小于每次Minor GC晋升到老年代的平均值则会发起一次 Full GC。
<img src="https://static001.geekbang.org/resource/image/28/20/2838514b87e62d69bf51d7a7f12a0c20.jpeg" alt="">
看到这里,我发现爱提问的同学始终爱提问,非常鼓励啊,技术是需要交流的,也欢迎你有任何疑问,随时留言给我,我会知无不尽。
现在回答W.LI同学的问题。这个会根据我们创建对象占用的内存使用率合理分配内存并不仅仅考虑对象晋升的问题还会综合考虑回收停顿时间等因素。针对某些特殊场景我们可以手动来调优配置。
## [第24讲](https://time.geekbang.org/column/article/108582)
<img src="https://static001.geekbang.org/resource/image/10/24/1080a8574a1a1ded35b736ccbec40524.jpeg" alt="">
下面解答Geek_75b4cd同学的问题。
我们知道ThreadLocal是基于ThreadLocalMap实现的这个Map的Entry继承了WeakReference而Entry对象中的key使用了WeakReference封装也就是说Entry中的key是一个弱引用类型而弱引用类型只能存活在下次GC之前。
如果一个线程调用ThreadLocal的set设置变量当前ThreadLocalMap则会新增一条记录但由于发生了一次垃圾回收此时的key值就会被回收而value值依然存在内存中由于当前线程一直存在所以value值将一直被引用。.
这些被垃圾回收掉的key就会一直存在一条引用链的关系Thread --&gt; ThreadLocalMap&gt;Entry&gt;Value。这条引用链会导致Entry不会被回收Value也不会被回收但Entry中的key却已经被回收的情况发生从而造成内存泄漏。
我们只需要在使用完该key值之后将value值通过remove方法remove掉就可以防止内存泄漏了。
<img src="https://static001.geekbang.org/resource/image/8d/9a/8da35d95d5b31e3f0a582dbd4d47fd9a.jpeg" alt="">
最后一个问题来自于WL同学。
内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。例如,我在[第03讲](https://time.geekbang.org/column/article/97215)中说到的Java6中substring方法就可能会导致内存泄漏。
当调用substring方法时会调用new string构造函数此时会复用原来字符串的char数组而如果我们仅仅是用substring获取一小段字符而在原本string字符串非常大的情况下substring的对象如果一直被引用由于substring里的char数组仍然指向原字符串此时string字符串也无法回收从而导致内存泄露。
内存溢出则是发生了OutOfMemoryException内存溢出的情况有很多例如堆内存空间不足栈空间不足还有方法区空间不足等都会导致内存溢出。
内存泄漏与内存溢出的关系:内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。
今天的答疑就到这里,如果你还有其它问题,请在留言区中提出,我会一一解答。最后欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他加入讨论。