JVM优化

1、JVM体系结构

概览

Java GC 主要回收 堆 和 方法区 的内容

类加载器作用

  • 双亲委派机制
  • Java类加载的沙箱安全机制

常见的垃圾回收算法

  • 引用计数法

在双端循环,互相引用的时候,容易报错,目前很少使用这种方式了

  • 复制算法

复制算法在年轻代的时候,进行使用,复制时候有交换 (谁空谁是to)

优点:速度快,没有产生内存碎片

  • 标记清除

先标记,后清除,缺点是会产生内存碎片,用于老年代多一些

  • 标记整理

2、汇总

1、JVM垃圾回收的时候如何确定垃圾?是否知道什么是GC Roots

1.1 什么是垃圾

简单来说就是内存中已经不再被使用的空间就是垃圾

1.2 如何判断一个对象是否可以被回收

1.2.1 引用计数法

Java中,引用和对象是有关联的。如果要操作对象则必须用引用进行。

因此,很显然一个简单的办法就是通过引用计数来判断一个对象是否可以回收。简单说,给对象中添加一个引用计数器

每当有一个地方引用它,计数器值加1

每当有一个引用失效,计数器值减1

任何时刻计数器值为零的对象就是不可能再被使用的,那么这个对象就是可回收对象。

那么为什么主流的Java虚拟机里面都没有选用这个方法呢?其中最主要的原因是它很难解决对象之间相互循环引用的问题。

该算法存在但目前无人用了,解决不了循环引用的问题,了解即可。

1.2.2 枚举根节点做可达性分析

根搜索路径算法

为了解决引用计数法的循环引用个问题,Java使用了可达性分析的方法:

所谓 GC Roots 或者说 Tracing Roots的“根集合” 就是一组必须活跃的引用

基本思路就是通过一系列名为 GC Roots的对象作为起始点,从这个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连,则说明此对象不可用。也即给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到的(可到达的)对象就被判定为存活,没有被遍历到的对象就被判定为死亡

必须从GC Roots对象开始,这个类似于linux的 / 也就是根目录

蓝色部分是从GC Roots出发,能够循环可达

而白色部分,从GC Roots出发,无法到达

一句话理解GC Roots

假设我们现在有三个实体,分别是 人,狗,毛衣

然后他们之间的关系是:人 牵着 狗,狗穿着毛衣,他们之间是强连接的关系

有一天人消失了,只剩下狗狗 和 毛衣,这个时候,把人想象成 GC Roots,因为 人 和 狗之间失去了绳子连接,

那么狗可能被回收,也就是被警察抓起来,被送到流浪狗寄养所

假设狗和人有强连接的时候,狗狗就不会被当成是流浪狗

那些对象可以当做GC Roots
  • 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
  • 方法区中的类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中的JNI(Native方法)的引用对象

代码说明


/**
 * 在Java中,可以作为GC Roots的对象有:
 * - 虚拟机栈(栈帧中的局部变量区,也叫做局部变量表)中的引用对象
 * - 方法区中的类静态属性引用的对象
 * - 方法区中常量引用的对象
 * - 本地方法栈中的JNI(Native方法)的引用对象
 */
public class GCRootDemo {

    // 方法区中的类静态属性引用的对象
    // private static GCRootDemo2 t2;

    // 方法区中的常量引用,GC Roots 也会以这个为起点,进行遍历
    // private static final GCRootDemo3 t3 = new GCRootDemo3(8);

    public static void m1() {
        // 第一种,虚拟机栈中的引用对象
        GCRootDemo t1 = new GCRootDemo();
        System.gc();
        System.out.println("第一次GC完成");
    }
    public static void main(String[] args) {
        m1();
    }
}

2、JVM参数调优

2.1 前言

你说你做过JVM调优和参数配置,请问如何盘点查看JVM系统默认值

使用jps和jinfo进行查看

-Xms:初始堆空间
-Xmx:堆最大值
-Xss:栈空间

-Xms 和 -Xmx最好调整一致,避免GC和应用程序争抢内存,进而导致内存忽高忽低产生停顿

2.2 JVM 参数类型

  • 标准参数(从JDK1-JDK12都在,很稳定)
    • -version
    • -help
    • java -showversion
  • X参数(了解)
    • -Xint: 解释执行
    • -Xcomp: 第一次使用就编译成本地代码
    • -Xmixed: 混合模式
  • XX参数(重点)
    • boolean类型
      • 公式:-XX: +或者-某个属性 +表示开启,-表示关闭
      • case: -XX:-PrintGCDetails:表示关闭GC详情输出
    • key-value类型
      • 公式: -XX:属性key=属性value
      • 不满意初始值,可以通过下列命令调整
      • case: -XX:MetaspaceSize=21807104 设置元空间大小的值
      • case: -XX:MaxTenuringThreshold=15 设置达到多少年龄升到养老区

