[转帖]一文看尽 JVM GC 调优

一文,jvm,gc · 浏览次数 : 0

小编点评

**内存泄漏与内存溢出** **内存泄漏**是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存空间的浪费。例如,在Java6中substring方法可能会导致内存泄漏情况发生。 **内存溢出**则是发生了OutOfMemoryException,内存溢出的情况有很多,例如堆内存空间不足,栈空间不足,以及方法区空间不足都会发生内存溢出异常。 **内存泄漏与内存溢出之间的区别** * **内存泄漏**是指对象无法得到及时的回收,持续占用内存空间,而**内存溢出**则是内存空间不足,导致程序异常。 * **内存泄漏**通常是由于对象在使用过程中一直存在,而**内存溢出**则是由于程序在使用过程中分配了超过内存空间的对象。 * **内存泄漏**会导致内存空间被浪费,而**内存溢出**会导致程序异常。 **其他** * 内存泄漏容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。 * 常见的方法用于解决内存泄漏问题是设置了内存回收策略,例如使用weakref和remove方法。

正文

https://zhuanlan.zhihu.com/p/428731068

 

首先看一个著名的学习方法论

向橡皮鸭求助
学会提问,提问也是一门艺术
提问前,先投入自己的时间做好功课
发生了什么事情
问题的基本情况
你投入的研究和发现
能正确提出你的问题,你的问题差不多已经解决一半
深入的思考你的问题,大多情况下,你已经找到答案

费曼学习方法论

费曼学习方法指出,想象你要将自己学习的内容,教授给一个完全不了解这个知识点的人,教授的内容呢,需要讲解得简单易懂,且这个过程中会不断有问题被提出,你需要重新去认识这些知识点。

CMS 最重要的是合理地设置年轻代和年老代的大小

jvm参数(调优)+jvm内存的年轻代/老年代/持久代

而对于 JVM 调优来说,主要是 JVM 垃圾收集的优化,

一般来说是因为有问题才需要优化,所以对于 JVM GC 来说

GC 优化时间点

如果你观察到 某个服务或者说是 Tomcat 进程的 CPU 使用率比较高
并且在 GC 日志中发现 GC 次数比较频繁、GC 停顿时间长,
这表明你需要对 GC 进行优化了。

从 Java 9 开始,采用 G1 作为默认垃圾收集器,而 G1 的目标也是逐步取代 CMS。

CMS vs G1
CMS 收集器将 Java 堆分为年轻代(Young)或年老代(Old)。这主要是因为有研究表
明,超过 90%的对象在第一次 GC 时就被回收掉,但是少数对象往往会存活较长的时间。

常见思考题:CMS 年轻代 和 老年代 的 内存划分比例?

新生代 : 老年代 = 1 : 2

Eden : from : to = 8 :1 : 1

 

为什么是这样子分配呢??

根据业务。
是 例如高并发 就尽可能把年轻代给大点。
因为并发的对象 大都是朝生夕死的,年轻代给大点 可能防止 突然的高并发把年轻代占满,使这些生命短暂的对象进到老年代,造成频繁full gc.

比如秒杀接口(加入这个接口运行时间是1s) 瞬间5000qps 代表1s会创建5000个订单对象(数字乱写的,一个接口会创建很多对象),这些订单对象 秒杀接口跑完就该回收的。。。加入年轻代很小 这个5000个对象一来 直接放不下 就只能去老年代了,然后下一秒秒杀接口跑完 这些对象其实就已经可以回收了。。。这就很难受了。

CMS 还将年轻代内存空间分为幸存者空间(Survivor)和伊甸园空间(Eden)。新的对象
始终在 Eden 空间上创建。一旦一个对象在一次垃圾收集后还幸存,就会被移动到幸存者空间。当一个对象在多次垃圾收集之后还存活时,它会移动到年老代。这样做的目的是在年轻代和年老代采用不同的收集算法,以达到较高的收集效率,

比如在年轻代采用复制 - 整理算法,

在年老代采用标记 - 清理算法。

因此 CMS 将 Java 堆分成如下区域:

 

G1 收集器有两大特点:

图上的 U 表示“未分配”区域。G1 将堆拆分成小的区域,一个最大的好处是可以做局部区
域的垃圾回收,而不需要每次都回收整个区域比如年轻代和年老代,这样回收的停顿时间会比较短。具体的收集过程是:

G1 可以并发完成大部分 GC 的工作,这期间不会“Stop-The-World”。

G1 使用非连续空间,这使 G1 能够有效地处理非常大的堆。此外,G1 可以同时收集年
轻代和年老代。G1 并没有将 Java 堆分成三个空间(Eden、Survivor 和 Old),而是将
堆分成许多(通常是几百个)非常小的区域。这些区域是固定大小的(默认情况下大约为
2MB)。每个区域都分配给一个空间。 G1 收集器的 Java 堆如下图所示:


