Java JNI GC Thread Error (EINVAL)

Keywords: JNI, Java, GC Thread, add_workers, static library, Thread Error

Solution: -XX:+AdjustStackSizeForTLS from JDK 14

Description

We cannot launch GC thread of JVM from JNI. And, it shows

1
[os,thread] Failed to start thread - pthread_create failed (EINVAL) for attributes: stacksize: 1024k, guardsize: 0k, detached.

The main reason is the glibc bugs for native C code issues. For example, we allocate thread-local variables by static library linking.

1
2
3
__thread int buf[1048576];
thread_local int buf[1048576];
#pragma omp threadprive(buf);

And then, glibc 2.12 will move them on stack, and require minimum stack size at least 1024k bytes. But, the default configuration JVM of JDK11 will minimum stacksize to be 1024k bytes as default in Linux OS. Therefore, the pthread_create throws the error EINVAL for specific stacksize setting.

Options

https://www.oracle.com/java/technologies/javase/14all-relnotes.html

The glibc library allocates some thread-local storage (TLS) in the stack of a newly created thread, leaving less stack than requested for the thread to do its work. This is particularly a problem for threads with small stack sizes. It is an inherited issue from a well-known glibc problem, ‘Program with large TLS segments fail’ [0] and has been observed in Java applications. In one of the reported JVM failure instances, the issue manifests as a StackOverflowError on the process reaper thread, which has a small stack size. The java.lang.Thread constructor enables users to specify the stack size for a new thread. The created thread may encounter the TLS problem when the specified size is too small to accommodate the on-stack TLS blocks.

In JDK 8, a system property, jdk.lang.processReaperUseDefaultStackSize, was introduced to address the TLS issue only for reaper threads. Setting the property gives a bigger stack size to the reaper threads.

To address the issue for all threads, a general purpose workaround was implemented in Java which adjusts thread stack size for TLS. It can be enabled by using the AdjustStackSizeForTLS command-line option:

When creating a new thread, if AdjustStackSizeForTLS is true, the static TLS area size is added to the user requested stack size. AdjustStackSizeForTLS is disabled by default.

Conclusion

整合進大型程式時,要多觀察靜態編譯的部分,如果有一些 TLS 的宣告,則會增加其他預設執行緒的最小使用堆疊大小。而 JVM 又傾向使用最少資源去完成,則很容易在計算最小資源時發生問題。

如果去查閱 JDK15 的 代碼 os_linux.cpp 時,有一段到動態鏈結函數庫 glibc 的詢問最低需求來解決此問題。這一段牽涉到 hack 技巧,偷偷利用 symbol 爬出私有函數,不算正常操作行為。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// On Linux, glibc places static TLS blocks (for __thread variables) on
// the thread stack. This decreases the stack size actually available
// to threads.
//
// For large static TLS sizes, this may cause threads to malfunction due
// to insufficient stack space. This is a well-known issue in glibc:
// http://sourceware.org/bugzilla/show_bug.cgi?id=11787.
//
// As a workaround, we call a private but assumed-stable glibc function,
// __pthread_get_minstack() to obtain the minstack size and derive the
// static TLS size from it. We then increase the user requested stack
// size by this TLS size.
//
// Due to compatibility concerns, this size adjustment is opt-in and
// controlled via AdjustStackSizeForTLS.
typedef size_t (*GetMinStack)(const pthread_attr_t *attr);
GetMinStack _get_minstack_func = NULL;
static void get_minstack_init() {
_get_minstack_func =
(GetMinStack)dlsym(RTLD_DEFAULT, "__pthread_get_minstack");
log_info(os, thread)("Lookup of __pthread_get_minstack %s",
_get_minstack_func == NULL ? "failed" : "succeeded");
}
// Returns the size of the static TLS area glibc puts on thread stacks.
// The value is cached on first use, which occurs when the first thread
// is created during VM initialization.
static size_t get_static_tls_area_size(const pthread_attr_t *attr) {
size_t tls_size = 0;
if (_get_minstack_func != NULL) {
// Obtain the pthread minstack size by calling __pthread_get_minstack.
size_t minstack_size = _get_minstack_func(attr);
// Remove non-TLS area size included in minstack size returned
// by __pthread_get_minstack() to get the static TLS size.
// In glibc before 2.27, minstack size includes guard_size.
// In glibc 2.27 and later, guard_size is automatically added
// to the stack size by pthread_create and is no longer included
// in minstack size. In both cases, the guard_size is taken into
// account, so there is no need to adjust the result for that.
//
// Although __pthread_get_minstack() is a private glibc function,
// it is expected to have a stable behavior across future glibc
// versions while glibc still allocates the static TLS blocks off
// the stack. Following is glibc 2.28 __pthread_get_minstack():
//
// size_t
// __pthread_get_minstack (const pthread_attr_t *attr)
// {
// return GLRO(dl_pagesize) + __static_tls_size + PTHREAD_STACK_MIN;
// }
//
//
// The following 'minstack_size > os::vm_page_size() + PTHREAD_STACK_MIN'
// if check is done for precaution.
if (minstack_size > (size_t)os::vm_page_size() + PTHREAD_STACK_MIN) {
tls_size = minstack_size - os::vm_page_size() - PTHREAD_STACK_MIN;
}
}
log_info(os, thread)("Stack size adjustment for TLS is " SIZE_FORMAT,
tls_size);
return tls_size;
}
Read More +