2.3 查看运行的Java程序,JVM参数是否开启,具体值为多少?

首先我们运行一个HelloGC的java程序

public class HelloGC {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("hello GC");
        Thread.sleep(Integer.MAX_VALUE);
    }
}

然后使用下列命令查看它的默认参数

jps:查看Java后台运行
jinfo:查看正在运行的程序

具体使用:

jps -l 得到进程号

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows
$ jps -l
2576 com.demo.juc.jvm.HelloGC
11020
21148 org.jetbrains.jps.cmdline.Launcher
23116 sun.tools.jps.Jps

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows

查看到HelloGC的进程号为:2576

#### 1) 查看boolean类型参数是否开启

我们使用jinfo -flag 然后查看是否开启PrintGCDetails这个参数

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows
$ jinfo -flag PrintGCDetails 2576
-XX:-PrintGCDetails

得到内容:

-XX:-PrintGCDetails

上面提到了,-号表示关闭,即没有开启PrintGCDetails这个参数

下面我们需要在启动HelloGC的时候,增加 PrintGCDetails这个参数,需要在运行程序的时候配置JVM参数

然后在VM Options中加入下面的代码,现在+号表示开启

-XX:+PrintGCDetails

然后在使用jinfo查看我们的配置

jps -l
jinfo -flag PrintGCDetails 13540

得到的结果为

-XX:+PrintGCDetails

我们看到原来的-号变成了+号,说明我们通过 VM Options配置的JVM参数已经生效了

2) 查看key-value类型参数具体值

查看元空间堆内存大小

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows
$ jinfo -flag MetaspaceSize 1916
-XX:MetaspaceSize=21807104 //默认大小

修改元空间堆内存大小

修改过后

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows
$ jinfo -flag MetaspaceSize 2400
-XX:MetaspaceSize=1073741824

总结:

查看公式:jinfo -flag 配置项 进程号

使用下列命令,会把jvm的全部默认参数输出

jinfo -flags 进程号

示例:

$ jinfo -flags 2400
Attaching to process ID 2400, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.281-b09
 //系统加载默认配置
Non-default VM flags: -XX:CICompilerCount=12 -XX:InitialHeapSize=264241152 -XX:MaxHeapSize=4221566976 -XX:MaxNewSize=1407188992 -XX:MetaspaceSize=1073741824 -XX:MinHeapDeltaBytes=524288
 -XX:NewSize=88080384 -XX:OldSize=176160768 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:-UseLargePagesIndividualAllocation -XX:+UseParalle
lGC
 //人工修改配置
Command line:  -XX:MetaspaceSize=1024m -javaagent:D:\utils\IntelliJ IDEA 2020.3.3\lib\idea_rt.jar=13408:D:\utils\IntelliJ IDEA 2020.3.3\bin -Dfile.encoding=UTF-8

题外话(坑题)

两个经典参数: -Xms 和 -Xmx 这两个参数如何解释

这两个参数还是属于 -xx参数,因为取了别名

  • -Xms 等价于-XX:InitialHeapSize : 初始化堆内存(默认只用最大物理内存的1/64)
  • -Xmx等价于-XX:MaxHeapSize: 最大堆内存(默认只用最大物理内存的1/4)

2.4 查看JVM默认参数

第一种
jps
jinfo -flag 具体参数 Java进程号 (查看单个参数)
jinfo -flags Java进程号 (查看所有)
第二种
  • -XX:PrintFlagsInitial 初始化默认安装,jdk出厂设置)
    • 主要是查看默认值
    • 公式
      • java -XX:+PrintFlagsInitial -version
      • java -XX:+PrintFlagsInitial (重要参数)

case:

