乐趣区

关于oom:一个神奇的bugOOM优雅终止线程系统内存占用较高

摘要: 该我的项目是 DAYU 平台的数据开发(DLF),数据开发中一个重要的性能就是 ETL(数据荡涤)。ETL 由源端到目标端,两头的业务逻辑个别由用户本人编写的 SQL 模板实现,velocity 是其中波及的一种模板语言。

Velocity 之 OOM

Velocity 的根本应用

Velocity 模板语言的根本应用代码如下:

1. 初始化模板引擎

2. 获取模板文件

3. 设置变量

4. 输入

在 ETL 业务中,Velocity 模板的输入是用户的 ETL SQL 语句集,相当于.sql 文件。这里官网提供的 api 须要传入一个 java.io.Writer 类的对象用于存储模板的生成的 SQL 语句集。而后,这些语句集会依据咱们的业务做 SQL 语句的拆分,一一执行。

java.io.Writer 类是一个抽象类,在 JDK1.8 中有多种实现,包含但不仅限于以下几种:

因为云环境对用户文件读写创立等权限的安全性要求比拟刻薄,因而,咱们应用了 java.io.StringWriter,其底层是 StringBuffer 对象,StringBuffer 底层是 char 数组。

简略模板 Hellovelocity.vm:

; “ 复制代码 ”)

set($iAMVariable = ‘good!’)

set($person.password = ‘123’)

Welcome ${name} to velocity.com
today is ${date}

foreach($one in $list)

$one

end

Name: ${person.name}
Password: ${person.password}

; “ 复制代码 ”)

HelloVelocity.java

; “ 复制代码 ”)

package com.xlf;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List; public class HelloVelocity {public static void main(String[] args) {// 初始化模板引擎

    VelocityEngine ve = new VelocityEngine();
    ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
    ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
    ve.init(); // 获取模板文件
    Template template = ve.getTemplate("Hellovelocity.vm");
    VelocityContext ctx = new VelocityContext(); // 设置变量
    ctx.put("name", "velocity");
    ctx.put("date", (new Date()));

    List temp = new ArrayList();
    temp.add("Hey");
    temp.add("Volecity!");
    ctx.put("list", temp);

    Person person = new Person();
    ctx.put("person", person); // 输入
    StringWriter sw = new StringWriter();
    template.merge(ctx, sw);
    System.out.println(sw.toString());
}

}

; “ 复制代码 ”)

控制台输入

OOM 重现

大模板文件 BigVelocity.template.vm

(文件字数超出博客限度,稍后在附件中给出~~)

模板文件自身就 379kb 不算大,关键在于其中定义了一个蕴含 90000 多个元素的 String 数组,数组的每个元素都是”1”,而后写了 79 层嵌套循环,循环的每一层都是遍历该 String 数组;最内层循环调用了一次:

show table;

这意味着这个模板将生成蕴含 96372 的 79 次方个 SQL 语句,其中每一个 SQL 语句都是:

show table;

将如此微小的字符量填充进 StringWriter 对象外面,至多须要 10 的 380 屡次方 GB 的内存空间,这简直是不事实的。因而 OOM 溢出是必然的。

BigVelocity.java

; “ 复制代码 ”)

package com.xlf;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.StringWriter; public class BigVelocity {public static void main(String[] args) {// 初始化模板引擎

    VelocityEngine ve = new VelocityEngine();
    ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
    ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
    ve.init(); // 获取模板文件
    Template template = ve.getTemplate("BigVelocity.template.vm");
    VelocityContext ctx = new VelocityContext();
    StringWriter sw = new StringWriter();
    template.merge(ctx, sw);
}

}

; “ 复制代码 ”)

控制台输入

OOM 起因剖析

Velocity 模板生成的后果写入 StringWriter 对象中,如后面剖析,其底层是一个 char 数组。间接产生 OOM 的代码在于 java.util.Array.copyOf() 函数:

StringWriter 底层 char 数组容量极限测试

StringWriterOOMTest.java

; “ 复制代码 ”)

package com.xlf;

import java.io.StringWriter; public class StringWriterOOMTest {public static void main(String[] args) {

    System.out.println("The maximum value of Integer is:" + Integer.MAX_VALUE);
    StringWriter sw = new StringWriter(); int count = 0; for (int i = 0; i < 100000; i++) {for (int j = 0; j < 100000; j++) {sw.write("This will cause OOMn");
            System.out.println("sw.getBuffer().length():" + sw.getBuffer().length() + ", count:" + (++count));
        }
    }
}

}

