后端性能分析

dump导出命令

# 导出指定进程的堆转储文件
jmap -dump:format=b,file=heap_dump.hprof <pid>
# 示例
jmap -dump:format=b,file=myapp.hprof 12345
# arthas导出dump文件
# 直接生成堆转储文件
heapdump /tmp/heap_dump.hprof
# 只 dump live 对象
heapdump --live /tmp/heap_dump.hprof

问题排查常用的JVM指标


 -XX:+HeapDumpOnOutOfMemoryError:当发生OutOfMemoryError错误时,自动生成heapdump文件。后缀一般是.hprof

 -XX:HeapDumpPath:指定生成的heapdump文件的路径。

 -XX:HeapDumpBeforeFullGC:在进行Full GC之前生成heapdump文件。

 -XX:HeapDumpAfterFullGC:在进行Full GC之后生成heapdump文件。

 -XX:HeapDumpOnCtrlBreak:在接收到Ctrl+Break信号时生成heapdump文件。

 -XX:HeapDumpOnOutOfMemoryErrorPath:指定OutOfMemoryError错误发生时生成的heapdump文件的路径。

jstack(查看线程)、jmap(查看内存)、jstat(性能分析)、jps查看java Pid

内存溢出问题排查:


 java进程的pid可以使用jps命令查看

 如果是linux,ps -ef|grep java

 手动导出dump文件:

 windos:
 
 jmap -dump:format=b,file=d:\dump\1.hprof javapid

 linux: 
 
 ./jmap -dump:live,format=b,file=heap.hprof <pid>

 jmap -dump:live,format=b,file=/path/to/heapdump.hprof <pid>

 宕机自动输出的文件路径:
 
 tomcat/bin

线程打死cpu问题

生成线程转储文件(Thread Dump)

jstack <pid> > /path/to/thread_dump.txt

java编译过程:java文件--java编译器--字节码--类加载器--JVM

idea模拟内存溢出分析

编写java代码

这里得使用main方法,test方法无法指定vm参数

package com.kgf.kgfjavalearning2021.jvm;
import java.util.ArrayList;
/***
 * 设置内存大小:-Xmx60m -XX:SurvivorRatio=8
 */
public class JmapTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            byte[] arr = new byte[1024*100];//每次向list中存放100kb的字节数组
            list.add(arr);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

//方案2:方案1时间太久
public static void main(String[] args) {
        List<OOMClass> list = new ArrayList<OOMClass>();
        while(true) {
            list.add(new OOMClass());
        }
    }
//启动参数设置
-XX:+HeapDumpOnOutOfMemoryError -Xms20m -Xmx20m

设置idea启动main方法参数

设置手动导出dump

使用方式

1、手动导出dump

main方法执行的时间长一点

然后idea配置vm参数,-Xmx80m

最后任务管理器找到java的pid

使用管理员运行cmd命令:jmap -dump:format=b,file=d:\1.hprof 10636 导出

或者:

jmap -dump:live,format=b,file=e:\dumptest\1.hprof javapid

设置自动导出dump

使用方式

-XX:+HeapDumpOnOutOfMemoryError:在程序发生OOM时,导出应用程序的当前堆快照。-XX: HeapDumpPath:可以指定堆快照的保存位置。比如: -Xmx100m -XX: +HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=D: \m.hprof

也可以不设置此参数HeapDumpPath,默认oom后.hprof文件导出到项目相对路径

82e545fc-bcda-4fae-a903-a90d49f74382.png

分析dump

说明

查看dump

在线工具:

本地工具:

  • MAT(Memory Analyzer Tool)eclipse

  • idea

idea分析dump方法

可直接拖入idea中进行分析:

068588d1-4e61-457e-87d0-233142daf50d.png

选中右侧摘要,可查看溢出的代码堆栈位置

381b181a-e1fd-4657-b165-39c2ac1e2888.png

或者使用eclipse-mat分析工具进行分析显示:

mat打开文件后默认此界面:

9a454e26-ffdc-4a08-883e-d9fc2adbd79c.png

 a.Histogram:列出每个类的实例列表;

 b.Dominator:列出最大的对象和他们存在的东西;

 c.Top Comsumer:打印按类和包分组的最昂贵的对象;

 d.Duplicate Classes:检测由多个类加载器加载的类。