$ java -XX:+PrintFlagsInitial
[Global flags] //表示安装jdk出厂参数默认配置
     intx ActiveProcessorCount                      = -1                                  {product}
    uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
    uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {product}
    uintx AdaptiveSizePausePolicy                   = 0                                   {product}
    uintx AdaptiveSizePolicyCollectionCostMargin    = 50                                  {product}
    uintx AdaptiveSizePolicyInitializingSteps       = 20                                  {product}
    uintx AdaptiveSizePolicyOutputInterval          = 0                                   {product}
    uintx AdaptiveSizePolicyWeight                  = 10                                  {product}
    uintx AdaptiveSizeThroughPutPolicy              = 0                                   {product}
    uintx AdaptiveTimeWeight                        = 25                                  {product}
     bool AdjustConcurrency                         = false                               {product}
     bool AggressiveHeap                            = false                               {product}
     bool AggressiveOpts                            = false                               {product}
     intx AliasLevel                                = 3                                   {C2 product}
     bool AlignVector                               = true                                {C2 product}
     intx AllocateInstancePrefetchLines             = 1                                   {product}
     intx AllocatePrefetchDistance                  = -1                                  {product}

  • -XX:+PrintFlagsFinal:表示修改以后,最终的值(根据每台计算机的配置不同,配置不同)(重点)

case:

$ java -XX:+PrintFlagsFinal
[Global flags]
     intx ActiveProcessorCount                      = -1                                  {product}
    uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
	uintx HeapFirstMaximumCompactionCount           = 3                                   {product}
    uintx HeapMaximumCompactionInterval             = 20                                  {product}
    uintx HeapSizePerGCThread                       = 87241520                            {product}
     bool IgnoreEmptyClassPaths                     = false                               {product}
     bool IgnoreUnrecognizedVMOptions               = false                               {product}
    uintx IncreaseFirstTierCompileThresholdAt       = 50                                  {product}
     bool IncrementalInline                         = true                                {C2 product}
    uintx InitialBootClassLoaderMetaspaceSize       = 4194304                             {product}
    uintx InitialCodeCacheSize                      = 2555904                             {pd product}
    uintx InitialHeapSize                          := 264241152                           {product} //表示改过
    uintx InitialRAMFraction                        = 64                                  {product}
   double InitialRAMPercentage                      = 1.562500                            {product}
    uintx InitialSurvivorRatio                      = 8                                   {product}
    uintx InitialTenuringThreshold                  = 7                                   {product}

会将JVM的各个结果都进行打印

如果有 := 表示修改过的(人为修改或者是jvm改过), = 表示没有修改过的

示例:

改之前:

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows
$ java -XX:+PrintFlagsFinal
[Global flags]
     intx ActiveProcessorCount                      = -1                                  {product}
    uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
	MetaspaceSize                             		= 21807104     						  {product}

MetaspaceSize = 21807104

改之后

DELL@DESKTOP-U5BTBFQ MINGW32 /c/Windows
$ java -XX:+PrintFlagsFinal -XX:MetaspaceSize=512M
[Global flags]
     intx ActiveProcessorCount                      = -1                                  {product}
    uintx AdaptiveSizeDecrementScaleFactor          = 4                                   {product}
	MetaspaceSize                             	   := 536870912        					  {product}

MetaspaceSize := 536870912

第三种

打印出JVM的默认的简单初始化参数

  • -XX:+PrintCommandLineFlags -version
    • 公式:java -XX:+PrintCommandLineFlags -version

case:

DELL@DESKTOP-U5BTBFQ MINGW32 /e/workSpace/idea/testWork/springboot-demo (master)
$ java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=263766848 -XX:MaxHeapSize=4220269568 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC //(默认使用垃圾回收器)
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)

3、 工作中常用的JVM基本配置参数

查看JVM的初始化堆内存 -Xms 和最大堆内存 Xmx

public class HelloGC {
    public static void main(String[] args) throws InterruptedException {
        // 返回Java虚拟机中内存的总量
        long totalMemory = Runtime.getRuntime().totalMemory();
        // 返回Java虚拟机中试图使用的最大内存量
        long maxMemory = Runtime.getRuntime().maxMemory();
        System.out.println("TOTAL_MEMORY(-Xms) = " + totalMemory + "(字节)、" + (totalMemory / (double)1024 / 1024) + "MB");
        System.out.println("MAX_MEMORY(-Xmx) = " + maxMemory + "(字节)、" + (maxMemory / (double)1024 / 1024) + "MB");

    }
}

结果为:

TOTAL_MEMORY(-Xms) = 257425408(字节)245.5MB
MAX_MEMORY(-Xmx) = 3790077952(字节)3614.5MB

-Xms 初始堆内存为:物理内存的1/64 -Xmx 最大堆内存为:系统物理内存的 1/4