图上的 U 表示“未分配”区域。G1 将堆拆分成小的区域,一个最大的好处是可以做局部区
域的垃圾回收,而不需要每次都回收整个区域比如年轻代和年老代,这样回收的停顿时间会比较短。具体的收集过程是:将所有存活的对象将从收集的区域复制到未分配的区域,比如收集的区域是 Eden 空间,把 Eden 中的存活对象复制到未分配区域,这个未分配区域就成了 Survivor 空间。理想情况下,如果一个区域全是垃圾(意味着一个存活的对象都没有),则可以直接将该区域声明为“未分配”。为了优化收集时间,G1 总是优先选择垃圾最多的区域,从而最大限度地减少后续分配和释放堆空间所需的工作量。这也是 G1 收集器名字的由来——Garbage-First。

之前也写过一篇调优的经历:

线上JVM调优,FullGC几十次/天到10天一次,我是怎么优化的

GC 调优原则

GC 是有代价的,因此我们调优的根本原则是每一次 GC 都回收尽可能多的对象,也就是减少无用功。因此我们在做具体调优的时候,针对 CMS 和 G1 两种垃圾收集器,分别有一些相应的策略。

CMS 收集器

 

 

对于 CMS 收集器来说,最重要的是合理地设置年轻代和年老代的大小。

年轻代太小的话,会导致频繁的 Minor GC,并且很有可能存活期短的对象也不能被回收,GC 的效率就不高。而年老代太小的话,容纳不下从年轻代过来的新对象,会频繁触发单线程 Full GC,导致较长时间的 GC 暂停,影响 Web 应用的响应时间。

G1 收集器(重点 java9默认)

不推荐直接设置年轻代的大小,这一点跟 CMS 收集器不一样,这
是因为 G1 收集器会根据算法动态决定年轻代和年老代的大小。
因此对于 G1 收集器,

1 我们需要关心的是 Java 堆的总大小(-Xmx)
2 G1 还有一个较关键的参数是-XX:MaxGCPauseMillis = n

此外 G1 还有一个较关键的参数是-XX:MaxGCPauseMillis = n,这个参数是用来限制
最大的 GC 暂停时间,目的是尽量不影响请求处理的响应时间。G1 将根据先前收集的信息以及检测到的垃圾量,估计它可以立即收集的最大区域数量,从而尽量保证 GC 时间不会超出这个限制。因此 G1 相对来说更加“智能”,使用起来更加简单。

GCViewer

CMS调优总结–合理设置年轻代和年老代的大小

对于 CMS 来说,我们要合理设置年轻代和年老代的大小。
你可能会问该如何确定它们的大小呢?这是一个迭代的过程,可以先采用 JVM 的默认值,然后通过压测分析 GC 日志。

总结:

1 如果我们看年轻代的内存使用率处在高位,导致频繁的 Minor GC,而频繁 GC 的效率又不高,说明对象没那么快能被回收,这时年轻代可以适当调大一点。


2 如果我们看年老代的内存使用率处在高位,导致频繁的 Full GC, 这样分两种情况:

如果每次 Full GC 后年老代的内存占用率没有下来,可以怀疑是内存泄漏;
如果 Full GC 后年老代的内存占用率下来了,说明不是内存泄漏,我们要考虑调大年老代。

对于 G1 收集器来说

我们可以适当调大 Java 堆,因为 G1 收集器采用了局部区域收集策
略,单次垃圾收集的时间可控,可以管理较大的 Java 堆。

思考问题:如果把年轻代和年老代都设置得很大,会有什么问题?

回复: 对的,主要是会引起gc停顿时间过长
设置过大,回收频率会降低,导致单次回收时间过长,因为需要回收的对象更多,导致GC
stop the world时间过长,卡顿明显,导致请求无法及时处理。

年轻代设置过大:
1.生命周期长的对象会长时间停留在年轻代,在S0和S1来回复制,增加复制开销。
2.年轻代太大会增加YGC每次停顿的时间,不过通过根节点遍历,OopMap,old scan等
优化手段这一部分的开销其实比较少。
3.浪费内存。内存也是钱啊虽然现在租的很便宜

内存溢出场景及方案

java.lang.OutOfMemoryError: Java heap space;

JVM 无法在堆中分配对象时,会抛出这个异常,导致这个异常的原因可能有三种:

  1. 内存泄漏。Java 应用程序一直持有 Java 对象的引用,导致对象无法被 GC 回收,比如
    对象池和内存池中的对象无法被 GC 回收。
  2. 配置问题。有可能是我们通过 JVM 参数指定的堆大小(或者未指定的默认大小),对于应用程序来说是不够的。解决办法是通过 JVM 参数加大堆的大小。

3.finalize 方法的过度使用。如果我们想在 Java 类实例被 GC 之前执行一些逻辑,比如清
理对象持有的资源,可以在 Java 类中定义 finalize 方法,这样 JVM GC 不会立即回收这些对象实例,而是将对象实例添加到一个叫“java.lang.ref.Finalizer.ReferenceQueue”的
队列中,执行对象的 finalize 方法,之后才会回收这些对象。Finalizer 线程会和主线程竞
争 CPU 资源,但由于优先级低,所以处理速度跟不上主线程创建对象的速度,因此
ReferenceQueue 队列中的对象就越来越多,最终会抛出 OutOfMemoryError。解决办法
是尽量不要给 Java 类定义 finalize 方法。