Java 優化內存使用 組合篇

整數映射 (Integer Mapping)

算法經常會使用整數映射到物件,而整數範圍就會影響到內存。為了技術性能 (capability),直接宣告

1
2
Map<Long, Object> a;
TLongObjectMap<Object> b;

以應付未來變化。考慮到鍵值欄位,實作上建立 $\mathcal{O}(n)$ long[]。在初始狀態下,大多情境只使用 int[] 的情況。為此,我們考慮分流使用

1
2
3
4
class MMap<T> {
TLongObjectMap<T> longMap;
TIntObjectMap<T> intMap;
}

使用這樣子的概念,通用狀態可以降低 $50\%$ 內存,均勻分布情況則可以降低 $25\%$

陣列替代 (Array Substitute)

前一篇,我們提到了將 ArrayList 換成最原生的 T[]。更進一步,以前寫 C 的時候,壓根不在意 T[].length 這一個需求,因為題目已經寫死在某個全區變數裡。可在 Java 裡頭,我們宣告不出純粹的陣列,只能硬生生多出 $4 \; \text{bytes}$

1
2
3
4
class Scheme {
K[] keys; // as known, keys.length = 10
V[] vals; // as known, vals.length = 10
}

換個方向思考,那朝著減少試試。進行陣列雙向合併

1
2
3
class Scheme {
Object[] keyval; // [key0, key1, ..., val1, val0]
}

Scheme 物件中,我們減少了 $8 \; \text{bytes}$ 的額外資訊。但在使用時, casting 是個開銷。

欄位轉移 (Field Translation)

1
2
3
Map<T, String> map;
map.put(a, "MARKED");
map.put(b, "UNMARKED");

當我們還沒有仔細規劃結構時,直接用 has-A 的精神去寫第一步。回過頭來,就會造成不少零散的映射出現。如果映射使用雜湊實作,而不是樹的話,整體的記憶體效率會低個 $33\%$,因為無法塞滿所有的 hash table 中的桶子。

1
2
3
4
class T {
...
Object userData;
}

那為了提升記憶體效率,先檢查是不是所有的項目,幾乎都在這個映射裡面。如果是的話,將這個欄位轉移到目標類別上。看起來很不美觀,但進一步的資料佈局是很可觀的,甚至可以比雜湊 $\mathcal{O}(1)$ 更快的存取。

卸載策略 (Offloading Strategy)

在平行處理中,我們可以劃分成不同數量級的策略。在小規模下,嘗試先以 CPU 計算,若發現更大規模,將資料卸載給其他異質計算單元來完成。同樣的道理,對於資料結構也能更近一步地劃分。

如集合實作:

1
2
3
class MSet {
Object mRaw; // size < 8: LinkedList; otherwise, HashSet
}