由于是一个简单的问题,因此我们直接选择Dominator,可以直接以百分比的形式打印出对象所占的百分比

14acbc67-1f4a-4ce8-b3df-ed7dc21fcbe0.png

找到源头:

14cacfc7-00a7-4832-8f35-983d5e990c02.png

0beea64a-42de-48e9-8c77-cc7a0eb4880e.png

d5c6f240-d472-4f67-9aa4-2b0eab96e4c3.png

74753cb3-b6e1-4510-82c5-bb70196bc033.png

然后拉到最后有个Theard Stack 可以看到目标堆栈所在代码位置

639bb20a-feec-4bf4-bab1-1a2c9e23e1ec.png

JVM内存结构解析

根据 JVM 规范,JVM 运行时区域大致分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区(jkd1.8废弃)五个部分。

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

程序计数器就是当前线程所执行的字节码的行号指示器,通过改变计数器的值,来选取下一行指令,通过它主要实现跳转、循环、恢复线程等功能。

在任何时刻,一个处理器内核只能运行一个线程,多线程是通过抢占 CPU,分配时间完成的。这时就需要有个标记,来标明线程执行到哪里,程序计数器便拥有这样的功能,所以,每个线程都已自己的程序计数器。

2、虚拟机栈(VM Stack)

虚拟机栈也就是平常所称的栈内存,每个线程对应一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法在执行的同时都会创建一个栈帧,方法被执行时入栈,执行完后出栈。

不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

局部变量表

存储着 java 基本数据类型(byte/boolean/char/int/long/double/float/short)以及对象的引用

注意:这里的基本数据类型指的是方法内的局部变量

由 局部变量表(随着栈帧的创建而创建,它的大小在编译时确定,创建时只需分配事先规定的大小即可。在方法运行过程中,局部变量表的大小不会发生改变)、操作数栈、动态连接、方法返回地址 构成

3、本地方法栈(Native Method Stack)

本地方法栈是为 JVM 运行 Native 方法准备的空间,由于很多 Native 方法都是用 C 语言实现的,所以它通常又叫 C 栈。

本地方法栈与虚拟机栈的作用是相似的,都是线程私有的,只不过本地方法栈是描述本地方法运行过程的内存模型。

本地方法被执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表、操作数栈、动态链接、方法出口信息等。方法执行结束后,相应的栈帧也会出栈,并释放内存空间。也会抛出 StackOverFlowError 和 OutOfMemoryError 异常。

虚拟机栈和本地方法栈的主要区别:

虚拟机栈执行的是 java 方法 本地方法栈执行的是 native 方法

与虚拟机栈类似,本地方法栈用于执行本地方法(即使用其他语言编写的方法)

4、堆(Heap)

Java 堆中是 JVM 管理的最大一块内存空间,主要存放对象实例。是所有线程共享的一块内存,在虚拟机启动时创建,几乎所有的对象实例都存放在这里,是垃圾收集器管理的主要区域。

Java 堆的分区:

在 jdk1.8 之前,分为新生代、老年代、永久代 在 jdk1.8 及之后,只分为新生代、老年代

永久代在 jdk1.8 已经被移除,被一个称为 “元数据区”(元空间)的区域所取代,jdk1.6之前:永久代,常量池是在方法区,jdk1.7去永久代,常量池在堆中,jdk1.8之后:无永久代,常量池在元空间(Metaspace)中

Java 堆内存大小:

堆内存大小 = 新生代 + 老年代(新生代占堆空间的1/3、老年代占堆空间2/3) 既可以是固定大小的,也可以是可扩展的(通过参数 -Xmx 和 -Xms 设定) 如果堆无法扩展或者无法分配内存时报 OOM

主要存储的内容是:

对象实例、类初始化生成的对象、基本数据类型的数组也是对象实例字符串常量池(字符串常量池原本存放在方法区,jdk8 开始放置于堆中)、静态变量(static 修饰的静态变量,jdk8 时从方法区迁移至堆中)、线程分配缓冲区(Thread Local Allocation Buffer)