; “ 复制代码 ”)

Jvm 参数设置(参考硬件配置)

环境:JDK8 + Windows10 台式机 + 32GB 内存 + 1TB SSD + i7-8700

如果你的硬件配置不充沛,请勿轻易尝试!

测试后果

StringWriterOOMTest 运行时的整个过程内存大小在 Windows 工作管理器中达 10300 多 MB 时,程序进行。

控制台输入

测试后果剖析

char 数组元素最大值不会超过 Integer.MAX_VALUE,回事十分靠近的一个值,我这里相差 20 多。网上搜寻了一番,比拟靠谱的说法是:的确比 Integer.MAX_VALUE 小一点,不会等于 Integer.MAX_VALUE,是因为 char[] 对象还有一些别的空间占用,比方对象头,应该说是这些空间加起来不能超过 Integer.MAX_VALUE。如果有读者感兴趣,能够自行摸索下别的类型数组的元素个数。我这里也算是一点高见,抛砖引玉。

OOM 解决方案

起因总结

通过下面一系列重现与剖析,咱们晓得了 OOM 的根本原因是模板文件渲染而成的 StringWriter 对象过大。具体表现在:

  1. 如果零碎没有足够大的内存空间调配给 JVM,会导致 OOM,因为这部分内存并不是无用内存,JVM 不能回收
  2. 如果零碎有足够大的内存空间调配给 JVM,char 数组中的元素个数在靠近于 MAX_VALUE 会抛出 OOM 谬误。

解决方案

后面剖析过,出于平安的起因,咱们只能用 StringWriter 对象去接管模板渲染后果的输入。不能用文件。所以只能在 StringWriter 自身去做文章进行改良了:

继承 StringWriter 类,重写其 write 办法为:

; “ 复制代码 ”)

StringWriter sw = new StringWriter() { public void write(String str) {int length = this.getBuffer().length() + str.length(); // 限度大小为 10MB

    if (length > 10 * 1024 * 1024) {this.getBuffer().delete(0, this.getBuffer().length()); throw new RuntimeException("Velocity template size exceeds limit!");
    } this.getBuffer().append(str);
}

};

; “ 复制代码 ”)

其余代码放弃不变

BigVelocitySolution.java

; “ 复制代码 ”)

package com.xlf;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

import java.io.StringWriter; public class BigVelocitySolution {public static void main(String[] args) {// 初始化模板引擎

    VelocityEngine ve = new VelocityEngine();
    ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
    ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
    ve.init(); // 获取模板文件
    Template template = ve.getTemplate("BigVelocity.template.vm");
    VelocityContext ctx = new VelocityContext();
    StringWriter sw = new StringWriter() { public void write(String str) {int length = this.getBuffer().length() + str.length(); // 限度大小为 10MB
            if (length > 10 * 1024 * 1024) {this.getBuffer().delete(0, this.getBuffer().length()); throw new RuntimeException("Velocity template size exceeds limit!");
            } this.getBuffer().append(str);
        }
    };
    template.merge(ctx, sw);
}

}

; “ 复制代码 ”)

控制台输入

如果 velocity 模板渲染后的 sql 语句集大小在容许的范畴内,这些语句集会依据咱们的业务做 SQL 语句的拆分,逐句执行。

如何优雅终止线程

在后续逐句执行 sql 语句的过程中,每一句 sql 都是调用的周边服务(DLI,OBS,MySql 等)去执行的,后果每次都会返回给咱们的作业开发调度服务(DLF)后盾。咱们的 DLF 平台反对及时进行作业的性能,也就是说如果这个作业在调度过程中要执行 10000 条 SQL,我要在中途进行不执行前面的 SQL 了——这样的性能是反对的。

在批改下面提到 OOM 那个 bug 并通过测试后,测试同学发现咱们的作业无奈停止下来,换句话说,咱们作业所在的 java 线程无奈进行。

线程进行失败重现

一番 debug 与代码深刻研读之后,发现咱们我的项目中的确是调用了对应的线程对象的 interrupt 办法 thread.interrupt(); 去终止线程的。

那么为什么调用了 interrupt 办法仍旧无奈终止线程?

TestForInterruptedException.java

; “ 复制代码 ”)

package com.xlf; public class TestForInterruptedException {public static void main(String[] args) {

    StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10; i++) {sb.append("show tables;n");
    } int i = 0; for (String str : sb.toString().split("n")) {if (i > 4) {Thread.currentThread().interrupt();
            System.out.println(i + "after interrupt");
        }
        System.out.println(str);
        System.out.println(i++);
    }

}

}