java.lang.OutOfMemoryError: GC overhead limit exceeded

出现这种 OutOfMemoryError 的原因是,垃圾收集器一直在运行,但是 GC 效率很低,比
如 Java 进程花费超过 98%的 CPU 时间来进行一次 GC,但是回收的内存少于 2%的 JVM
堆,并且连续 5 次 GC 都是这种情况,就会抛出 OutOfMemoryError。

解决办法是查看 GC 日志或者生成 Heap Dump,确认一下是不是内存泄漏,如果不是内
存泄漏可以考虑增加 Java 堆的大小。当然你还可以通过参数配置来告诉 JVM 无论如何也
不要抛出这个异常,方法是配置-XX:-UseGCOverheadLimit,但是我并不推荐这么做,
因为这只是延迟了 OutOfMemoryError 的出现。

java.lang.OutOfMemoryError: Requested array size exceeds VM limit。

从错误消息我们也能猜到,抛出这种异常的原因是“请求的数组大小超过 JVM 限制”,应
用程序尝试分配一个超大的数组。比如应用程序尝试分配 512MB 的数组,但最大堆大小为256MB,则将抛出 OutOfMemoryError,并且请求的数组大小超过 VM 限制。

通常这也是一个配置问题(JVM 堆太小),或者是应用程序的一个 Bug,比如程序错误地
计算了数组的大小,导致尝试创建一个大小为 1GB 的数组。

java.lang.OutOfMemoryError: MetaSpace

如果 JVM 的元空间用尽,则会抛出这个异常。我们知道 JVM 元空间的内存在本地内存中
分配,但是它的大小受参数 MaxMetaSpaceSize 的限制。当元空间大小超过
MaxMetaSpaceSize 时,JVM 将抛出带有 MetaSpace 字样的 OutOfMemoryError。解
决办法是加大 MaxMetaSpaceSize 参数的值。

java.lang.OutOfMemoryError: Request size bytes for reason. Out of swap space

当本地堆内存分配失败或者本地内存快要耗尽时,Java HotSpot VM 代码会抛出这个异
常,VM 会触发“致命错误处理机制”,它会生成“致命错误”日志文件,其中包含崩溃时
线程、进程和操作系统的有用信息。如果碰到此类型的 OutOfMemoryError,你需要根据
JVM 抛出的错误信息来进行诊断;或者使用操作系统提供的 DTrace 工具来跟踪系统调
用,看看是什么样的程序代码在不断地分配本地内存

java.lang.OutOfMemoryError: Unable to create native threads抛出这个异常的过程大概是这样的:

1.Java 程序向 JVM 请求创建一个新的 Java 线程。
2.JVM 本地代码(Native Code)代理该请求,通过调用操作系统 API 去创建一个操作系
统级别的线程 Native Thread。
3. 操作系统尝试创建一个新的 Native Thread,需要同时分配一些内存给该线程,每一个
Native Thread 都有一个线程栈,线程栈的大小由 JVM 参数-Xss决定。
4. 由于各种原因,操作系统创建新的线程可能会失败,下面会详细谈到。
5.JVM 抛出“java.lang.OutOfMemoryError: Unable to create new native thread”错
误。
因此关键在于第四步线程创建失败,JVM 就会抛出 OutOfMemoryError,那具体有哪些因
素会导致线程创建失败呢?

  1. 内存大小限制:我前面提到,Java 创建一个线程需要消耗一定的栈空间,并通过-Xss参
    数指定。请你注意的是栈空间如果过小,可能会导致 StackOverflowError,尤其是在递归
    调用的情况下;但是栈空间过大会占用过多内存,而对于一个 32 位 Java 应用来说,用户
    进程空间是 4GB,内核占用 1GB,那么用户空间就剩下 3GB,因此它能创建的线程数大致可以通过这个公式算出来:

不过对于 64 位的应用,由于虚拟进程空间近乎无限大,因此不会因为线程栈过大而耗尽虚拟地址空间。但是请你注意,64 位的 Java 进程能分配的最大内存数仍然受物理内存大小的限制。

其中的“max user processes”就是一个进程能创建的最大线程数,我们可以修改这个参
数:

对于线程创建失败的 OutOfMemoryError,除了调整各种参数,我们还需要从程序本身找
找原因,看看是否真的需要这么多线程,有可能是程序的 Bug 导致创建过多的线程。

实际工作中要根据具体的错误信息去分析背后的原因,尤其是 Java 堆内存不够时,需要生成 Heap Dump 来分析,看是不是内存泄漏;排除内存泄漏之后,我们再调整各种 JVM 参数,否则根本的问题原因没有解决的话,调整 JVM 参数也无济于事。

