1. 概況
程序運行操作系統: CentOS6.5 64bit
JDK版本:7
2. 測試
2.1 準備測試程序
測試程序很簡單,就一個類一個main函數,大概流程:
先從參數中讀取 獲取zip文件的時間間隔interval,再從參數中獲取zip文件路徑。再通過ZipFile類的api來從zip文件中獲取文件的全路徑名。每次獲取一個文件sleep interval時間,便於測試。
代碼如下:
/**
* Usage: App <interval in ms to get entry> <zip file path>
* @param args
*/
public static void main( String[] args ) {
String arg0 = args[0];
String arg1 = args[1];
Long interval = Long.valueOf(arg0);
System.out.println("interval = " + interval);
String filename = arg1;
System.out.println("filename = " + filename);
ZipFile zipFile = null;
try {
zipFile = new ZipFile(filename);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
System.out.println(entry.getName());
try {
Thread.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipFile != null) {
try {
zipFile.close();
} catch (IOException e) {
}
}
}
}
2.2 開始測試
先在/tmp目錄下放置一個用於測試的test.zip的壓縮文件
將程序打包到服務器,執行如下命令:
java -classpath $CLASSPATH com.spiro.test.App 5000 /tmp/test.zip > $LOG_HOME/app.log
app.log輸出:
interval = 5000
filename = /tmp/test.zip
frontend/
frontend/gulpfile.js
frontend/node_modules/
frontend/node_modules/.bin/
frontend/node_modules/.bin/browser-sync
frontend/node_modules/.bin/browser-sync.cmd
frontend/node_modules/.bin/gulp
緊接着重新開一個終端執行如下命令:
echo "abcd" > /tmp/test.zip
將test.zip文件的內容重置修改
app.log則輸出:
interval = 5000
filename = /tmp/test.zip
frontend/
frontend/gulpfile.js
frontend/node_modules/
frontend/node_modules/.bin/
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGBUS (0x7) at pc=0x00007fa6701cb7b2, pid=7262, tid=140352832034560
#
# JRE version: OpenJDK Runtime Environment (7.0_141-b02) (build 1.7.0_141-mockbuild_2017_05_09_14_20-b00)
# Java VM: OpenJDK 64-Bit Server VM (24.141-b02 mixed mode linux-amd64 compressed oops)
# Derivative: IcedTea 2.6.10
# Distribution: CentOS release 6.9 (Final), package rhel-2.6.10.1.el6_9-x86_64 u141-b02
# Problematic frame:
# C [libzip.so+0x47b2]
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /tmp/jvm-7262/hs_error.log
#
# If you would like to submit a bug report, please include
# instructions on how to reproduce the bug and visit:
# http://icedtea.classpath.org/bugzilla
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
jps查看進程已經消失。
根據日誌提示,jvm dump文件輸出到文件/tmp/jvm-7262/hs_error.log,
查看棧:
Stack: [0x00007fa670a25000,0x00007fa670b26000], sp=0x00007fa670b24650, free space=1021k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [libzip.so+0x47b2]
C [libzip.so+0x4dc8] ZIP_GetNextEntry+0x48
j java.util.zip.ZipFile.getNextEntry(JI)J+0
j java.util.zip.ZipFile.access$500(JI)J+2
j java.util.zip.ZipFile$1.nextElement()Ljava/util/zip/ZipEntry;+54
j java.util.zip.ZipFile$1.nextElement()Ljava/lang/Object;+1
j com.spiro.test.App.main([Ljava/lang/String;)V+100
v ~StubRoutines::call_stub
V [libjvm.so+0x60ea4e]
V [libjvm.so+0x60d5e8]
V [libjvm.so+0x61e9c7]
V [libjvm.so+0x632fac]
C [libjli.so+0x34c5]
可以看到java代碼定位在
private static native long getNextEntry(long jzfile, int i);
至此重現了問題。
2.3. 問題解釋
通過查詢資料,這個跟mmap的linux操作系統機制有關,大致意識是:mmap機制通過將文件映射到內存,這樣可以提高文件的訪問效率,但是一旦來讀取的過程中,文件被修改了,就可能導致錯誤,從而導致jvm crash。
https://bugs.openjdk.java.net/browse/JDK-8160933
網上給的解決方案中有通過在jvm參數中加入 -Dsun.zip.disableMemoryMapping=true選項,禁用mmap機制,我們下面就來看看加上這個選項的效果;
2.4. 禁用mmap測試
執行:
java -Dsun.zip.disableMemoryMapping=true
-classpath $CLASSPATH com.spiro.test.App 5000 /tmp/test.zip > $LOG_HOME/app.log
app.log輸出:
frontend/
frontend/gulpfile.js
frontend/node_modules/
frontend/node_modules/.bin/
frontend/node_modules/.bin/browser-sync
frontend/node_modules/.bin/browser-sync.cmd
frontend/node_modules/.bin/gulp
執行:
echo "abcd" > test.zip
發現進程會繼續執行,並在一段時間後會拋Error異常:
Exception in thread "main" java.util.zip.ZipError: jzentry == 0,
jzfile = 140689976146736,
total = 8313,
name = /tmp/test.zip,
i = 62,
message = null
at java.util.zip.ZipFile$1.nextElement(ZipFile.java:505)
at java.util.zip.ZipFile$1.nextElement(ZipFile.java:483)
at com.spiro.test.App.main(App.java:38)
2.5 再次解釋
在禁用了mmap後,進程沒有crash,而是在一段時間後拋了異常,然後退出進程。
禁用mmap後,文件沒有映射到內存,而是程序預先加載一部分數據到內存後繼續讀取,後文件數據變化後,才發生異常錯誤。這還只是猜測,後續有空再繼續研究。
3. 總結
可以看到jvm crash的根源就在開啓mmap機制後,zip文件在讀取過程中被修改了。
解決的方法有兩種:
1. 從代碼邏輯上控制zip文件在操作過程中,不要被其他邏輯修改。
2. 在jvm啓動參數中加入-Dsun.zip.disableMemoryMapping=true 。
但是個人覺得2這種方式指標不治本,問題根源還在於文件資源共享訪問時要做控制。