; “ 复制代码 ”)

控制台输入

测试后果剖析

TestForInterruptedException.main 函数中做的事件足够简略,先产生一个大一点的字符串,拆分成 10 段小字符串,for 循环中逐段打印小字符串;并希图从第 5 段(初始段为 0)开始,去终止线程。后果发现线程并没有终止!

这是怎么回事?为什么调用了线程的 interrupt 办法并没有终止线程?或者说是因为 jvm 须要一点工夫去响应这个办法?其实并非如此,感兴趣的同学能够把循环次数加的更大一些,在循环开始几次就进行 interrupt,你会发现后果还是这样。

通过一番摸索,线程终止的办法无外乎两种:

  • 应用该 Thread 对象的 stop() 办法能让线程马上进行,然而这种办法太过于暴力,实际上并不会被应用到,详见 JDK1.8 的正文:
  • Deprecated. This method is inherently unsafe. Stopping a thread with Thread.stop causes it to unlock all of the monitors that it has locked (as a natural consequence of the unchecked ThreadDeath exception propagating up the stack). If any of the objects previously protected by these monitors were in an inconsistent state, the damaged objects become visible to other threads, potentially resulting in arbitrary behavior. Many uses of stop should be replaced by code that simply modifies some variable to indicate that the target thread should stop running. The target thread should check this variable regularly, and return from its run method in an orderly fashion if the variable indicates that it is to stop running. If the target thread waits for long periods (on a condition variable, for example), the interrupt method should be used to interrupt the wait…
  • 第二种办法就是下面 JDK 正文中提到的设置标记位的做法。这类做法又分为两种,无论哪一种都须要去被终止的线程自身去“被动”地判断该标记位的状态:
  1. 设置一个惯例的标记位,比方:boolean 类型变量的 true/ false, 依据变量的状态去决定线程是否持续运行——代码里去被动判断变量状态。这种个别用在循环中,检测到相应状态就 break, return 或者 throw exception。
  2. 应用 Thead 类的实例办法 interrupt 去终止该 thread 对象代表的线程。然而 interrupt 办法实质上也是设置了一个中断标识位,而且该标记位一旦被捕捉(读取),“大部分时候”就会被重置(生效)。因而它并不保障线程肯定可能进行,而且不保障马上可能进行,有如下几类状况:
  3. interrupt 办法设置的中断标识位后,如果该线程往后的程序执行逻辑中执行了 Object 类的 wait/join/sleep,这 3 个办法会及时捕捉 interrupt 标记位,重置并抛出 InterruptedException。
  4. 相似于上一点,java.nio.channels 包下的 InterruptibleChannel 类也会去被动捕捉 interrupt 标记位,即线程处于 InterruptibleChannel 的 I / O 阻塞中也会被中断,之后标记位同样会被重置,而后 channel 敞开,抛出 java.nio.channels.ClosedByInterruptException;同样的例子还有 java.nio.channels.Selector,详见 JavaDoc
  5. Thread 类的实例办法 isInterrupted() 也能去捕捉中断标识位并重置标识位,这个办法用在须要判断程序终止的中央,能够了解为被动且显式地去捕捉中断标识位。
  6. 值得注意的是:抛出与捕捉 InterruptedException 并不波及线程标识位的捕捉与重置
  7. 怎么了解我后面说的中断标识位一旦被捕捉,“大部分时候”就会被重置?Thread 类中有 private native boolean isInterrupted(boolean ClearInterrupted); 当传参为 false 时就能在中断标识位被捕捉后不重置。然而个别状况它只会用于两个中央
  8. Thread 类的 static 办法:此处会重置中断标识位,而且无奈指定某个线程对象,只能是以后线程去判断

  1. Thread 类的实例办法:这个办法也是罕用的判断线程中断标识位的办法,而且不会重置标识位。

小结

要终止线程,目前 JDK 中可行的做法有:

  1. 本人设置变量去标识一个线程是否已中断
  2. 正当利用 JDK 自身的线程中断标识位去判断线程是否中断

这两个做法都须要后续做相应解决比方去 break 循环,return 办法或者抛出异样等等。

线程何时终止?

线程终止起因一般来讲有两种:

  1. 线程执行完他的失常代码逻辑,天然完结。
  2. 线程执行中抛出 Throwable 对象且不被显式捕捉,JVM 会终止线程。家喻户晓:Throwable 类是 Exception 和 Error 的父类!