感悟:
之前在使用es的时候想用线程池来优化频繁获取连接造成的资源浪费,但因为自己粗心,
使用的过程中错误的操作获取连接都去new线程池,而不是从线程池获取线程,导致内存
老是到顶,那次内存泄露还是自己的基本功不扎实导致的,最后也是通过一些jvm工具找到
了问题,当时花了不少时间在上面,挺感慨的。

CPU 的问题

CPU 资源经常会成为系统性能的一个瓶颈,这其中的原因是多方面的,可能是内存泄露导致频繁 GC,进而引起 CPU 使用率过高;又可能是代码中的 Bug创建了大量的线程,导致 CPU 上下文切换开销。

“Java 进程 CPU 使用率高”的解决思路是什么?

对于 CPU 的问题,最重要的是要找到是哪些线程在消耗 CPU,通过线程栈定位到问题代
码;如果没有找到个别线程的 CPU 使用率特别高,我们要怀疑到是不是线程上下文切换导致了 CPU 使用率过高。

线程 有一个独有的 栈空间。要注意

-Xss*** 操作刺痛默认值 8192KB == 8 MB 挺大的

  1. 使用 top 命令,我们看到 Java 进程的 CPU 使用率达到了 262.3%,注意到进程 ID 是4361
  2. 接着我们用更精细化的 top 命令查看这个 Java 进程中各线程使用 CPU 的情况:
  3. #top -H -p 4361
  4. 为了找出线程在做什么事情,我们需要用 jstack 命令生成线程快照,具体方法是:
jstack 4361
jstack 4361 > 4361.log
当我们遇到 CPU 过高的问题时,首先要定位是哪个进程的导致的,之后可以通过top -H
-p pid命令定位到具体的线程。其次还要通 jstack 查看线程的状态,看看线程的个数或者
线程的状态,如果线程数过多,可以怀疑是线程上下文切换的开销,我们可以通过 vmstat
和 pidstat 这两个工具进行确认

思考问题:哪些情况可能导致程序中的线程数失控,产生大量线程呢?

1.使用了Java的newCachedThreadPool,因为最大线程数是int最大值
2.自定义线程池最大线程数设置不合理
3.线程池的拒绝策略,选择了如果队列满了并且线程达到最大线程数后,提交的任务交给
提交任务线程处理。

请你讲解下 JVM 的内存模型?

我们先通过一张 JVM 内存模型图,来熟悉下其具体设计。在 Java 中,JVM 内存模型主要
分为堆、程序计数器、方法区、虚拟机栈和本地方法栈。

 

 

  1. 堆(Heap)
    堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被
    分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和
    Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成。
    在 Java6 版本中,永久代在非堆内存区;到了 Java7 版本,永久代的静态变量和运行时常
    量池被合并到了堆中;而到了 Java8,永久代被元空间取代了。 结构如下图所示:

 

  1. 程序计数器(Program Counter Register)

程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分
支、循环、跳转、异常、线程恢复等都依赖于计数器。

由于 Java 是多线程语言,当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮
询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU
资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的
指令。

  1. 方法区(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 总数、常量池的大小和方法的大小等.

  1. 虚拟机栈(VM stack)
    Java 虚拟机栈是线程私有的内存空间,它和 Java 线程一起创建。当创建一个线程时,会在虚拟机栈中申请一个线程栈,用来保存方法的局部变量、操作数栈、动态链接方法和返回地址等信息,并参与方法的调用和返回。每一个方法的调用都伴随着栈帧的入栈操作,方法的返回则是栈帧的出栈操作。
  2. 本地方法栈(Native Method Stack)
    本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本
    地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实
    现的.

那如果有一个类中定义了 String a="b"和 String c = new String(“b”),请问这两个对象会分别创建在 JVM 内存模型中的哪块区域呢?

String a="b"应该会放在字符串常量池中。
String c= new String(“b”) 首先应该放在 堆中一份,再在常量池中放一份。但是常量池中
有b了.

String 对象

在特定的场景使用 String.intern 可以很大程度地节约内存成本。我们可以使用不同的引用类型,改变一个对象的正常生命周期,从而提高 JVM 的回收效率,这也是 JVM 性能调优的一种方式。

面对不同的业务场景,垃圾回收的调优策略也不一样。例如,在对内存要求苛刻的情况下,
需要提高对象的回收效率;在 CPU 使用率高的情况下,需要降低高并发时垃圾回收的频

看看回收(后面简称 GC)的算法有哪些???

体现GC 算法好坏的指标有哪些????

如何根据自己的业务场景对 GC 策略进行调优???

垃圾回收机制

  1. 回收发生在哪里?

JVM 的内存区域中,程序计数器、虚拟机栈和本地方法栈这 3 个区域是线程私有的,随着
线程的创建而创建,销毁而销毁;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作,
每个栈帧中分配多少内存基本是在类结构确定下来的时候就已知的,因此这三个区域的内存
分配和回收都具有确定性。

那么垃圾回收的重点就是关注堆和方法区中的内存了,

堆中的回收主要是对象的回收,

方法区的回收主要是废弃常量和无用的类的回收。

  1. 对象在什么时候可以被回收?
    那 JVM 又是怎样判断一个对象是可以被回收的呢?
    一般一个对象不再被引用,就代表该对象可以被回收。
    目前有以下两种算法可以判断该对象是否可以被回收

 


引用计数算法:这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每当对
象被引用,引用计数器就会加 1;每当引用失效,计数器就会减 1。当对象的引用计数器的
值为 0 时,就说明该对象不再被引用,可以被回收了。这里强调一点,虽然引用计数算法
的实现简单,判断效率也很高,但它存在着对象之间相互循环引用的问题。

可达性分析算法:GC Roots 是该算法的基础,GC Roots 是所有对象的根对象,在 JVM
加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收
时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连
时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法

  1. 如何回收这些对象?

自动性:Java 提供了一个系统级的线程来跟踪每一块分配出去的内存空间,当 JVM 处于空
闲循环时,垃圾收集器线程会自动检查每一块分配出去的内存空间,然后自动回收每一块空
闲的内存块。

不可预期性:一旦一个对象没有被引用了,该对象是否立刻被回收呢?答案是不可预期的。
我们很难确定一个没有被引用的对象是不是会被立刻回收掉,因为有可能当程序结束后,这
个对象仍在内存中。

垃圾回收线程在 JVM 中是自动执行的,Java 程序无法强制执行。我们唯一能做的就是通过
调用 System.gc 方法来"建议"执行垃圾收集器,但是否可执行,什么时候执行?仍然不可
预期。

 


如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现,JDK1.7
update14 之后 Hotspot 虚拟机所有的回收器整理如下(以下为服务端垃圾收集器)

 

GC 调优策略

1. 降低 Minor GC 频率

通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此
我们可以通过增大新生代空间来降低 Minor GC 的频率。

可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次
Minor GC 的时间吗?如果单次 Minor GC 的时间增加,那也很难达到我们期待的优化效
果呀。

我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对
象)。假设一个对象在 Eden 区的存活时间为 500ms,Minor 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 区的大
小。