堆和栈的区别:

管理方式,堆需要GC,栈自动释放 大小不同,堆比栈大 碎片相关:栈产生的碎片远小于堆,因为GC不是实时的 分配方式:栈支持静态分配内存和动态分配,堆只支持动态分配 效率:栈的效率比堆高

新生代

伊甸园区Eden(对象都是在这个区new出来的) 幸存区toSurvivorTo、幸存区fromSurvivorFrom:幸存区位置会互相交换,谁空谁是to

新生代的过程和规则:

1.当 Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收 2.SurvivorFrom 区:上一次 GC 的幸存者,作为这一次 GC 的被扫描者 3.SurvivorTo 区:保留了一次 MinorGC 过程中的幸存者 4.Eden 和 S0,S1 区的比例为 8 : 1 : 1,幸存者 S0,S1 区:复制之后发生交换,谁是空的,谁就是 SurvivorTo 区 5.JVM 每次只会使用 eden 和其中一块 survivor 来为对象服务,所以无论什么时候,都会有一块 survivor 是空的,因此新生代实际可用空间只有 90% 6.当 JVM 无法为新建对象分配内存空间的时候 (Eden 满了),Minor GC 被触发。因此新生代空间占用率越高,Minor GC 越频繁

老年代

老年代是Java堆内存中的一部分,用于存放生命周期较长的对象。官方文档中将老年代描述为“用于存放长期存活的对象,通常被认为是存活时间较长的对象的集合”。这些对象可能是经过多次Minor GC(新生代垃圾回收)后仍然存活的对象,或者是直接分配在老年代的大对象。

老年代的垃圾回收相对较少,因为老年代中的对象生命周期较长。通常情况下,老年代的垃圾回收采用标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法。标记-清除算法会标记所有存活的对象,然后清理掉不再使用的对象,但会导致内存碎片化。标记-整理算法则会将存活的对象向一端移动,然后清理掉边界外的对象,以解决内存碎片化的问题

永久代

存储的是java的运行环境或类信息,这个区域不存在垃圾回收,关闭jvm就会释放内存 一个启动类加载大量的jar包。tomcat部署太多应用。内存满了就oom jdk1.8,没有永久代,被“元空间”代替

5、方法区(Method Area)

方法区是被所有线程共享的内存区域。用于存储类的结构信息,常量池、静态变量(static)以及方法信息(修饰符、方法名、返回值、参数等)、类信息(类变量)等

方法区是 JVM 的一个规范,所有虚拟机必须要遵守的。常见的 JVM 虚拟机有 Hotspot 、 JRockit(Oracle)、J9(IBM)

方法区逻辑上属于堆的一部分,但是为了与堆区分,通常又叫非堆区

各个线程共享,主要用于存储已经被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。关闭 JVM 就会释放这个区域的内存。

Java8 以前是放在 JVM 内存中的,由堆空间中的永久代实现,受 JVM 内存大小参数限制 Java8 移除了永久代和方法区,引入了元空间

元空间(元数据区、Metaspace)

元空间是 JDK1.8 及之后,HotSpot 虚拟机对方法区的新实现。 元空间不在虚拟机中,而是直接用物理(本地)内存实现,不再受 JVM 内存大小参数限制,JVM 不会再出现方法区的内存溢出问题,但如果物理内存被占满了,元空间也会报 OOM

元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。

元空间与永久代之间最大的区别在于:

元空间并不在虚拟机中,而是使用本地内存。

默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

-XX:MetaspaceSize (初始空间大小):达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整,如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过 MaxMetaspaceSize时,适当提高该值。 -XX:MaxMetaspaceSize(最大空间)默认是没有限制的。 -XX:MinMetaspaceFreeRatio :在 GC 之后,最小的 Metaspace 剩余空间容量的百分比,减少为分配空间所导致的垃圾收集; -XX:MaxMetaspaceFreeRatio :在GC之后,最大的 Metaspace 剩余空间容量的百分比,减少为释放空间所导致的垃圾收集;

类的元数据放入本地内存中,字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由虚拟机的 MaxPermSize 控制,而由系统的实际可用空间来控制。