线程异样终止 ExplicitlyCatchExceptionAndDoNotThrow.java

; “ 复制代码 ”)

package com.xlf; public class ExplicitlyCatchExceptionAndDoNotThrow {public static void main(String[] args) throws Exception {

    boolean flag = true;
    System.out.println("Main started!"); try {throw new InterruptedException();
    } catch (InterruptedException exception) {System.out.println("InterruptedException is caught!");
    }
    System.out.println("Main doesn't stop!"); try {throw new Throwable();
    } catch (Throwable throwable) {System.out.println("Throwable is caught!");
    }
    System.out.println("Main is still here!"); if (flag) {throw new Exception("Main is dead!");
    }
    System.out.println("You'll never see this!");
}

}

; “ 复制代码 ”)

控制台输入

测试后果剖析

这个测试验证了后面对于线程异样终止的论断:

线程执行中抛出 Throwable 对象且不被显式捕捉,JVM 会终止线程。

优雅手动终止线程

线程执行中须要手动终止,最好的做法就是设置标识位(能够是 interrupt 也能够是本人定义的),而后及时捕捉标识位并抛出异样,在业务逻辑的最初去捕捉异样并做一些收尾的清理动作:比方统计工作执行失败胜利的比例,或者敞开某些流等等。这样,程序的执行就兼顾到了失常与异样的状况并失去了优雅的解决。

TerminateThreadGracefully.java

; “ 复制代码 ”)

package com.xlf; public class TerminateThreadGracefully {public static void main(String[] args) {

    StringBuilder sb = new StringBuilder(); for (int i = 0; i < 10; i++) {sb.append("show tables;n");
    } int i = 0; try {for (String str : sb.toString().split("n")) {if (i > 4) {Thread.currentThread().interrupt(); if (Thread.currentThread().isInterrupted()) {throw new InterruptedException();
                }
                System.out.println(i + "after interrupt");
            }
            System.out.println(str);
            System.out.println(i++);
        }
    } catch (InterruptedException exception) { // TODO:此处可能做一些清理工作
        System.out.println(Thread.currentThread().isInterrupted());
    }
    System.out.println("Thread main stops normally!");
}

}

; “ 复制代码 ”)

控制台输入

为何我的项目中的线程终止失败?

咱们我的项目中的确是调用了对应的线程对象的 interrupt 办法 thread.interrupt(); 去终止线程的。

那么为什么线程不能相应中断标识位并终止呢?

回到咱们我的项目的业务逻辑:

整个 job 分为模板读取、渲染以及 SQL 执行三个阶段,一般而言前两个阶段工夫会比拟快。在后续逐句执行 sql 语句的过程中,每一句 sql 都是调用的周边服务(DLI,OBS,MySql 等)去执行的,后果每次都会返回给咱们的作业开发调度服务(DLF)后盾。咱们的 DLF 平台反对及时进行作业的性能,也就是说如果这个作业在调度过程中要执行 10000 条 SQL,我要在中途进行不执行前面的 SQL 了——这样的性能是反对的。

因而问题就出在了 SQL 执行的过程。通过屡次 debug 发现:在 SQL 执行过程中须要每次都往 OBS(华为自研,第三方包)中写 log,该过程不可略去。调用该线程对象的 interrupt 办法 thread.interrupt(),interrupt 标识位最早被 OBS 底层用到的 java.util.concurrent. CountDownLatch 类的 await() 办法捕捉到,重置标识位并抛出异样,而后在一层层往上抛的时候被转变成了别的异样类型,而且不能依据最终抛的异样类型去判断是否是因为咱们手动终止 job 引起的。

对于第三方包 OBS 依据本人的底层逻辑去解决 CountDownLatch 抛的异样,这本无可非议。然而咱们的程序终止不了!为了达到终止线程的做法,我在其中退出了一个自定义的标记变量,当调用 thread.interrupt() 的时候去设置变量的状态,并在几个关键点比方 OBS 写 log 之后去判断我的自定义标识位的状态,如果状态扭转了就抛出 RuntimeException(能够不被捕捉,最小化改变代码)。并且为了能重用线程池里的线程对象,在每次 job 开始的中央去从重置这一自定义标识位。最终达到了优雅手动终止 job 的目标。

这一部分的源码波及我的项目细节就不贴出来了,然而相干的逻辑后面曾经代码展现过。

零碎内存占用较高且不精确