因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大 小。

结论:GC 后存活对象的数量多的话,不应该增加年轻代空间。

2. 降低 Full GC 的频率

通常情况下,由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full GC 会
带来上下文切换,增加系统的性能开销。

我们可以使用哪些方法来降低 Full GC 的频率呢?

减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于
web 端显示。例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象
如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻
代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多
的 Full GC。

我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅
助查看,再通过第二次查询显示剩余的字段。

增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆
内存,也可以降低 Full GC 的频率。

3 选择合适的 GC 回收器。

假设我们有这样一个需求,要求每次操作的响应时间必须在 500ms 以内。这个时候我们一
般会选择响应速度较快的 GC 回收器,CMS(Concurrent Mark Sweep)回收器和 G1 回
收器都是不错的选择。而当我们的需求对系统吞吐量有要求时,就可以选择 Parallel Scavenge 回收器来提高系统的吞吐量。

垃圾收集器的种类很多,我们可以将其分成两种类型,一种是响应速度快,一种是吞吐量
高。通常情况下,CMS 和 G1 回收器的响应速度快,Parallel Scavenge 回收器的吞吐量
高。

在 JDK1.8 环境下,默认使用的是 Parallel Scavenge(年轻代)+Serial Old(老年代)垃
圾收集器,你可以通过文中介绍的查询 JVM 的 GC 默认配置方法进行查看。

通常情况,JVM 是默认垃圾回收优化的,在没有性能衡量标准的前提下,尽量避免修改
GC 的一些性能配置参数。如果一定要改,那就必须基于大量的测试结果或线上的具体性能
来进行调整。

GC 性能衡量指标

吞吐量:这里的吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照
这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运
行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于
95%

停顿时间:指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时
间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时
间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。

垃圾回收频率:多久发生一次垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空
间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加
回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

minor gc是否会导致stop the world?

不管什么GC,都会发送stop the world,区别是发生的时间长短。而这个时间跟垃圾收集器
又有关系,Serial、PartNew、Parallel Scavenge收集器无论是串行还是并行,都会挂起用户线
程,而CMS和G1在并发标记时,是不会挂起用户线程,但其他时候一样会挂起用户线程,stop
the world的时间相对来说小很多了。

major gc什么时候会发生,它和full gc的区别是什么?

major gc很多参考资料指的是等价于full gc,我们也可以发现很多性能监测工具中只有minor
gc和full gc。
一般情况下,一次full gc将会对年轻代、老年代以及元空间、堆外内存进行垃圾回收。
而触发FullGC的原因有很多:
a、当年轻代晋升到老年代的对象大小比目前老年代剩余的空间大小还要大时,此时会触发Full
GC;
b、当老年代的空间使用率超过某阈值时,此时会触发Full GC;
c、当元空间不足时(JDK1.7永久代不足),也会触发Full GC;
d、当调用System.gc()也会安排一次Full GC;

回复: serial old是标记清除算法,parallel old是标记整理算法。

怀疑人生:JVM 的性能调优做过吗??

JVM 内存分配性能问题