由於 Java 中的 HashSet<T> 並不是單一欄位的實作,實值為 HashMap<T, T>,相較於 C/C++ 的狀態,空間多了兩、三成。我們可以簡單使用 LinkedList 取代 HashSet,並且避免小規模數據中未能填滿 hash table 的狀態。在元素個數為一時,串列的記憶體效率 $100 \%$,但雜湊集合只有 $12.5 \%$。換句話說,有時候數據分布很極端,內存用量就差了十倍。

Read More +

Java 優化內存使用 結構篇

概要

首先,針對 JVM 這一類的語言設計,所有事物皆為物件,那麼就表示每一個物件都必須額外紀錄 object header,也就是說一個物件在 64 位元的作業系統環境下,通常會額外帶有 12 bytes 資料 (mark word:4 bytes 和 klass word:8 bytes),但 64 位元計算機架構需對齊 8 bytes 的規範,使得每一個物件都大上許多。

例如,一個 struct Point 在 C 語言裡面,

1
2
3
4
struct Point {
long x; // 8 byte
long y; // 8 byte
}

我們輕易地就明白單一物件大小為 16 bytes,而在 Java 撰寫時,卻會變成 $32 \; \text{bytes} \;(\text{header:}12 + \text{x:}8 + \text{y:}8 + \text{padding:}4)$

1
2
3
4
5
class Point { // object header 12 byte
long x; // 8 byte
long y; // 8 byte
// padding 4 byte
}

這樣看起來並不妙。相同的演算法、配上相同的數據,Java 處理時的內容硬是比 C 還多出整整兩倍。物件導向有其優點,必然存在一些缺點,可以很彈性地開發、豐富的權限管理、方便追蹤剖析,卻容易在非常基礎的大量計算中暴露記憶體方面的問題。

打包物件 (packed object)

對於基礎物件,就只能倚靠撰寫 JNI 或者修改 JVM 的參數,來降低每一個單一物件的大小。對於底層不夠熟悉的開發者而言,還是可以透過一些實作方法來完成。但必須犧牲一點物件導向的方法論,喪失維護性,面對真實問題。

例如,我們儲存一個矩形,需要一個左上角和右下角的點。

1
2
3
4
class Rectangle {
Point a; // pointer: 8 bytes
Point b; // pointer: 8 bytes
}

資深工程師總會很直覺地 重複使用,這個原則並沒有錯,但對於大量的基礎物件而言,宣告一個矩形實際上會占用 $32 + 2 \times 32 = 96 \; \text{bytes}$。接著,老是有人跟我唱反調說「效能分析工具 (Profiler) 只給出 $32 \; \text{bytes}$,你肯定算錯了!」別忘了,你寫的不是 C/C++,是一切皆為指標的 Java。

那我們要怎麼打包物件呢?或者說 攤平,目標要減少 Point 多出來的 object header。

1
2
3
4
5
6
class Rectangle {
long lx;
long ly;
long rx;
long ry;
}

返璞歸真,直接將欄位往上提取,破壞一點基礎建設,在一些 API 上犧牲一點效能。這樣就變成了 $48 \; \text{bytes} = 12 + 8 \times 4 + 4 \; \text{bytes}$ 的物件。如此一來,減少了一半的內存用量。

原始型別 (Primitive Type)

在很多不經意的操作下,很容易將原始型別 (primitive type) 儲存成物件型別 (object type),也就是物件導向最容易造成的問題,從整體用法來分析,並不影響程式正確性,但會造成記憶體用量飆高。如 long 原本為 8-byte 物件,轉換成 Long 會變成 24-byte 物件。

繼承與泛型影響

1
class Version extends Pair<Long, Long> {}

當想重複使用類別,透過繼承的泛型很容易自動轉換成物件型別。這個問題在 C# 內並不存在,CIL 能允許宣告 Pair<long, long>,並且不透過型別抹除,分別建立相應的代碼,能解決此問題。在 Java 上要解決這個問題,只能更樸素一點。

1
2
3
4
class Version {
long major;
long minor;
}

代碼多了一點,取而代之的是記憶體量減少,物件轉換次數的降低。

多形使用

1
2
3
4
List<Object> list;
list.add(1);
Map<Object, Object> map;
map.put(2, 3);

