一個神奇的bug:OOM?優雅終止線程?系統內存佔用較高?

2020-12-06 紙鶴視界

摘要:該項目是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.comtoday is ${date}#foreach($one in $list) $one#endName: ${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;publicclass HelloVelocity { publicstaticvoid 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;publicclass BigVelocity { publicstaticvoid 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;publicclass StringWriterOOMTest { publicstaticvoid 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 OOM\n"); 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對象過大。具體表現在:

如果系統沒有足夠大的內存空間分配給JVM,會導致OOM,因為這部分內存並不是無用內存,JVM不能回收如果系統有足夠大的內存空間分配給JVM,char數組中的元素個數在接近於MAX_VALUE會拋出OOM錯誤。解決方案

前面分析過,出於安全的原因,我們只能用StringWriter對象去接收模板渲染結果的輸出。不能用文件。所以只能在StringWriter本身去做文章進行改進了:

繼承StringWriter類,重寫其write方法為:

StringWriter sw = new StringWriter() { publicvoid write(String str) { int length = this.getBuffer().length() + str.length(); // 限制大小為10MBif (length > 10 * 1024 * 1024) { this.getBuffer().delete(0, this.getBuffer().length()); thrownew 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;publicclass BigVelocitySolution { publicstaticvoid 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() { publicvoid write(String str) { int length = this.getBuffer().length() + str.length(); // 限制大小為10MBif (length > 10 * 1024 * 1024) { this.getBuffer().delete(0, this.getBuffer().length()); thrownew 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;publicclass TestForInterruptedException { publicstaticvoid 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注釋中提到的設置標誌位的做法。這類做法又分為兩種,無論哪一種都需要去被終止的線程本身去「主動」地判斷該標誌位的狀態:設置一個常規的標誌位,比如:boolean類型變量的true/ false, 根據變量的狀態去決定線程是否繼續運行——代碼裡去主動判斷變量狀態。這種一般用在循環中,檢測到相應狀態就break, return或者throw exception。使用Thead類的實例方法interrupt去終止該thread對象代表的線程。但是interrupt方法本質上也是設置了一個中斷標識位,而且該標誌位一旦被捕獲(讀取),「大部分時候」就會被重置(失效)。因此它並不保證線程一定能夠停止,而且不保證馬上能夠停止,有如下幾類情況:interrupt方法設置的中斷標識位後,如果該線程往後的程序執行邏輯中執行了Object類的wait/join/sleep,這3個方法會及時捕獲interrupt標誌位,重置並拋出InterruptedException。類似於上一點,java.nio.channels包下的InterruptibleChannel類也會去主動捕獲interrupt標誌位,即線程處於InterruptibleChannel的I/O阻塞中也會被中斷,之後標誌位同樣會被重置,然後channel關閉,拋出java.nio.channels.ClosedByInterruptException;同樣的例子還有java.nio.channels.Selector,詳見JavaDocThread類的實例方法isInterrupted()也能去捕獲中斷標識位並重置標識位,這個方法用在需要判斷程序終止的地方,可以理解為主動且顯式地去捕獲中斷標識位。值得注意的是:拋出與捕獲InterruptedException並不涉及線程標識位的捕獲與重置怎麼理解我前面說的中斷標識位一旦被捕獲,「大部分時候」就會被重置?Thread類中有private native boolean isInterrupted(boolean ClearInterrupted);當傳參為false時就能在中斷標識位被捕獲後不重置。然而一般情況它只會用於兩個地方Thread類的static方法:此處會重置中斷標識位,而且無法指定某個線程對象,只能是當前線程去判斷

Thread類的實例方法:這個方法也是常用的判斷線程中斷標識位的方法,而且不會重置標識位。

小結

要終止線程,目前JDK中可行的做法有:

自己設置變量去標識一個線程是否已中斷合理利用JDK本身的線程中斷標識位去判斷線程是否中斷這兩個做法都需要後續做相應處理比如去break循環,return方法或者拋出異常等等。

線程何時終止?

線程終止原因一般來講有兩種:

線程執行完他的正常代碼邏輯,自然結束。線程執行中拋出Throwable對象且不被顯式捕獲,JVM會終止線程。眾所周知:Throwable類是Exception和Error的父類!線程異常終止ExplicitlyCatchExceptionAndDoNotThrow.java

package com.xlf;publicclass ExplicitlyCatchExceptionAndDoNotThrow { publicstaticvoid main(String[] args) throws Exception { boolean flag = true; System.out.println("Main started!"); try { thrownew InterruptedException(); } catch (InterruptedException exception) { System.out.println("InterruptedException is caught!"); } System.out.println("Main doesn't stop!"); try { thrownew Throwable(); } catch (Throwable throwable) { System.out.println("Throwable is caught!"); } System.out.println("Main is still here!"); if (flag) { thrownew Exception("Main is dead!"); } System.out.println("You'll never see this!"); }}

控制臺輸出

測試結果分析

這個測試驗證了前面關於線程異常終止的結論:

線程執行中拋出Throwable對象且不被顯式捕獲,JVM會終止線程。

優雅手動終止線程

線程執行中需要手動終止,最好的做法就是設置標識位(可以是interrupt也可以是自己定義的),然後及時捕獲標識位並拋出異常,在業務邏輯的最後去捕獲異常並做一些收尾的清理動作:比如統計任務執行失敗成功的比例,或者關閉某些流等等。這樣,程序的執行就兼顧到了正常與異常的情況並得到了優雅的處理。

TerminateThreadGracefully.java

package com.xlf;publicclass TerminateThreadGracefully { publicstaticvoid 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()) { thrownew 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命令相應進程內存佔用還是很高。

首先我用jmap -histo:alive pid命令對jvm進行進行了強制GC,發現此時堆內存確實基本上沒用到多少(不關老年帶還是年輕帶都大概是1%左右。)但是top命令看到的佔用大概在18% * 7G(linux總內存)左右。其次,我用了jcmd命令去對對外內存進行分析,排斥了堆外內存洩露的問題然後接下來就是用jstack命令查看jvm進程的各個線程都是什麼樣的狀態。與job有關的幾個線程全部是waiting on condition狀態(線程結束,線程池將他們掛起的)。那麼,現在得到一個初步的結論就是:不管是該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;publicclass SystemMemoryOccupiedAndReleaseTest { publicstaticvoid 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) { returnnew 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 publicvoid run() { byte[] b = newbyte[100 * 1024 * 1024]; System.out.println("t1分配了100M空間給數組"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); thrownew 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 publicvoid run() { byte[] b = newbyte[100 * 1024 * 1024]; System.out.println("t2分配了100M空間給數組"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); thrownew 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 publicvoid run() { byte[] b = newbyte[100 * 1024 * 1024]; System.out.println("t3分配了100M空間給數組"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); thrownew 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上試了一把,也有這個內存算不準的問題。這個問題留待下次填坑吧~~

結論

線程結束可以是正常結束,也可以是拋出不被catch的Throwable對象而異常終止線程結束後,線程所佔內存空間會在jvm需要空間時進行回收利用,這些空間主要包括:分配在堆上的對象,其唯一引用只存在於該線程中JVM在進行FullGC後雖然堆空間佔用很小,但並不會僅僅向作業系統申請xms大小的內存,這部分看似很大的可用內存,實際上會在有新的線程任務分配時得到利用JVM進程堆內存佔用比作業系統層面統計的該進程內存佔用稍高一些,可能是共享內存的原因,這點留待下次填坑!寫在最後

附上本文中描述的所有代碼以及對應資源文件,供大家參考學習!也歡迎大家評論提問!

VelocityExperiment.zip 19.40KB

本文分享自華為雲社區《一個神奇的bug:OOM?優雅終止線程?系統內存佔用較高?》,原文作者:UnstoppableRock。

點擊關注,第一時間了解華為雲新鮮技術~

相關焦點

  • Java常見內存溢出異常分析
    對於Linux系統,我們可以通過ulimit -u來查看此限制。   給虛擬機分配的內存過大,導致創建線程的時候需要的native內存太少。我們都知道作業系統對每個進程的內存是有限制的,我們啟動Jvm,相當於啟動了一個進程,假如我們一個進程佔用了4G的內存,那麼通過下面的公式計算出來的剩餘內存就是建立線程棧的時候可以用的內存。
  • 進程和線程常見的19個問題
    子進程的初始化空間是父進程的一個副本,這裡涉及兩個不同地址空間,不可寫的內存區是共享的,某些UNIX的實現使程序正文在兩者間共享,因為它是不可修改的。還有一種寫時複製共享技術,子進程共享父進程的所有內存,一旦兩者之一想要修改部分內存,則這塊內存被複製確保修改發生在當前進程的私有內存區域。06進程為何終止?有什麼事件會觸發進程的終止呢?
  • 智能硬體風口 廣東新支點推嵌入式作業系統
    內存信息統計開源的Linux內核,未對內存的各項信息提供一個完成的統計顯示,而中興新支點嵌入式作業系統則針對需求,在proc文件系統中新增了六類內存統計信息的功能:物理內存大小;可用物理內存大小;剩餘物理內存大小;高端內存大小;低端內存大小;用戶空間佔用內存大小。
  • 陳延偉:任督二脈之內存管理總結筆記
    所謂的一一映射,是指虛擬地址和物理地址只是存在一個物理上的偏移。        在x86系統中,內存分區和內存映射區存在如下的關係:        在Linux系統,每一個進程都有一個oomscore,這個數值越高,說明進程消耗的內存越多,在發生OOM的情況下,oom score越高的進程就越有可能被系統幹掉,從而緩解系統的內存壓力。我們可以通過/proc/pid/oom_score文件查看進程的oom score。
  • 佔用6G內存的原神也叫大?你永遠也猜不透黑粉的腦迴路!
    對配置要求高? 罵! 對手機內存佔用高? 罵! 截至現如今,原神這款遊戲在各大主流遊戲平臺的風評好壞半摻,一星與五星共存,那麼在今天這期文章中,就讓小編帶各位讀者聊一聊,有關於元神手機版內存佔用的問題吧。
  • adobe pr做視頻渲染到底是什麼資源佔用最多
    大家好,這次給大家解疑一下adobe pr做視頻渲染的用什麼配置電腦的問題現在大家很多人都在做短視頻,自媒體,頭條號,百家號,企鵝號等平臺上很多人也都在做,自拍的,原創的,視頻二次加工的,這個都需要用到視頻處理軟體,現在用的比較多的也就adobe公司的pr和索尼的Vegas,但是做這個需要什麼的樣配置的電腦呢,到底是要CPU好點還是顯卡好點,還是內存大點呢
  • 谷歌瀏覽器將採用新機制降低內存的佔用
    微軟在Windows 10 v2004版中新增段堆內存管理機制,該機制有助於谷歌瀏覽器等軟體降低使用時的內存開銷。在微軟推出這項新技術後谷歌瀏覽器也進行積極適配,但谷歌工程師發現內存確實降低然而處理器使用量卻飆升。
  • Linux系統監控工具atop
    此狀態的進程通常在等待系統資源,如磁碟IO或網絡IO。一個進程使用fork創建子進程,如果子進程退出,而父進程並沒有調用wait或waitpid獲取子進程的狀態信息,那么子進程的進程描述符仍然保存在系統中,這種進程稱之為殭屍進程。大量殭屍進程可能會佔用進程描述符空間導致無法創建進程。
  • Java進行內存洩露 GC 分析都有哪些常用好用的工具
    jmap: 查看某個Java進程的堆內存使用情況 jvisualvm:可視化查看堆內存與metaspace佔用情況 jstack:查看具體某個java進行的線程堆棧情況
  • 手機內存不夠用?兩個「不可缺少」的軟體佔用了20G 教你如何解決
    清除軟體數據這種方法操作起來是最便利的,只需要打開手機內部的設置頁面,點擊「應用管理」,找到QQ和微信之後再點擊清除數據,之後選擇是「清除緩存」還是「清除全部數據」就可以了,選擇前者是清理APP佔用臨時緩存,後者則是將軟體
  • 512M內存老用戶升級難題:組雙通道還是加容量?
    我們分析在Business Winstone 2004和MCC Winstone 2004兩項密集操作的合成類測試軟體裡面, 512M和1G兩套系統內存佔用情況大不一樣。由於WIndowsXP系統本身佔用一定的系統內存,512M的系統內存並不能應付對內存容量耗用較大的Business Winstone 2004和MCC Winstone 2004。
  • 怎麼查看佔 cpu 最多的線程
    本文轉載自【微信公眾號:五角錢的程式設計師,ID:xianglin965】一起學習、成長、溫情的熱愛生活前言:某些線上服務,一段時間之內佔用CPU特別高,如何確認這是否屬於正常情況還是代碼中出現了異常導致佔用CPU特高呢?如何定位確認是哪個線程導致的?
  • CPU溫度高 微星教你一招關閉超線程功能
    CPU 散熱是很多玩家極為關注的問題,溫度高了不僅對CPU性能不利,還會影響使用壽命等。如果發覺自己的CPU溫度過高了,不妨試試微星的方法——關閉沒什麼用的超線程功能。
  • 為什麼大多數6G運行內存的手機,實際可用內存卻不到3G?
    今年上市的眾多安卓手機中,無論你是性價比高的千元機,還是年度旗艦手機,6G運行內存都得到了普及,原來用4G運行內存的手機都感覺都不夠用了,那6G的運行內存就一定夠用嗎?細心的我發現,現在很多搭載6GB運行內存的手機中,當你開機後,你會發現其實系統就已經佔用了近一半的運行內存,新手機實際可用的運行內存在3GB左右,而當你安裝一些軟體後,實際上可用內存可能就像我一樣,只有1.8GB可用。
  • linux多線程之線程資源的釋放
    一般來說,對一段運行代碼進行加鎖然後解鎖,如下所示:pthread_mutex_lock(&mutex);//運行代碼;pthread_mutex_unlock(&mutex);如果在運行代碼這塊發生錯誤,有異常,導致這個線程異常退出,那麼怎麼辦,pthread_unlock
  • 谷歌修復 Win10 Chrome 惱人內存佔用和崩潰問題
    「Segment Heap」也被引入 Chromium 開源項目,谷歌計劃用這個功能來修復臭名昭著的 Chrome 瀏覽器的內存佔用問題。根據一份文件顯示,PartitionAlloc-Everywhere 有其自身的好處:減少了 Chrome 瀏覽器中的內存使用、分配器和提高了安全性。 IT之家獲悉,這使得谷歌 Chrome 更有效地管理內存,從而降低內存的使用量,同時它也將減少潛在的安全問題。
  • 谷歌計劃用Segment Heap修復Chrome瀏覽器內存佔用問題
    「Segment Heap」也被引入 Chromium 開源項目,谷歌計劃用這個功能來修復臭名昭著的 Chrome 瀏覽器的內存佔用問題。根據一份文件顯示,PartitionAlloc-Everywhere 有其自身的好處:減少了 Chrome 瀏覽器中的內存使用、分配器和提高了安全性。   這使得谷歌 Chrome 更有效地管理內存,從而降低內存的使用量,同時它也將減少潛在的安全問題。   谷歌 Chrome 瀏覽器的安全性將使用 「MiraclePtr」來提高,這需要 PartitionAlloc。