摘要:该我的项目是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。

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