現代 JVM 的垃圾回收裡面還有「引用計數」嗎?

如果有引用計數,那它和其他垃圾回收演算法之間如何協同的?java中大部分垃圾是被引用=0回收掉的呢?還是各種標記掃描出來的?還是標記掃描只主要是解決循環引用,其他的都是引用歸零回收了?如果沒有引用計數,那麼 jni.h裡面:

const jchar *(JNICALL *GetStringChars) (JNIEnv *env, jstring str, jboolean *isCopy);
void (JNICALL *ReleaseStringChars) (JNIEnv *env, jstring str, const jchar *chars);

如果返回 true,那著兩行可能是內存拷貝和釋放實現的。而如果返回 isCopy = false,同時用完還需要 ReleaseStringChars,這樣做是為了防止外部 C模塊在訪問 jvm內部對象的時候對象被回收?還是其他?它內部是引用來實現的么?

如果 JVM完全不用引用計數了,那什麼時候 isCopy會等於 false呢?如果用了引用計數,那為啥還要 Copy出來降低性能呢?這個 isCopy參數留在這裡,有什麼考慮呢?

所有主流 JVM實現都是統一的么?如果java架構比較大型項目,一個對象不再需要使用的時候,人為手工的接除改對象對外部的各種引用是否對垃圾回收有所幫助?還是由於沒有引用計數了 java層也用不著去刻意解除引用,後期的標記掃描會自動找出來?


首先,最初設計JVM的時候就從來沒有假設過要用引用計數來實現JVM管理Java對象的的GC。JNI的設計自然也不會假設引用計數。

Sun -&> Oracle所實現的JVM沒有一個是用引用計數來實現JVM的GC的。其它能稱得上「主流」的JVM也都無一使用過引用計數來實現GC。

請看傳送門:目前主流的 Java 虛擬機有哪些? - RednaxelaFX 的回答 其中只有研究專用的Jikes RVM在某些研究項目里有過引用計數的實現。

其次,JNI里的GetStringChars與ReleaseStringChars是幹嘛的。

GetStringChars

const jchar * GetStringChars(JNIEnv *env, jstring string,
jboolean *isCopy);

Returns a pointer to the array of Unicode characters of the string. This pointer is valid until ReleaseStringchars() is called.

If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.

LINKAGE:Index 165 in the JNIEnv interface function table.PARAMETERS:

env: the JNI interface pointer.

string: a Java string object.

isCopy: a pointer to a boolean.

RETURNS:

Returns a pointer to a Unicode string, or NULL if the operation fails.

ReleaseStringChars

void ReleaseStringChars(JNIEnv *env, jstring string,
const jchar *chars);

Informs the VM that the native code no longer needs access to chars. The chars argument is a pointer obtained from string using GetStringChars().

LINKAGE: Index 166 in the JNIEnv interface function table.PARAMETERS:

env: the JNI interface pointer.

string: a Java string object.

chars: a pointer to a Unicode string.

GetStringChars()參數里的isCopy是一個「傳出參數」(out parameter),用於告知調用者JVM在執行這個函數時是否有對字元串內容進行拷貝。

然後看看Oracle JDK8u的HotSpot VM里GetStringChars()的實現:

jdk8u/jdk8u/hotspot: 3fa5c654c143 src/share/vm/prims/jni.cpp

JNI_QUICK_ENTRY(const jchar*, jni_GetStringChars(
JNIEnv *env, jstring string, jboolean *isCopy))
JNIWrapper("GetStringChars");

jchar* buf = NULL;
oop s = JNIHandles::resolve_non_null(string);
typeArrayOop s_value = java_lang_String::value(s);
if (s_value != NULL) {
int s_len = java_lang_String::length(s);
int s_offset = java_lang_String::offset(s);
buf = NEW_C_HEAP_ARRAY_RETURN_NULL(jchar, s_len + 1, mtInternal); // add one for zero termination
/* JNI Specification states return NULL on OOM */
if (buf != NULL) {
if (s_len &> 0) {
memcpy(buf, s_value-&>char_at_addr(s_offset), sizeof(jchar)*s_len);
}
buf[s_len] = 0;
//%note jni_5
if (isCopy != NULL) {
*isCopy = JNI_TRUE;
}
}
}
return buf;
JNI_END

也就是說,HotSpot VM在這個函數里,只要看到非null的字元串都會做拷貝,所以正常情況下isCopy傳出的結果總是JNI_TRUE。只有兩種情況它會是JNI_FALSE:

  • 傳入的string為NULL,或
  • 無法申請到足夠內存空間來做拷貝。此時主返回值會是NULL。

至於ReleaseStringChars(),它就是把GetStringChars()所malloc()出來的buffer空間給釋放掉而已:

JNI_QUICK_ENTRY(void, jni_ReleaseStringChars(JNIEnv *env, jstring str, const jchar *chars))
JNIWrapper("ReleaseStringChars");

//%note jni_6
if (chars != NULL) {
// Since String objects are supposed to be immutable, don"t copy any
// new data back. A bad user will have to go after the char array.
FreeHeap((void*) chars);
}
JNI_END

很多其它JVM都是如此實現這兩個函數的。也就是說在這些JVM上,外部代碼都不會看到GetStringChars()返回非NULL值時isCopy為JNI_FALSE。