在线程中运行过程中定义的一般的局部变量,非 ThreadLocal 型,一般而言会随着线程完结而失去回收。我所遇到的景象是下面的那个线程无奈进行的 bug 解决之后,线程停下来了,然而在 linux 上运行 top 命令相应过程内存占用还是很高。

  1. 首先我用 jmap -histo:alive pid 命令对 jvm 进行进行了强制 GC,发现此时堆内存的确基本上没用到多少(不关老年带还是年老带都大略是 1% 左右。)然而 top 命令看到的占用大略在 18% * 7G(linux 总内存)左右。
  2. 其次,我用了 jcmd 命令去对对外内存进行剖析,排挤了堆外内存泄露的问题
  3. 而后接下来就是用 jstack 命令查看 jvm 过程的各个线程都是什么样的状态。与 job 无关的几个线程全副是 waiting on condition 状态(线程完结,线程池将他们挂起的)。
  4. 那么,当初失去一个初步的论断就是:不论是该 jvm 过程用到的堆内存还是堆外内存,都很小(绝对于 top 命令显式的 18% * 8G 占用量而言)。所以是否能够猜测:jvm 只是向操作系统申请了这么多内存临时没有偿还回去,留待下次线程池有新工作时持续复用呢?本文最初一部分试验就围绕着一点开展。

景象重现

在如下试验中

设置 jvm 参数为:

-Xms100m -Xmx200m -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

其意义在于:

限度 jvm 初始内存为 100M,最大堆内存为 200M。并在 jvm 产生垃圾回收时及时打印具体的 GC 信息以及工夫戳。而我的代码里要做的事件就是重现 jvm 内存不够而不得不产生垃圾回收。同时察看操作系统层面该 java 过程的内存占用。

SystemMemoryOccupiedAndReleaseTest.java

; “ 复制代码 ”)

package com.xlf;

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; public class SystemMemoryOccupiedAndReleaseTest {public static void main(String[] args) {try {

        System.out.println("start");
        Thread.sleep(5000);
    } catch (InterruptedException e) {e.printStackTrace();
    }

    ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 3, 30, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() { public Thread newThread(Runnable r) {return new Thread(r);
            }
        }, new ThreadPoolExecutor.AbortPolicy()); try {System.out.println("(executor 已初始化):");
        Thread.sleep(1000);
    } catch (InterruptedException e) {e.printStackTrace();
    }

    Thread t1 = new Thread(new Runnable() {
        {System.out.println("t1 曾经初始化");
        }
        @Override public void run() { byte[] b = new byte[100 * 1024 * 1024];
            System.out.println("t1 调配了 100M 空间给数组"); try {Thread.sleep(5000);
            } catch (InterruptedException e) {e.printStackTrace(); throw new RuntimeException("t1 stop");
            }
            System.out.println("t1 stop");
        }
    }, "t1"); try {Thread.sleep(1000);
    } catch (InterruptedException e) {e.printStackTrace();
    }

    Thread t2 = new Thread(new Runnable() {
        {System.out.println("t2 曾经初始化");
        }
        @Override public void run() { byte[] b = new byte[100 * 1024 * 1024];
            System.out.println("t2 调配了 100M 空间给数组"); try {Thread.sleep(5000);
            } catch (InterruptedException e) {e.printStackTrace(); throw new RuntimeException("t2 stop");
            }
            System.out.println("t2 stop");
        }
    }, "t2"); try {Thread.sleep(1000);
    } catch (InterruptedException e) {e.printStackTrace();
    }

    Thread t3 = new Thread(new Runnable() {
        {System.out.println("t3 曾经初始化");
        }
        @Override public void run() { byte[] b = new byte[100 * 1024 * 1024];
            System.out.println("t3 调配了 100M 空间给数组"); try {Thread.sleep(5000);
            } catch (InterruptedException e) {e.printStackTrace(); throw new RuntimeException("t3 stop");
            }
            System.out.println("t3 stop");
        }
    }, "t3"); try {Thread.sleep(1000);
    } catch (InterruptedException e) {e.printStackTrace();
    }

    executor.execute(t1);
    System.out.println("t1 executed!"); try {Thread.sleep(10000);
    } catch (InterruptedException e) {e.printStackTrace();

    }
    executor.execute(t2);
    System.out.println("t2 executed!"); try {Thread.sleep(10000);
    } catch (InterruptedException e) {e.printStackTrace();

    }
    executor.execute(t3);
    System.out.println("t3 executed!"); try {Thread.sleep(10000);
    } catch (InterruptedException e) {e.printStackTrace();

    }

    System.out.println("jmap -histo:live pid by cmd:"); try {Thread.sleep(20000);
    } catch (InterruptedException e) {e.printStackTrace();

    }
    System.out.println("After jmap!"); // You may run jmap -heap pid using cmd here // executor.shutdown();

}
}