多形操作下,將好幾種不同類別的物件一起使用,隱式轉換問題就會發生,一般要追到底層才知道最後發生什麼事情。由於 Java 官方的庫並沒有預設大量的原生型別的資料結構,我們只能透過像 trove4j、eclipse collections 等插件來補足,必要時自己刻一個。

1
2
3
TLongObjectMap<T> a;
TLongArrayList b;
...

資料結構 (Data Structure)

雜湊表 (Hash Table)

在大多數的實作下,雜湊表佔據線性空間 $\mathcal{O}(n)$,存取時間 $\mathcal{O}(1)$,理論分析上也近乎完美。在處理碰撞的技巧上,影響常數級別的空間用量。通常分成以下兩種

開放定址法 (Open Addressing)

trove4j 和 eclipse collections 採用此方法。

  • 使用記憶體空間最小
    僅有兩個陣列 key[]value[],沒有多餘的 object header。
  • 存取常數大,調整時間較長
    通常會有兩個以上的 probing 操作,中間穿插刪除操作時,容易造成退化。

串鏈法 (Chaining)

jdk 預設採用此方法。

  • 使用記憶體空間最大
    HashNode {val, next, prev} 佔據了大部分的記憶體,且大量的 object header 出現。
  • 存取常數較小

從穩定性分析上,串鏈法在某種程度上可以複合許多結構,如 unrolled linked list (bucket) 或者是 binary tree 來降低最慘複雜度。而開放定址法,雖然有記憶體用量小,快取能力更高,但數據量一大,很容易在 rehash 的階段花太久的時間,而且不容易做到刪除操作。

固定長度陣列 (Fixed-length Array)

Java 開發久了,連宣告陣列都很吃技術。有時候,並沒仔細探討固定長度與不固定長度的差異。劈頭就寫一個

1
2
3
class Scheme {
ArrayList<Field> fields;
}

這樣的設計擴充性佳,具有彈性。在底層設計時,卻會發現 ArrayList 至少包含了 sizeval[] 兩個欄位,而 val[] 包含了真正的 array pointerlength 兩個欄位。如果已知固定長度且不再變更,或者變更的使用量極低,且能保證 size == val.length,那不如直接宣告

1
2
3
class Scheme {
Field[] fields;
}

如此一來,一個 Scheme 物件便能減少 $16 \; \text{bytes}$。更進一步,我們可以利用封裝 (Encapsulation) 和多型技術,再降低 $8 \; \text{bytes}$

1
2
3
4
5
6
7
8
class Scheme1 {
Field f1;
}
class Scheme2 {
Field f1;
Field f2;
}
...

結語

根據實際應用的數據分布,對最常用的物件進行優化。醜了點,但能解決問題。雖然並不是黑魔法,更能了解實作。

Read More +

Java JNI Thread Error (EINVAL)

Keywords: JNI, Java, ProcessBuilder, Thread Error

Error message: [os,thread] Failed to start thread - pthread_create failed (EINVAL) for attributes: stacksize: 136k, guardsize: 0k, detached.

Solution: -Djdk.lang.processReaperUseDefaultStackSize=true

Description

From JNI process, it is fine to execute Java internal multi-threading, but can not use Runetime.exec or ProcessBuilder to do anything. Even through adjust java stack-related option

1
setenv _JAVA_OPTIONS "-ea -Xlog -XX:ThreadStackSize=31744 -XX:VMThreadStackSize=1024 -XX:CompilerThreadStackSize=1024"

cgroup (Linux control group) limition, and set_rlimit. Same error message will be displayed.

When you link large C/C++ program with large thread local storage (TLS), default minimum stacksize computation is not enough to create pthread in OS-level. Only to set -Djdk.lang.processReaperUseDefaultStackSize=true to apply default pthread arguments. Then, it will be working for you.

問題描述

無法透過 JNI 啟動的 JVM 運行外部程序,如 Runtime.exec 或者是 Process 都無法使用,卻可以使用內部執行緒,甚至達到了數百個執行緒。

首先,JVM 內部拆分了好幾種不同的執行緒使用策略,也有不同的資源需求判斷,去挖原始碼的時候會追到底層的 C 部分,就可以看出這些問題。但是錯誤訊息仍然不足,到了 JDK15 後,才能透過 -Xlog dump 更多的配置訊息,但有些配置訊息的翻譯仍然有錯,還是得看原始碼去得知實際情況。