1、生活常用调优参数

  • -Xms:初始化堆内存,默认为物理内存的1/64,等价于 -XX:initialHeapSize
  • -Xmx:最大堆内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize
  • -Xss:设计单个线程栈的大小,一般默认为512K~1024K,等价于 -XX:ThreadStackSize
    • 使用 jinfo -flag ThreadStackSize 会发现 -XX:ThreadStackSize = 0
    • 这个值的大小是取决于平台的
    • Linux/x64:1024KB
    • OS X:1024KB
    • Oracle Solaris:1024KB
    • Windows:取决于虚拟内存的大小
  • -Xmn:设置年轻代大小
  • -XX:MetaspaceSize:设置元空间大小
    • 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。
    • -Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
    • 但是默认的元空间大小:只有20多M
    • 为了防止在频繁的实例化对象的时候,让元空间出现OOM,因此可以把元空间设置的大一些
  • -XX:PrintGCDetails:输出详细GC收集日志信息
    • GC
    • Full GC

GC日志收集流程图:

我们使用一段代码,制造出垃圾回收的过程

首先我们设置一下程序的启动配置: 设置初始堆内存为10M,最大堆内存为10M

-Xms10m -Xmx10m -XX:+PrintGCDetails

然后用下列代码,创建一个 非常大空间的byte类型数组

byte [] byteArray = new byte[50 * 1024 * 1024];

运行后,发现会出现下列错误,这就是OOM:java内存溢出,也就是堆空间不足

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.moxi.interview.study.GC.HelloGC.main(HelloGC.java:22)

同时还打印出了GC垃圾回收时候的详情

[GC (Allocation Failure) [PSYoungGen: 1972K->504K(2560K)] 1972K->740K(9728K), 0.0156109 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC (Allocation Failure) [PSYoungGen: 504K->480K(2560K)] 740K->772K(9728K), 0.0007815 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 480K->0K(2560K)] [ParOldGen: 292K->648K(7168K)] 772K->648K(9728K), [Metaspace: 3467K->3467K(1056768K)], 0.0080505 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 648K->648K(9728K), 0.0003035 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 648K->630K(7168K)] 648K->630K(9728K), [Metaspace: 3467K->3467K(1056768K)], 0.0058502 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 80K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 3% used [0x00000000ffd00000,0x00000000ffd143d8,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 630K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 8% used [0x00000000ff600000,0x00000000ff69dbd0,0x00000000ffd00000)
 Metaspace       used 3510K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 389K, capacity 392K, committed 512K, reserved 1048576K

问题发生的原因:

因为们通过 -Xms10m 和 -Xmx10m 只给Java堆栈设置了10M的空间,但是创建了50M的对象,因此就会出现空间不足,而导致出错

同时在垃圾收集的时候,我们看到有两个对象:GC 和 Full GC

GC垃圾收集

GC在新生区

[GC (Allocation Failure) [PSYoungGen: 1972K->504K(2560K)] 1972K->740K(9728K), 0.0156109 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]

GC (Allocation Failure):表示分配失败,那么就需要触发年轻代空间中的内容被回收

[PSYoungGen: 1972K->504K(2560K)] 1972K->740K(9728K), 0.0156109 secs]
Full GC垃圾回收

Full GC大部分发生在养老区

[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 648K->630K(7168K)] 648K->630K(9728K), [Metaspace: 3467K->3467K(1056768K)], 0.0058502 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 

规律:

[名称: GC前内存占用 -> GC后内存占用 (该区内存总大小)]

当我们出现了老年代都扛不住的时候,就会出现OOM异常

2、 -XX:SurvivorRatio

调节新生代中 eden 和 S0、S1的空间比例,默认为 -XX:SuriviorRatio=8,Eden:S0:S1 = 8:1:1

加入设置成 -XX:SurvivorRatio=4,则为 Eden:S0:S1 = 4:1:1

SurvivorRatio值就是设置eden区的比例占多少,S0和S1相同

Java堆从GC的角度还可以细分为:新生代(Eden区,From Survivor区合To Survivor区)和老年代

  • eden、SurvivorFrom复制到SurvivorTo,年龄 + 1

首先,当Eden区满的时候会触发第一次GC,把还活着的对象拷贝到SurvivorFrom去,当Eden区再次触发GC的时候会扫描Eden区合From区域,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到To区域(如果对象的年龄已经到达老年的标准,则赋值到老年代区),通知把这些对象的年龄 + 1

  • 清空eden、SurvivorFrom

然后,清空eden,SurvivorFrom中的对象,也即复制之后有交换,谁空谁是to

  • SurvivorTo和SurvivorFrom互换