谈到 JVM 内存表现出的性能问题时,你可能会想到一些线上的 JVM 内存溢出事故。但这
方面的事故往往是应用程序创建对象导致的内存回收对象难,一般属于代码编程问题

JVM 内存分配不合理最直接的表现就是频繁的 GC,这会导致上下文切换等性能问题,从
而降低系统的吞吐量、增加系统的响应时间.因此,如果你在线上环境或性能测试时,发现
频繁的 GC,且是正常的对象创建和回收,这个时候就需要考虑调整 JVM 内存分配了,从
而减少 GC 所带来的性能开销。

对象在堆中的生存周期

在 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 根据默认值来配置当前内存大
小。我们可以通过以下命令来查看堆内存配置的默认值:

java -XX:+PrintFlagsFinal -version | grep HeapSize

在 JDK1.7 中,默认情况下年轻代和老年代的比例是 1:2,我们可以通过–XX:NewRatio 重
置该配置项。年轻代中的 Eden 和 To Survivor、From Survivor 的比例是 8:1:1,我们可
以通过 -XX:SurvivorRatio 重置该配置项.

java 1.8 UseAdaptiveSizePolicy

还有,在 JDK1.8 中,不要随便关闭 UseAdaptiveSizePolicy 配置项,除非你已经对初始
化堆内存 / 最大堆内存、年轻代 / 老年代以及 Eden 区 /Survivor 区有非常明确的规划了。
否则 JVM 将会分配最小堆内存,年轻代和老年代按照默认比例 1:2 进行分配,年轻代中的
Eden 和 Survivor 则按照默认比例 8:2 进行分配。这个内存分配未必是应用服务的最佳配
置,因此可能会给应用服务带来严重的性能问题。

GC 频率:高频的 FullGC 会给系统带来非常大的性能消耗,虽然 MinorGC 相对 FullGC 来
说好了许多,但过多的 MinorGC 仍会给系统带来压力。

内存:这里的内存指的是堆内存大小,堆内存又分为年轻代内存和老年代内存。首先我们要
分析堆内存大小是否合适,其实是分析年轻代和老年代的比例是否合适。如果内存不足或分
配不均匀,会增加 FullGC,严重的将导致 CPU 持续爆满,影响系统性能。

吞吐量:频繁的 FullGC 将会引起线程的上下文切换,增加系统的性能开销,从而影响每次
处理的线程请求,最终导致系统的吞吐量下降。

延时:JVM 的 GC 持续时间也会影响到每次请求的响应时间。

具体调优方法:

调整堆内存空间减少 FullGC:通过日志分析,堆内存基本被用完了,而且存在大量
FullGC,这意味着我们的堆内存严重不足,这个时候我们需要调大堆内存空间

-Xms:堆初始大小;
-Xmx:堆最大值。

我们还可以将年轻代设置得大一些,从而减少一些MinorGC。

设置 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 的内存分配性能。

JVM 内存调优通常和 GC 调优是互补的,基于以上调优,我们可以继续对年轻代和堆内存
的垃圾回收算法进行调优。一些 JVM 内存分配调优的常用方法,但我还是建议你在进行性能压测后如果没有发现突出的性能瓶颈,就继续使用 JVM 默认参数,起码在大部分的场景下,默认配置已经可以满足我们的需求了。

盲目增大堆内存可能会让吞吐量不增反减,堆内存大了,每次gc扫描对象也就越多也越需
要花费时间,反而会适得其反。

回复: 对的。合理设置堆内存大小,根据实际业务调整,不宜过大,也不宜过小。

思考题

以上我们都是基于堆内存分配来优化系统性能的,但在 NIO 的 Socket 通信中,其实还使
用到了堆外内存来减少内存拷贝,实现 Socket 通信优化。

你知道堆外内存是如何创建和回 收的吗?

回复: 对的,可以手动回收掉,如果不手动回收,则会通过FullGC来回收。

内存持续上升,该如何排查问题?

遇到过内存溢出,或是内存使用率过高的问题。碰到内存持续上升的情况,其实
我们很难从业务日志中查看到具体的问题,那么面对多个进程以及大量业务线程,

我们该如何精准地找到背后的原因呢?

Linux 命令行工具之 top 命令

它可以实时显示正在执行进程的 CPU 使用率、内存使用率以及系统负载等信息。
其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使用率统计信息。

除了简单的 top 之外,我们还可以通过 top -Hp pid 查看具体线程使用系统资源情况:

Linux 命令行工具之 vmstat 命令

vmstat 是一款指定采样周期和次数的功能性监测工具,我们可以看到,它不仅可以统计内
存的使用情况,还可以观测到 CPU 的使用率、swap 的使用情况。但 vmstat 一般很少用
来查看内存的使用情况,而是经常被用来观察进程的上下文切换。

r:等待运行的进程数;
b:处于非中断睡眠状态的进程数;
swpd:虚拟内存使用情况;
free:空闲的内存;
buff:用来作为缓冲的内存数;
si:从磁盘交换到内存的交换页数量;
so:从内存交换到磁盘的交换页数量;
bi:发送到块设备的块数;
bo:从块设备接收到的块数;
in:每秒中断数;
cs:每秒上下文切换次数;
us:用户 CPU 使用时间;
sy:内核 CPU 系统使用时间;
id:空闲时间;
wa:等待 I/O 时间;
st:运行虚拟机窃取的时间