元空间替换永久代的原因分析:

1、字符串存在永久代中,容易出现性能问题和内存溢出。

2、通常会使用 PermSize 和 MaxPermSize 设置永久代的大小就决定了永久代的上限,但是类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

3、当使用元空间时,可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

4、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低

元空间和方法区不同的地方在于编译期间和类加载完成后的内容有少许不同,不过总的来说分为这两部分:

类元信息(Class)

类元信息在类编译期间放入元空间,里面放置了类的基本信息:版本、字段、方法、接口以及常量池表

常量池表:主要存放了类编译期间生成的字面量、符号引用,这些信息在类加载完后会被解析到运行时常量池中

运行时常量池(Runtime Constant Pool)

运行时常量池主要存放在类加载后被解析的字面量与符号引用,但不止这些

运行时常量池具备动态性,可以添加数据,比较多的使用就是 String 类的 intern() 方法

直接内存(Direct Memory)

直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

常见于 NIO 操作时,用于数据缓冲区(比如 ByteBuffer 使用的就是直接内存)。

分配、回收成本较高,但读写性能高。

直接内存不受 JVM 内存回收管理(直接内存的分配和释放是 Java 会通过 UnSafe 对象来管理的),但是系统内存是有限的,物理内存不足时会报OOM。

拓展:

JDK版本方法区的实现运行时常量池所在的位置
JDK6PermGen space(永久代)PermGen space(永久代)
JDK7PermGen space(永久代)Heap(堆)
JDK8Metaspace(元空间)Heap(堆)

JAVA GC垃圾回收方式详解

垃圾回收(Garbage Collection)是一种自动内存管理的机制,用于自动清理不再使用的对象并释放内存。在Java虚拟机中,垃圾回收可以分为以下几种方式:Minor GC, Major GC, Full GC ……

除了以上几种常见的垃圾回收方式,还有一些特殊的垃圾回收方式,如增量式垃圾回收(Incremental Garbage Collection)、并发垃圾回收(Concurrent Garbage Collection)等。这些方式旨在减少垃圾回收对应用程序的影响,提高系统的吞吐量和响应性能。

完整的GC过程:

对象的生命周期通常会经历新生代(Young Generation)和老年代(Old Generation)两个阶段。

  1. 对象的创建:当程序中创建一个新的对象时,该对象会被分配在新生代的Eden区域。
  2. Minor GC(新生代垃圾回收):当Eden区域满时,会触发Minor GC。Minor GC的目标是清理掉Eden区域中不再使用的对象,并将存活的对象复制到Survivor区域中。在复制过程中,如果对象经历了一次Minor GC并仍然存活,它将被移动到Survivor区域的另一个区域(通常是From区域)。
  3. Survivor区域的对象复制:在Survivor区域中,对象会经历多次的复制和年龄判断。当对象经历了一定次数的Minor GC后,它的年龄会增加。当对象的年龄达到一定阈值(通常是15),它将被晋升到老年代。
  4. Major GC(老年代垃圾回收):当老年代空间不足时,会触发Major GC。Major GC的目标是清理掉老年代中不再使用的对象。通常采用标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法进行垃圾回收。
  5. Full GC(全局垃圾回收):Full GC是对整个堆内存进行垃圾回收,包括新生代和老年代。Full GC的触发条件较为复杂,通常是在新生代无法容纳新对象时触发,或者在老年代空间不足时触发。

1、Minor GC(新生代垃圾回收)

Minor GC(新生代垃圾回收):主要针对新生代(Young Generation)进行垃圾回收。新生代中的对象生命周期较短,因此Minor GC的频率较高。通常采用复制算法(Copying Algorithm)进行垃圾回收。

MinorGC 的过程(采用复制算法):

首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,一般是 15,则赋值到老年代区) 同时把这些对象的年龄 + 1(如果 ServicorTo 不够位置了就放到老年区) 然后,清空 Eden 和 ServicorFrom 中的对象; 最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。

Minor GC 触发机制:

当年轻代满(指的是 Eden 满,Survivor 满不会引发 GC)时就会触发 Minor GC(通过复制算法回收垃圾)