連JRockit在此處的行為都是跟HotSpot VM一樣的。

不過,同一系列的另一對函數則與此正好相反:GetStringCritical() / ReleaseStringCritical()。

同樣有isCopy的傳出參數,這裡GetStringCritical()會儘可能返回JNI_FALSE。

GetStringCritical

ReleaseStringCritical

const jchar * GetStringCritical(JNIEnv *env, jstring string, jboolean *isCopy);

void ReleaseStringCritical(JNIEnv *env, jstring string, const jchar *carray);

The semantics of these two functions are similar to the existing Get/ReleaseStringChars functions. If possible, the VM returns a pointer to string elements; otherwise, a copy is made. However, there are significant restrictions on how these functions can be used. In a code segment enclosed by Get/ReleaseStringCritical calls, the native code must not issue arbitrary JNI calls, or cause the current thread to block.

The restrictions on Get/ReleaseStringCritical are similar to those on Get/ReleasePrimitiveArrayCritical.

LINKAGE (GetStringCritical):Index 224 in the JNIEnv interface function table.LINKAGE (ReleaseStingCritical):Index 225 in the JNIEnv interface function table.SINCE:

JDK/JRE 1.2

GetStringCritical()的實現方式在各JVM里也不一樣。HotSpot VM的話會通過阻止GC發生來保證對象不發生移動;JRockit VM的話,默認是可以對對象單獨做pinning,即便執行GC也可以保持特定對象不被移動。

=================================================================

題主要是對某些其它JVM實現在這部分的細節感興趣的話,回頭可以補充點信息到這個回答里。

這個isCopy參數自然有它的黑歷史…只是跟題主所想像的不一樣。

=================================================================

至於題主問:

所有主流 JVM實現都是統一的么?

嗯那當然不是咯。每個JVM實現都有各自不同的取捨,就算同樣的演算法在具體實現中都會有不少差異。

如果Java架構比較大型項目,一個對象不再需要使用的時候,人為手工的解除該對象對外部的各種引用是否對垃圾回收有所幫助?

跟大不大型沒關係。只跟具體用法有關係。

通常,人工解除Java對象之間的引用關係是沒有必要的。GC的工作就是把沒有活引用的對象都收集掉。

但有些時候出問題是應該要解除引用但卻沒有解除的。典型情況是在復用線程池裡的線程的情況下忘記清理ThreadLocal的內容——線程池裡的線程可能被複用於多種任務,而每個任務各自使用各自的ThreadLocal變數。殊不知ThreadLocal的生命期不是跟任務綁定,而是跟線程綁定的,如果這種使用場景里在任務結束時不清理自己的ThreadLocal,就可能導致意外的內存泄漏。

另外一些情況,例如說Java對象持有native資源,這不是GC管轄的範疇,自然要程序員自己解決。Finalizer(或例如PhantomReference-based方案)是讓清理動作與GC之間建立聯繫的辦法。這又是另一個話題了。請看傳送門:

Java使用JNI調用C寫的庫時,使用malloc分配的內存是由誰來管理? - RednaxelaFX 的回答

為什麼Java有GC還需要自己來關閉某些資源? - RednaxelaFX 的回答

最後補充一點:JVM雖然不用引用計數方式實現對Java對象的管理,但JVM內部有些數據結構還是有可能用引用計數的。例如說從JDK7開始HotSpot VM里「Symbol」就是通過引用計數來管理的,而它適用的原因很簡單:Symbol只是一個被intern了的簡單的UTF-8字元串,它們之間無法產生相互引用,因而不會有循環引用,這就避開了引用計數最大的弱點。


請忘了引用計數吧!請忘了引用計數吧!請忘了引用計數吧!重要事情說三遍

判斷alive與否就是樹遍歷,不要問我是深度優先,還是廣度優先!!!都有,具體得查查code。


現在Java GC基於代實現,不用計數了。一般不用手工置null,除非在同一個scope內需要GC ,比如一個方法內分多次new 了好幾個大數組,在前面的數組用完後置null有助於GC 了解這個數組已經沒有引用可以回收了。

當然實際應用中這種情況很少,一般等方法結束後引用自動消失比較自然


當前Java GC沒有使用引用計數的方法,並且以前的java gc仍然不使用,標記階段都用的是可達性分析方法。在jvm 中,引用計數法是一種很簡單的標記方法,也是一種不適用的標記方法(因為不能解決循環引用的問題),之所以有的書上提出引用計數法,目的在於讓我們對jvm 的堆內存分配又一個清晰的認識!不要搞混!


引用計數演算法無法實現垃圾判斷,一個對象被引用次數為0,這肯定是垃圾,但是引用次數不為0時,不一定不是垃圾。比如n個互相引用的死對象(孤島)。

所以引用技術演算法不可能用於jvm垃圾回收機制。真正的演算法是可達性分析。關於可達性分析演算法可以谷歌一下。


推薦閱讀:

如何評價 2015 年 CCPC ?
編程越來越平民化傻瓜化,這對於IT行業是否有影響?
知乎上那些自學計算機的人們,你們都有什麼學習計劃和未來規劃?
為什麼processing坐標系的原點在左上角?

TAG:編程 | Java | Java虛擬機JVM | JavaNativeInterface | GC垃圾回收計算機科學 |