JDK 工具之 jstat 命令

jstat 可以监测 Java 应用程序的实时运行情况,包括堆内存信息以及垃圾回收信息。我们可
以运行 jstat -help 查看一些关键参数信息:

JDK 工具之 jstack 命令

它是一种线程堆栈分析工具,最常用的功能就是使用 jstack pid 命令查看线程的堆栈信息,通常会结合 top -Hp pid 或 pidstat -p pid -t 一起查看具体线程的状态,也经常用来排查一些死锁的异常。

每个线程堆栈的信息中,都可以查看到线程 ID、线程的状态(wait、sleep、running 等状态)以及是否持有锁等。

内存溢出问题

我们平时遇到的内存溢出问题一般分为两种,
一种是由于大峰值下没有限流,瞬间创建大量对象而导致的内存溢出;
另一种则是由于内存泄漏而导致的内存溢出。

使用限流,我们一般就可以解决第一种内存溢出问题,

但其实很多时候,内存溢出往往是内存泄漏导致的,这种问题就是程序的 BUG,我们需要及时找到问题代码

ThreadLocal 不恰当使用造成的内存泄漏

我们知道,ThreadLocal 的作用是提供线程的私有变量,这种变量可以在一个线程的整个
生命周期中传递,可以减少一个线程在多个函数或类中创建公共变量来传递信息,避免了复
杂度。但在使用时,如果 ThreadLocal 使用不恰当,就可能导致内存泄漏

我们知道,ThreadLocal 的作用是提供线程的私有变量,这种变量可以在一个线程的整个
生命周期中传递,可以减少一个线程在多个函数或类中创建公共变量来传递信息,避免了复
杂度。但在使用时,如果 ThreadLocal 使用不恰当,就可能导致内存泄漏。

第一步:我们首先通过 Linux 系统命令查看进程
在整个系统中内存的使用率是多少,最简单就是 top 命令了

第二步:再通过 top -Hp pid 查看具体线程占用系统资源情况

第三步:再通过 jstack pid 查看具体线程的堆栈信息

第四步:jmap 查看堆内存的使用情况

第五步:我们需要查看具体的堆内存对象,看看是哪个对象占用了堆内存,可以通过 jstat 查看存活
对象的数量

第六步:设置了 dump 文件,通过 MAT 打开 dump 的内存日志文件,找到 再点击进入到 Histogram 页面,可以查看到对象数量排序 。选中对象后右击选择 with incomming reference 功能,可以查看到具体哪个
对象引用了这个对象。

总结

在一些比较简单的业务场景下,排查系统性能问题相对来说简单,且容易找到具体原因。但
在一些复杂的业务场景下,或是一些开源框架下的源码问题,相对来说就很难排查了,有时
候通过工具只能猜测到可能是某些地方出现了问题,而实际排查则要结合源码做具体分析。
可以说没有捷径,排查线上的性能问题本身就不是一件很简单的事情,除了将今天介绍的这
些工具融会贯通,还需要我们不断地去累积经验,真正做到性能调优。

如何避免threadLocal内存泄漏呢???

回复: 我们知道,ThreadLocal是基于ThreadLocalMap实现的,这个Map的Entry继承了
WeakReference,而Entry对象中的key使用了WeakReference封装,也就是说Entry中的key是
一个弱引用类型,而弱引用类型只能存活在下次GC之前。

如果一个线程调用ThreadLocal的set设置变量,当前ThreadLocalMap则新增一条记录,但发生
一次垃圾回收,此时key值被回收,而value值依然存在内存中,由于当前线程一直存在,所以
value值将一直被引用。.

这些被垃圾回收掉的key就存在一条引用链的关系一直存在:Thread --> ThreadLocalMap–Entry–>Value,这条引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

我们只需要在使用完该key值之后,通过remove方法remove掉,就可以防止内存泄漏了

放两篇网友在工作中排查JVM问题的一篇文章

JVM调优实战总结!

内存泄露 和 内存溢出 具体有啥区别?

回复: 内存泄漏是指不再使用的对象无法得到及时的回收,持续占用内存空间,从而造成内存
空间的浪费。
例如,在Java6中substring方法可能会导致内存泄漏情况发生。当调用substring方法时会调用new string构造函数,此时会复用原来字符串的char数组,而如果我们仅仅是用substring获取一小段字符,而原本string字符串非常大的情况下,substring的对象如果一直被引用,由于substring的里面的char数组仍然指向原字符串,此时string字符串也无法回收,从而导致内存泄露。

内存溢出则是发生了OutOfMemoryException,内存溢出的情况有很多,例如堆内存空间不足,
栈空间不足,以及方法区空间不足都会发生内存溢出异常。

内存泄漏与内存溢出的关系

内存泄漏很容易导致内存溢出,但内存溢出不一定是内存泄漏导致的。