最后,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代

3、 -XX:NewRatio(了解)

配置年轻代new 和老年代old 在堆结构的占比

默认: -XX:NewRatio=2 新生代占1,老年代2,年轻代占整个堆的1/3

-XX:NewRatio=4:新生代占1,老年代占4,年轻代占整个堆的1/5,NewRadio值就是设置老年代的占比,剩下的1个新生代

新生代特别小,会造成频繁的进行GC收集

4、 -XX:MaxTenuringThreshold(记住)

设置垃圾最大年龄,SurvivorTo和SurvivorFrom互换,原SurvivorTo成为下一次GC时的SurvivorFrom区,部分对象会在From和To区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认为15),最终如果还是存活,就存入老年代

这里就是调整这个次数的,默认是15,并且设置的值 在 0~15之间

查看默认进入老年代年龄:jinfo -flag MaxTenuringThreshold 17344

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻对象不经过Survivor区,直接进入老年代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大的值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概念

4、Java内存溢出OOM

JVM中常见的两个错误

StackoverFlowError :栈溢出

OutofMemoryError: java heap space:堆溢出

除此之外,还有以下的错误:

  • java.lang.StackOverflowError
  • java.lang.OutOfMemoryError:java heap space
  • java.lang.OutOfMemoryError:GC overhead limit exceeeded
  • java.lang.OutOfMemoryError:Direct buffer memory
  • java.lang.OutOfMemoryError:unable to create new native thread
  • java.lang.OutOfMemoryError:Metaspace

架构

OutOfMemoryError和StackOverflowError是属于Error,不是Exception

StackoverFlowError

堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用

栈一般是512K,不断的深度调用,直到栈被撑破

public class StackOverflowErrorDemo {

    public static void main(String[] args) {
        stackOverflowError();
    }
    /**
     * 栈一般是512K,不断的深度调用,直到栈被撑破
     * Exception in thread "main" java.lang.StackOverflowError
     */
    private static void stackOverflowError() {
        stackOverflowError();
    }
}

运行结果:

Exception in thread "main" java.lang.StackOverflowError
	at com.moxi.interview.study.oom.StackOverflowErrorDemo.stackOverflowError(StackOverflowErrorDemo.java:17)

OutOfMemoryError

1、Java heap space

创建了很多对象,导致堆空间不够存储

public class JavaHeapSpaceDemo {
    public static void main(String[] args) {

        // 堆空间的大小 -Xms10m -Xmx10m
        // 创建一个 80M的字节数组
        byte [] bytes = new byte[80 * 1024 * 1024];
    }
}

我们创建一个80M的数组,会直接出现Java heap space

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
2、GC overhead limit exceeded

GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存

连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成什么情况呢?

那就是GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。

代码演示:

为了更快的达到效果,我们首先需要设置JVM启动参数

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m

这个异常出现的步骤就是,我们不断的像list中插入String对象,直到启动GC回收

/**
 * GC 回收超时
 * JVM参数配置: -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
 */
public class GCOverheadLimitDemo {
    public static void main(String[] args) {
        int i = 0;
        List<String> list = new ArrayList<>();
        try {
            while(true) {
                list.add(String.valueOf(++i).intern());
     //intern():如果字符串s在字符串常量池中存在对应字面量,则intern()方法返回该字面量的地址;如果不存在,则创建一个对应的字面量,并返回该字面量的地址
            }
        } catch (Exception e) {
            System.out.println("***************i:" + i);
            e.printStackTrace();
            throw e;
        } finally {

        }

    }
}

运行结果:

[Full GC (Ergonomics) [PSYoungGen: 2047K->2047K(2560K)] [ParOldGen: 7106K->7106K(7168K)] 9154K->9154K(9728K), [Metaspace: 3504K->3504K(1056768K)], 0.0311093 secs] [Times: user=0.13 sys=0.00, real=0.03 secs] 
[Full GC (Ergonomics) [PSYoungGen: 2047K->0K(2560K)] [ParOldGen: 7136K->667K(7168K)] 9184K->667K(9728K), [Metaspace: 3540K->3540K(1056768K)], 0.0058093 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 2560K, used 114K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 5% used [0x00000000ffd00000,0x00000000ffd1c878,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
  to   space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
 ParOldGen       total 7168K, used 667K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 9% used [0x00000000ff600000,0x00000000ff6a6ff8,0x00000000ffd00000)
 Metaspace       used 3605K, capacity 4540K, committed 4864K, reserved 1056768K
  class space    used 399K, capacity 428K, committed 512K, reserved 1048576K


Exception in thread “main” java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.toString(Integer.java:403)
at java.lang.String.valueOf(String.java:3099)
at com.moxi.interview.study.oom.GCOverheadLimitDemo.main(GCOverheadLimitDemo.java:18)

我们能够看到 多次Full GC,并没有清理出空间,在多次执行GC操作后,就抛出异常 GC overhead limit

3、Direct buffer memory

Netty + NIO:这是由于NIO引起的

写NIO程序的时候经常会使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel) 与 缓冲区(Buffer)的I/O方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

ByteBuffer.allocate(capability):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢

ByteBuffer.allocteDirect(capability):第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快

但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候怼内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那么程序就奔溃了。

一句话说:本地内存不足,但是堆内存充足的时候,就会出现这个问题

查看jvm直接使用物理内存大小:

System.out.println("配置的maxDirectMemory:"+(sun.misc.VM.maxDirectMemory()/(double)1024/1024)+"MB");

我们使用 -XX:MaxDirectMemorySize=5m 配置能使用的堆外物理内存为5M

-Xms10m -Xmx10m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m

然后我们申请一个6M的空间

// 只设置了5M的物理内存使用,但是却分配 6M的空间
ByteBuffer bb = ByteBuffer.allocateDirect(6 * 1024 * 1024);

运行结果,出现下面问题

配置的maxDirectMemory:5.0MB
[GC (System.gc()) [PSYoungGen: 2030K->488K(2560K)] 2030K->796K(9728K), 0.0008326 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 488K->0K(2560K)] [ParOldGen: 308K->712K(7168K)] 796K->712K(9728K), [Metaspace: 3512K->3512K(1056768K)], 0.0052052 secs] [Times: user=0.09 sys=0.00, real=0.00 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:693)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
	at com.moxi.interview.study.oom.DIrectBufferMemoryDemo.main(DIrectBufferMemoryDemo.java:19)
4、unable to create new native thread

不能够创建更多的新的线程了,也就是说创建线程的上限达到了

在高并发场景的时候,会应用到

高并发请求服务器时,经常会出现如下异常java.lang.OutOfMemoryError:unable to create new native thread,准确说该native thread异常与对应的平台有关

导致原因:

  • 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
  • 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread

解决方法:

  1. 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
  2. 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
/**
 * 无法创建更多的线程
 */
public class UnableCreateNewThreadDemo {
    public static void main(String[] args) {
        for (int i = 0; ; i++) {
            System.out.println("************** i = " + i);
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

这个时候,就会出现下列的错误,线程数大概在 900多个

Exception in thread "main" java.lang.OutOfMemoryError: unable to cerate new native thread

如何查看线程数

ulimit -u
5、Metaspace

元空间内存不足,Matespace元空间应用的是本地内存

-XX:MetaspaceSize 的处理化大小为20M左右(默认)

元空间是什么:

元空间就是我们的方法区,存放的是类模板,类信息,常量池等

Metaspace是方法区HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟内存中,而是使用本地内存,也即在java8中,class metadata(the virtual machines internal presentation of Java class),被存储在叫做Matespace的native memory

永久代(java8后背元空间Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码

模拟Metaspace空间溢出,我们不断生成类 往元空间里灌输,类占据的空间总会超过Metaspace指定的空间大小

代码

在模拟异常生成时候,因为初始化的元空间为20M,因此我们使用JVM参数调整元空间的大小,为了更好的效果

-XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m

代码如下:

/**
 * 元空间溢出
 */
public class MetaspaceOutOfMemoryDemo {

    // 静态类
    static class OOMTest {

    }

    public static void main(final String[] args) {
        // 模拟计数多少次以后发生异常
        int i =0;
        try {
            while (true) {
                i++;
                // 使用Spring的动态字节码技术
                Enhancer enhancer = new Enhancer();
                enhancer.setSuperclass(OOMTest.class);
                enhancer.setUseCache(false);
                enhancer.setCallback(new MethodInterceptor() {
                    @Override
                    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                        return methodProxy.invokeSuper(o, args);
                    }
                });
            }
        } catch (Exception e) {
            System.out.println("发生异常的次数:" + i);
            e.printStackTrace();
        } finally {

        }

    }
}

结果如下:

发生异常的次数: 201
java.lang.OutOfMemoryError:Metaspace

教程视频

参考笔记

参考笔记

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值