对象年龄(Age)计数器

虚拟机给每个对象定义了一个对象年龄(Age)计数器。

如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。

对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。

对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold (阈值) 来设置。

2、Major GC(老年代垃圾回收)

又叫Old GC,老年代的对象比较稳定,所以 MajorGC 不会频繁执行,主要针对老年代(Old Generation)进行垃圾回收。老年代中的对象生命周期较长,因此Major GC的频率较低。通常采用标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)算法进行垃圾回收

在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。

当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

MajorGC 采用标记-清除算法:

1、首先扫描一次所有老年代,标记出存活的对象

2、然后回收没有标记的对象。

注意:

1、MajorGC 的耗时比较长(速度一般会比 Minor GC 慢10倍以上,STW 的时间更长),因为要扫描再回收。

2、MajorGC 会产生内存碎片,为了减少内存损耗,一般需要进行合并或者标记出来方便下次直接分配。

3、当老年代也满了装不下的时候,就会抛出 OOM(Out of Memory)异常。

3、Full GC(全局垃圾回收)

全局垃圾回收是指对整个堆内存进行垃圾回收,包括新生代和老年代。Full GC的触发条件较为复杂,通常是在新生代无法容纳新对象时触发,或者在老年代空间不足时触发。Full GC的执行时间较长,会导致应用程序的停顿。

Full GC 触发机制:

1、调用 System.gc 时,系统建议执行 Full GC,但是不必然执行 2、老年代空间不足 3、方法区空间不足 4、通过 Minor GC 后进入老年代的平均大小大于老年代的可用内存 5、由 Eden 区、survivor space1(From Space)区向 survivor space2(To Space)区复制时,对象大小大于 To Space 可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小 6、当永久代满时也会引发 Full GC,会导致 Class、Method 元信息的卸载

4、Minor GC、Major GC、Full GC 的区别

新生代收集(Minor GC/Young GC):只是新生代的垃圾收集

老年代收集(Major GC/Old GC ):只是老年代的垃圾收集

整堆收集(Full GC):收集整个 java 堆(young gen + old gen)和方法区的垃圾收集

5、STW(Stop The World)事件

stop-the-world,简称 STW,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW。

可达性分析算法中枚举根节点(GC Roots)会导致所有 Java 执行线程停顿。

注意:

1、分析工作必须在一个能确保一致性的快照中进行 2、一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上 3、如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证 4、被 STW 中断的应用程序线程会在完成 GC 之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以需要减少 STW 的发生。

STW 事件和采用哪款 GC 无关,所有的 GC 都有这个事件。哪怕是 G1 也不能完全避免 Stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

STW 是 JVM 在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。

6、GC 常用算法

分代收集算法(现在的虚拟机垃圾收集大多采用这种方式)

它根据对象的生存周期,将堆分为新生代(Young)和老年代(Tenure)。

新生代中,由于对象生存期短,每次回收都会有大量对象死去,所以使用的是复制算法。

老年代里的对象存活率较高,没有额外的空间进行分配担保,所以使用的是标记-整理 或者 标记-清除。

标记-清除算法

每个对象都会存储一个标记位,记录对象的状态(活着或是死亡)。

标记-清除算法分为两个阶段,一个是标记阶段,这个阶段内,为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行 GC 操作。

优点是可以避免内存碎片。

标记-压缩(标记-整理)算法

标记-压缩法是标记-清除法的一个改进版,和标记清除算法基本相同。

不同的就是,在清除完成之后,会把存活的对象向内存的一边进行压缩(整理),然后把剩下的所有对象全部清除,这样就可以解决内存碎片问题。

复制算法

复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。

当有效内存空间耗尽时,JVM 将暂停程序运行,开启复制算法 GC 线程。接下来 GC 线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC 线程将更新存活对象的内存引用地址指向新的内存地址。

此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

复制算法优点是不会产生内存碎片。

JVM堆内存解释

堆空间分成不同区的原因

堆空间分为新生代和老年代的原因