内存泄露导致有大量对象无法回收,占满了堆内存情况下,就会导致内存溢出。

cpu占用过高排查思路

  1. top 查看占用cpu的进程 pid
  2. top -Hp pid 查看进程中占用cpu过高的线程id tid
  3. printf ‘%x/n’ tid 转化为十六进制
  4. jstack pid |grep tid的十六进制 -A 30 查看堆栈信息定位
  5. jvm old区占用过高排查思路
  6. top查看占用cpu高的进程
  7. jstat -gcutil pid 时间间隔 查看gc状况
  8. jmap -dump:format=b,file=name.dump pid 导出dump文件
  9. 用visualVM分析dump文件
作者:246炫
链接: 

补充:

Full GC频繁,相比之下,Young GC次数较少,什么原因?

常用的JVM调优工具:

Jconsole
jProfile
VisualVM

Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里

JProfiler:商业软件,需要付费。功能强大。详细说明参考这里

VisualVM:JDK自带,功能强大,与JProfiler类似

我是架构师小于哥

@终端研发部

关注我,偶尔出来聊聊天,写写代码,经常闲逛知乎分享开发心得和职场经验~

与[转帖]一文看尽 JVM GC 调优相似的内容:

[转帖]一文看尽 JVM GC 调优

https://zhuanlan.zhihu.com/p/428731068 首先看一个著名的学习方法论 向橡皮鸭求助学会提问,提问也是一门艺术提问前,先投入自己的时间做好功课发生了什么事情问题的基本情况你投入的研究和发现能正确提出你的问题,你的问题差不多已经解决一半深入的思考你的问题,大多情况下,

[转帖]一文看懂家庭宽带光纤是如何入户

目前,家庭宽带普遍实现了光纤入户,入户光纤一般在弱电箱的位置,家庭装修需要预埋网线,才能在后期流畅的使用网络。下文对网线预埋、网线选择、组网方式三个方面说一说。 一、光纤如何入户 1、光纤如何入户 入户光纤是不用家庭用户操心的,运营商在小区附件,一般部署了分光箱等。装机员会从分光箱拉一条皮纤到房子的

[转帖]一文看懂 .dockerignore

https://dhcp.cn/k8s/docker/dockerignore.html#dockerignore-%E8%AF%A6%E7%BB%86%E4%BB%8B%E7%BB%8D 一文看懂 .dockerignore 在 dockerfile 同级目录中创建名为 .dockerignore

[转帖]一文看懂eBPF、eBPF的使用(超详细)

https://zhuanlan.zhihu.com/p/480811707 eBPF(extended Berkeley Packet Filter) 可谓 Linux 社区的新宠,很多大公司都开始投身于 eBPF 技术,如 Goole、Facebook、Twitter 等。 eBPF 究竟有什么

[转帖]一文看懂mysql数据库事务隔离级别

概述 我们都知道除了MySQL默认采用RR隔离级别之外,其它几大数据库都是采用RC隔离级别。那为啥mysql要这样设置呢?其实是MySQL为了规避一个数据复制场景中的缺陷,而选择 Repeatable Read 作为默认隔离级别。不过不同数据库实现方式还是不太一样。 Oracle仅仅实现了RC 和

[转帖]一文带你掌握 Redis

https://www.bbsmax.com/A/8Bz8AKGkJx/ 一、摘要 谈起 Redis,相信大家都不会陌生,做过云平台开发的程序员多多少少会接触到它,Redis 英文全称:Remote Dictionary Server,也被称之为远程字典服务。 从官方的定义看,Redis 是一款开源

[转帖]一文解决内核是如何给容器中的进程分配CPU资源的?

https://zhuanlan.zhihu.com/p/615570804 现在很多公司的服务都是跑在容器下,我来问几个容器 CPU 相关的问题,看大家对天天在用的技术是否熟悉。 容器中的核是真的逻辑核吗? Linux 是如何对容器下的进程进行 CPU 限制的,底层是如何工作的? 容器中的 thr

[转帖]一文搞懂不同方式Redis集群搭建

https://bbs.huaweicloud.com/blogs/380521 【摘要】 1 实验环境准备 1.1 构建Redis的Docker镜像[root@iZ2ze4m2ri7irkf6h6n8zoZ redis]# docker pull redis[root@iZ2ze4m2ri7irk

[转帖]一文读懂GaussDB(openGauss) 的六大关键技术特性

https://www.314idc.com/article/5238906720560318 发布日期:2022-07-29 07:43:22 浏览量 :254 GaussDB(openGauss)是深度融合华为在数据库领域多年的经验,结合企业级场景需求,推出的新一代企业级分布式数据库,支持集中式

[转帖]一文详解 Redis 中 BigKey、HotKey 的发现与处理

https://baijiahao.baidu.com/s?id=1709288518127882966&wfr=spider&for=pc 一 前言 在Redis的使用过程中,我们经常会遇到BigKey(下文将其称为“大key”)及HotKey(下文将其称为“热key”)。大Key与热Key如果未