追了好幾天,調適了許多不同的 stack 配置都沒有起色。自己寫的小短碼 JNI 都沒有事情,架在產品上就突然運行失敗。因此,只能去找有沒有現有的產品開發或使用上發生類似的問題,不該去找任何的教學文件。最後挖到幾篇安裝不同版本的產品失敗,據說更改 JDK 版本就能解決,然後有人分析說因為 TLS 的大小變動才導致新版本突然無法工作,才明白果然是 JNI 上的問題。

實作 JNI 介面的時候,若連接的 C++ 程式中存在大量的 thread_local 變數時,那麼在每一個執行緒中都必須給予相對應的大小進行初始化。而對於 JVM 內部會透過一些預設 stack 配置進行最小化,透過 JNI 連接的時候,便會受到影響。

透過 -Djdk.lang.processReaperUseDefaultStackSize=true 取消最小化 stack 配置,直接使用預設的構造行為,這樣就能暫時解決 JNI 啟動的 JVM 無法運行外部程序的問題。很明顯地,每一條執行緒都會開相當大的 stack size,大部分的情況下預設最多為 4 MB,而實際工作的產品中預設就要 20 MB,這個得從其關聯函數庫去分析為什麼需要這麼大的堆疊,可能是寫錯,也或者是連結太多沒有必要的庫進來。

Read More +

為什麼不推薦用 Java Stream 改寫迴圈

Java 8 開始推出了 stream 介面,搭配 lambda 可以達到函數式編程 (functional language) 的能力,但是也造成非常多的效能爆炸的憾事。尤其是在我們沒有深入了解官方實作的細節時,請別輕易地轉換到 stream 介面。

Stream

Stream 出現之前,我們可以透過 Iterator 的實作來達到串流的概念,如 guava 庫所提供的 FluentIterable 就是一個替代方案。公司內部則是因為維護問題自己實作了一個自己的庫,因此沒辦法輕易地用 guava 的項目。但其中一點很明顯的不同在於 parallel 並行特性,即使是 FluentIterable 中,我也未見相關的平行操作。

在自己實作庫、人手不足的團隊配置下,直接吹捧去使用現有的函數庫,那麼問題就容易出現在於轉換上。必須考慮「何時我們該去使用」,決定好使用場景,再決定是否要改寫。公司已經升級到了 JDK 11 的,在銜接官方的 stream 實作時,依舊造成顯著的效能退化,甚至還有嚴重的運行問題,導致 dead lock 等問題。

Static Initialization Block

1
2
3
4
5
class classA {
static {
getXXX().parallelStream().forEach(e -> doSomething(e));
}
}

在 JDK 11 中,可能會無知地不小心在 static initialization block 中觸發 parallel 特性,此時會造成程式無法運行、卡住。這個問題在於 threading 之間調度時的鎖,另一部分則是 lambda 抓取變數造成的。目前在 bug JDK-8143380 回報系統上看出無意解決此問題,因為這是人為撰寫的問題。如果是呼叫大量的函數來初始化參數,很容易遇到不知道哪個函數區塊內偷偷開了平行計算。

像同事都把 stream() 都改寫成 parallelStream(),不管問題是大是小,這樣改寫有時候就造成莫名其妙的 Bug 發生,造成找了半天才發現問題。如果覺得著官方提供的項目都有公信力,而且不會做很蠢的事情,一直都可以很聰明地在平行與不平行取捨的話,那其實也不用設計 parallelStream() 給我們使用,統一一個 stream() 接口就好。

想到同事如此狂妄的想法「工程師不該考慮資源,從摩爾定律來看,硬體明年就會解決,我們要做出更好的 scalability 的寫法」,讓我整個火氣都上來,無法接受 用而不知 的習慣。在沒人力挺下,只能眼真真地看著代碼被改得亂七八糟。

Stream Cost

了解 Stream 基礎實作後,就會明白在沒有平行的狀況下,維護 stage 的狀態會是額外開銷,相較於手刻的 Iterator,很多沒有必要的狀態紀錄容易在非常簡單的迭代器中暴露。

Stream Concatentation