根据对象存活的时间,有的对象寿命长,有的对象寿命短。应该将寿命长的对象放在一个区,寿命短的对象放在一个区。不同的区采用不同的垃圾收集算法。寿命短的区清理频次高一点,寿命长的区清理频次低一点。

新生代分为了 eden、Survivor 区的原因

为了更好的管理堆内存中的对象,方便GC算法(复制算法)来进行垃圾回收。

如果没有 Survivor 区,那么 Eden 每次满了清理垃圾,存活的对象被迁移到老年区,老年区满了,就会触发 Full GC,而 Full GC 是非常耗时的。

将 Eden 区满了的对象,添加到 Survivor 区,等对象反复清理几遍之后都没清理掉,再放到老年区,这样老年区的压力就会小很多。即 Survivor 相当于一个筛子,筛掉生命周期短的,将生命周期长的放到老年代区,减少老年代被清理的次数。

新生代的 Survivor 区又分为 s0 和 s1 区的原因:

分两个区的好处就是解决内存碎片化。

为什么一个 Survivor 区不行?

假设现在只有一个survivor区,模拟一下流程:

新建的对象在 Eden 中,一旦 Eden 满了,触发一次 Minor GC,Eden 中的存活对象就会被移动到 Survivor 区。这样继续循环下去,下一次 Eden 满了的时候,问题来了,此时进行 Minor GC,Eden和 Survivor 各有一些存活对象,如果此时把 Eden 区的存活对象硬放到 Survivor 区,很明显这两部分对象所占有的内存是不连续的,也就导致了内存碎片化

GC 优化的本质,也是为什么分代的原因:

减少GC次数和GC时间,避免全区扫描

堆不是对象存储的唯一选择(逃逸分析)

如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样无需在堆上分配内存。也无须进行垃圾回收了。

逃逸分析概述:

一种可以有效减少 Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

逃逸分析的基本行为就是分析对象动态作用域:

当一个对象在方法中被定义后,对象只在方法内部引用,则认为没有发生逃逸 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。

JAVA应用内存

Java 程序内存 = JVM 内存 + 本地内存

JVM 内存(JVM 虚拟机数据区)

JVM 内存分堆外,堆内

1.堆内内存(见JVM堆内存解析)

2.堆外内存:

线程堆栈

可通过 -Xss 调整大小

内存不足时抛出

StackOverflowError(如果线程请求的栈深度大于虚拟机所允许的深度) OutOfMemoryError(如果 Java 虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存)

Socket 缓存区

每个 Socket 连接都 Receive 和 Send 两个缓存区,分别占大约 37KB 和 25KB 内存,连接多的话这块内存占用也比较可观。

如果无法分配,可能会抛出 IOException:Too many open files异常

JNI 代码

如果代码中使用了 JNI 调用本地库,那本地库使用的内存也不在堆中,而是占用 Java 虚拟机的本地方法栈和本地内存

虚拟机和垃圾收集器

虚拟机、垃圾收集器的工作也是要消耗一定数量的内存

本地内存(元空间 + 直接内存)

对于虚拟机没有直接管理的物理内存,也会有一定的利用,这些被利用但不在虚拟机内存的地方称为本地内存。

本地内存不受虚拟机内存参数的限制,只受物理内存容量的限制。

虽然不受参数的限制,如果所占内存超过物理内存,仍然会报 OOM

直接内存:

直接内存不是虚拟机运行时数据区的一部分,而是在 Java 堆外,直接向系统申请的内存区域。

可通过 -XX:MaxDirectMemorySize 调整大小,默认和 Java 堆最大值一样

内存不足时抛出OutOf-MemoryError或 者OutOfMemoryError:Direct buffer memory;

JVM优化(重点)

调优参数-配置方式

java [options] MainClass [arguments] options :JVM 启动参数。 配置多个参数的时候,参数之间使用空格分隔。 参数命名: 常见为 -参数名 参数赋值: 常见为 -参数名=参数值 或 -参数名:参数值

内存参数:

-Xms(s 为 strating):初始堆大小,JVM启动的时候,给定堆空间大小。

-Xmx(x 为 max):最大堆大小,JVM运行过程中,如果初始堆空间不足的时候,最大可以扩展到多少。

-Xmn(n 为 new):新生代大小