; “ 复制代码 ”)

上述代码里我先定义了三个 Thread 对象,这三个对象都是在 run() 办法里调配了 100M 大小的 char[],而后线程休眠(sleep)5 秒。而后 new 一个线程池,并将这三个线程对象顺次交给线程池去 execute。线程池每两次 execute 之间相隔 10 秒,这是为了给足工夫给上一个线程跑完并让 jvm 去回收这部分内存(200M 的最大堆内存,一个线程对象要占用 100 多 M,要跑下一个线程必然会产生 GC),这样就能把 GC 信息打印下来便于察看。最初等到三个线程都执行结束 sleep 一段时间(大略 20 秒),让我有工夫手动在 cmd 执行 jmap -histo live pid,该命令会强制触发 FullGC,jmap 命令之后你也能够试着执行 jmap -heap pid,该命令不会触发 gc,然而能够看下整个 jvm 堆的占用详情.

控制台输入

在 jmp -histo:live 执行之前过程在操作系统内存占用:

执行 jmp -histo:live 之后

执行 jmap -heap pid 的后果:

测试后果剖析 /win10 工作管理器不精确

t1 调配了 100M 空间给数组之后,t2 完结:

内存占用:107042K,总可用堆空间大小:166400K

无奈给 t2 调配 100M,触发 FullGC:

103650K->1036K(98304K)

t2 调配了 100M 空间给数组之后,t2 完结:

内存占用:104461K,总可用堆空间大小:166400K

无奈给 t3 调配 100M,触发 FullGC:

103532K->1037K(123904K)

t3 调配了 100M 空间给数组之后,t3 完结.

jmap -histo:live pid by cmd:

103565K->997K(123904K)

最初 jmap -heap pid 后果中堆大小也是 123M。

这一过程中,操作系统层面 jvm 过程内存占用不会超过 122M,jmap -histo:live pid 触发 FullGC 之后维持在 87M 左右(重复几次试验都是这个后果)

那么为什么 jvm 的堆栈信息大小与资源管理器对应的不统一呢?

这个问题在网上搜了一圈,论断如下:

提交内存指的是程序要求零碎为程序运行的最低大小,如果得不到满足,就会呈现内存不足的提醒。

工作集内存才是程序真正占用的内存,而工作集内存 = 可共享内存 + 专用内存

可共享内存的用途是当你关上更多更大的软件时,或者进行内存整理时,这一部分会被分给其他软件,所以这一块算是为程序运行预留下来的内存专用内存,专用内存指的是目前程序运行独占的内存,这一块和可共享内存不一样,无论目前零碎内存如许缓和,这块专用内存是不会被动给其余程序腾出空间的

所以总结一下就是,工作管理器显示的内存,实际上是显示的程序的专用内存而程序真正占用的内存,是工作集内存

下面两张图能对的上:

如下两张图“勉强”能对的上:

然而和 jmap 触发 gc 之后的堆内存 123904K 还有点差距,这部分差距不大,临时网上找不到比拟靠谱的答复,笔者猜测可能这一部分用的是别的过程的可共享内存。我去 linux 上试了一把,也有这个内存算不准的问题。这个问题留待下次填坑吧~~

论断

  1. 线程完结能够是失常完结,也能够是抛出不被 catch 的 Throwable 对象而异样终止
  2. 线程完结后,线程所占内存空间会在 jvm 须要空间时进行回收利用,这些空间次要包含:调配在堆上的对象,其惟一援用只存在于该线程中
  3. JVM 在进行 FullGC 后尽管堆空间占用很小,但并不会仅仅向操作系统申请 xms 大小的内存,这部分看似很大的可用内存,实际上会在有新的线程任务分配时失去利用
  4. JVM 过程堆内存占用比操作系统层面统计的该过程内存占用稍高一些,可能是共享内存的起因,这点留待下次填坑!

写在最初

附上本文中形容的所有代码以及对应资源文件,供大家参考学习!也欢送大家评论发问!

 VelocityExperiment.zip 19.40KB

本文分享自华为云社区《一个神奇的 bug:OOM?优雅终止线程?零碎内存占用较高?》,原文作者:UnstoppableRock。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版