串流串接又是更可怕的實作技術,請參閱附錄文章 Efficient multiple-stream concatenation in Java

1
2
3
4
5
6
7
concat(concat(concat(a, b), c), d)
/ \
concat(concat(a, b), c) d
/ \
concat(a, b) c
/ \
a b

串接的實作像是二元樹。如果操作順序不當,馬上會產生一棵偏斜的二元樹,假設我們要取 a 裏頭的元素,則必須從根一路走到葉節點去。如果構造 $n$ 次操作,且每一個恰好為一個元素,蒐集所有的內容,時間複雜度為 $O(n^2)$。

因此,亂改寫遞迴構造器很容易出現問題,不如用原本的回傳值 List/Collection 把要的東西收集好,不支援惰性操作也好,舊的寫法依舊在 $O(n)$ 完成。若要支援惰性操作,使用 StreamBuilder 建立平衡的二元樹也能在 $O(n \log n)$ 最慘複雜度下完成迭代。

這個問題並不是開 parallelStream() 解決,必須從根本的複雜度去分析,

Iterator to Stream

從內建的 CollectionStream 大部分都由官方做得很好,用不著花太多的心思去思考如何實作,直接呼叫相關函數即可。如果工作需要自己的 Iterator 變成 Stream 就沒有這麼好運。必須了解什麼是 Spliterator,而 Spliterator 是怎麼實作的,會有什麼樣的介面,而它是從何支援 parallelStream 這一項特性。

假設您已經詳讀 Spliterators.java 下的所有代碼,如自動建造函數

1
static <T> Spliterator<T> spliteratorUnknownSize(Iterator<? extends T> iterator, int characteristics)

標示不確定迭代器實際運行的元素個數,好比在 stream 的 flatMap()filter() 後,無法得知下一階段的元素多寡。在這些情況下只能透過這一類函數構造。然而官方預設的行為卻造成了嚴重的效能問題,它要求建造一個 IteratorSpliterator,而為了支援平行特性,平行通常需要透過高效的拆分陣列完成工作分配,因此會呼叫 Spliterator::trySplit 不斷地拆分,從源代碼中我們可以得知在未知大小的狀況下,則建立 new Object[1024] 預處理資料結構,即使在沒有開啟 parallelStream,依舊按照相同的運行流程。

有人說在 Java 開 new Object[1024] 很快,而最慘情況迭代器就只有一個元素,甚至沒有任何的狀況下,用最蠢的實作只消耗了一個迭代器的宣告開銷,而轉換串流卻消耗了整整數千個位元組,這是個效能損耗。就算 GC 很強,也不代表它能發現這麼複雜的操作,它整整跨足了超過一個函數區塊。

工作中因為繪製處理,需要運算數億次,若每一次都宣告這麼多餘的陣列,那麼莫虛有內存損耗高達幾十 GB,GC 若要運行 10 次,畫面繪製需要多一分鐘都是有可能的,將數秒不到的計算整整拖累下去。

Lazy Evaluation

惰性計算是 stream 的主要特性之一,從另一個術語按需計算 (on-demand)。即使是官方提供的庫也有一些小 Bug 造成假的惰性計算。最常見的像是 flatMapmapfilter 混用的時候,最後使用 findFirst 操作。從代碼上,我們會預期差不多就是找到一個元素之前的所有計算,而從回報系統裡面下關鍵字去找,會發現不少多餘的計算發生。

同事老是說「我們自己的庫超慢的,用 stream 比較快。」

言下之意,只是沒有遇到官方庫的 Bug,而你看到了庫的 Bug 卻從沒打算解決。

最常見的迭代器實作,忘了 Lazy Evaluation 的寫法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyIterator<T> implements Iterator<T> {
...
@Override
public T next() {
if (!hasNext())
throw new NoSuchElementException();
T val = nextVal;
findNext(); // on-demand ?
return val;
}
@Override
public boolean hasNext() {...}
private void findNext() {
... // time complexity ?
}
}

很明顯地,透過 findNext() 我們可能只拿了第一個元素,在不需要第二個元素的狀況下,卻耗費了線性時間。

Note

還有一些問題,下一次再聊聊吧。

Reading List

Read More +