整个堆大小 = 新生代大小 + 老年代大小 + 持久代大小(jkd1.8废弃)

注意:持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小,此值对系统性能影响较大,Sun官方推荐配置为整个堆的 3/8

-Xss:设置每个线程的 Java 栈大小。根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。JDK5.0 以后每个线程 Java 栈大小为1M,以前每个线程堆栈大小为 256K。

示例:-Xss128k :设置每个线程的堆栈大小为128k。

-XX:NewSize=n:设置年轻代大小

-XX:NewRatio=n:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值。

-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值

-XX:MaxPermSize=n:设置永久代大小

-XX:MaxTenuringThreshold=n:设置垃圾最大年龄

垃圾回收器参数

JVM给了三种选择:串行收集器、并行收集器、并发收集器。串行收集器只适用于小数据量的情况。

-XX:+UseSerialGC: 设置串行收集器。

-XX:+UseParallelGC: 设置并行收集器,表示年轻代使用并行收集器。

-XX:+UseParNewGC: 设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会根据系统配置自行设置,所以无需再设置此值。

-XX:+UseParallelOldGC: 设置并行年老代收集器JDK6.0 支持对年老代并行收集。

-XX:+UseConcMarkSweepGC: 设置年老代并发收集器 CMS。

-XX:+UseG1GC: 设置G1收集器

-XX:ParallelGCThreads=n: 设置并行收集器收集时最大线程数使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n: 设置并行收集最大暂停时间,单位毫秒。可以减少STW时间。

-XX:GCTimeRatio=n: 设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n) 并发收集器设置

-XX:+CMSIncrementalMode: 设置为增量模式。适用于单 CPU 情况。

-XX:+UseAdaptiveSizePolicy: 设置此选项后,并行收集器会自动选择年轻代区大小和相应的 Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等。此值建议使用并行收集器时,一直打开。

-XX:CMSFullGCsBeforeCompaction=n: 此值设置运行多少次 GC 以后对内存空间进行压缩、整理。因为并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。

-XX:+UseCMSCompactAtFullCollection: 打开对年老代的压缩。可能会影响性能,但是可以消除碎片。

元空间参数:

-XX:MetaspaceSize:初始化的 Metaspace 大小,该值越大触发 Metaspace GC 的时机就越晚。随着GC的到来,虚拟机会根据实际情况调控 Metaspace 的大小,而上下浮动主要由 -XX:MaxMetaspaceFreeRatio 和 -XX:MinMetaspaceFreeRatio 两个参数控制。

在默认情况下,这个值大小根据不同的平台在 12M 到 20M 浮动。使用 java -XX:+PrintFlagsInitial 命令查看本机的初始化参数。

-XX:MinMetaspaceFreeRatio:当进行过 Metaspace GC 之后,会计算当前 Metaspace 的空闲空间比,如果空闲比小于这个参数,那么虚拟机将增加 MetaspaceSize 的大小(为了避免过早引发一次垃圾回收)。默认值为40,也就是40%。

设置该参数可以控制 Metaspace 的增长的速度,太小的值会导致 Metaspace 增长的缓慢,Metaspace的使用逐渐趋于饱和,可能会影响之后类的加载。而太大的值会导致 Metaspace 增长的过快,浪费内存。

-XX:MaxMetaspaceFreeRatio:当进行过 Metaspace GC 之后, 会计算当前Metaspace的空闲空间比,如果空闲比大于这个参数,那么虚拟机会减小 MetaspaceSize 的大小。默认值为70,也就是70%。

-XX:MaxMetaspaceExpansion :Metaspace 增长时的最大幅度。默认值大约为5MB。

-XX:MinMetaspaceExpansion :Metaspace 增长时的最小幅度。默认值大约330KB。

-XX:MaxMetaspaceSize:最大空间。默认是没有限制的。指定该值可以防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。

辅助参数

JVM提供了大量命令行参数,打印信息,供调试使用。商业项目上线的时候,不允许使用。一定使用 loggc。

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCApplicationConcurrentTime :打印每次垃圾回收前,程序未中断的执行时间。

-XX:+PrintGCApplicationStoppedTime: 打印垃圾回收期间程序暂停的时间。

-XX:PrintHeapAtGC :打印GC前后的详细堆栈信息

-Xloggc:filename :与上面几个配合使用,把相关日志信息记录到文件以便分析。

调优建议

年轻代大小选择

响应时间优先的应用:

尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。

在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达老年代的对象。

吞吐量优先的应用:

尽可能的设置大,可能到达 Gbit 的程度。

因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8 CPU 以上的应用。

老年代大小选择

响应时间优先的应用:

老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。

如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;

如果堆大了,则需要较长的收集时间。

最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息 持久代并发收集次数 传统GC信息 花在年轻代和年老代回收上的时间比例 减少年轻代和老年代花费的时间,一般会提高应用的效率

吞吐量优先的应用:

一般吞吐量优先的应用都有一个很大的年轻代和一个较小的老年代。

原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。

较小堆引起的碎片问题

因为老年代的并发收集器使用标记-清除算法,所以不会对堆进行压缩。

当收集器回收时,它会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记-清除方式进行回收。

如果出现“碎片”,可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。 -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对老年代进行压缩

性能故障常识

代码手动GC:

Runtime.getRuntime().gc();
System.gc();

System.gc()

System.gc() 是用 Java,C#和许多其他流行的高级编程语言提供的API。

当它被调用时,它将尽最大努力从内存中清除垃圾(即未被引用的对象)。

在默认情况下,通过 System.gc() 或者Runtime.getRuntime().gc() 的调用,会显式触发 Full GC(完整的 GC 事件),对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

在 GC 完成之前,整个 JVM 将冻结(即正在运行的所有服务将被暂停),通常完整的 GC 需要很长时间才能完成。

因此在不合适的时间运行 GC,将导致不良的用户体验,甚至是崩溃。

JVM 具有复杂的算法,该算法始终在后台运行,进行所有计算以及有关何时触发 GC 的计算。当显式调用 System.gc() 调用时,所有这些计算都将被抛掉。 system.gc() 调用附带一个免责声明,无法保证对垃圾收集器的调用**(不能确保立即生效)** System.gc() 可以从应用程序堆栈的各个部分调用:

开发的应用程序可以显式的调用 System.gc() 方法 System.gc() 也可以由第三方库,框架触发 可以由外部工具(如 VisualVM)通过使用 JMX 触发 如果应用程序使用了RMI,RMI会定期调用 System.gc() GC 操作应该由 JVM 自行控制,在绝大部分的场景都不建议程序员手动写代码显式进行 System.gc() 操作。

但是也不排除其中个别例外:

在开发多个微服务时,每个服务都有多个备份节点。在非业务高峰时段,可以从微服务-负载均衡的节点池中取出其中一个 JVM 实例。然后通过该 JVM 上的 JMX 显式触发 System.gc() 调用,一旦 GC 事件完成并且从内存中清除了垃圾,将该 JVM 放回到微服务-负载均衡的节点池中。

当然这个过程需要很好的微服务管理及服务发布机制配合,这样既能保证 JVM 垃圾内存的有效清理,又不影响业务的正常运行。

通过传递 JVM 参数 -XX:+DisableExplicitGC 来可以强制禁止显式调用gc

什么代码场景下会导致内存溢出?

  1. 静态变量:将对象赋值给静态变量,使其在整个程序的生命周期内都可访问和使用。
  2. 单例模式:使用单例模式创建对象,确保只有一个实例存在,并且可以在整个程序中被访问。
  3. 缓存对象:将对象缓存在内存中,以便在需要时重复使用,例如使用缓存框架(如Guava Cache)。
  4. 对象池:使用对象池来管理对象的创建和回收,以便在需要时从池中获取对象,而不是每次都创建新的对象。
  5. 长时间运行的线程:在长时间运行的线程中创建的对象通常会存活较长时间,例如后台任务、定时任务等。
  6. 强引用:将对象赋值给强引用变量,确保对象不会被垃圾回收器回收。
  7. 死循环,记录一次生产bug,一行日志打印了po对象,导致递归造成死循环,不仅造成内存溢出,而且还造成打印大量日志