JDK源码研究Jstack,JMap,threaddump,dumpheap的原理
JDK最新bug和任務(wù)領(lǐng)取:https://bugs.openjdk.java.net/projects/JDK/issues
參加OpenJDK社區(qū):https://bugs.openjdk.java.net/projects/JDK/issues
openjdk源碼地址:
https://jdk.java.net/java-se-ri/8
https://download.java.net/openjdk/jdk8u40/ri/openjdk-8u40-src-b25-10_feb_2015.zip
如果國外網(wǎng)速不行,這里有下好放csdn上的: JDK源碼 openjdk-8u40-src-b25-10_feb_2015.zip
線上源碼:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/6e2900603bc6/
如果官網(wǎng)很慢,可以直接CSDN下載:https://download.csdn.net/download/21aspnet/11028742
JEP 0:JEP指數(shù):http://openjdk.java.net/jeps/0
Java8官方文檔總目錄? ?
Java8語言規(guī)范? ? ??Java8虛擬機規(guī)范? ? ?HotSpot虛擬機垃圾收集調(diào)整指南
Java8 API
--------------
https://jdk.java.net/java-se-ri/12
https://download.java.net/openjdk/jdk12/ri/openjdk-12+32_src.zip
----------------
Java語言和虛擬機規(guī)范[各語言總目錄]
其他jdk文檔地址:https://docs.oracle.com/en/java/javase/index.html
----
擴展閱讀:雖然是歷史資源,但是還是閃爍著智慧的
Oracle JRockit文檔? ??Oracle JRockit在線文檔? ?【有參考價值】
Oracle JRockit聯(lián)機文檔庫4.0版? ?【很有價值】
----
Java12【總目錄】
Java?教程【有參考價值】:
學習Java語言? ?異常? ?基本I/O? ??并發(fā)??泛型??反射??集合??序列化
Lambda表達式??聚合操作? ?
垃圾收集調(diào)整【重要】? ??Java虛擬機指南【很重要】? ??JRockit到HotSpot遷移指南【有參考價值】
故障排除指南【重要】
https://docs.oracle.com/en/java/javase/11/? ?和12類似
----
分析Jstack源碼
這是起點>>>?
\openjdk\jdk\src\share\classes\sun\tools目錄下
常見的jvm命令jstack? jmap? ?jps都在這里
package sun.tools.jstack;import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.io.IOException;
import java.io.InputStream;import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.AttachNotSupportedException;
import sun.tools.attach.HotSpotVirtualMachine;/** This class is the main class for the JStack utility. It parses its arguments* and decides if the command should be executed by the SA JStack tool or by* obtained the thread dump from a target process using the VM attach mechanism*/
public class JStack {public static void main(String[] args) throws Exception {if (args.length == 0) {usage(1); // no arguments}boolean useSA = false;boolean mixed = false;boolean locks = false;// Parse the options (arguments starting with "-" )int optionCount = 0;while (optionCount < args.length) {String arg = args[optionCount];if (!arg.startsWith("-")) {break;}if (arg.equals("-help") || arg.equals("-h")) {usage(0);}else if (arg.equals("-F")) {useSA = true;}else {if (arg.equals("-m")) {mixed = true;} else {if (arg.equals("-l")) {locks = true;} else {usage(1);}}}optionCount++;}// mixed stack implies SA toolif (mixed) {useSA = true;}// Next we check the parameter count. If there are two parameters// we assume core file and executable so we use SA.int paramCount = args.length - optionCount;if (paramCount == 0 || paramCount > 2) {usage(1);}if (paramCount == 2) {useSA = true;} else {// If we can't parse it as a pid then it must be debug serverif (!args[optionCount].matches("[0-9]+")) {useSA = true;}}// now execute using the SA JStack tool or the built-in thread dumperif (useSA) {// parameters (<pid> or <exe> <core>String params[] = new String[paramCount];for (int i=optionCount; i<args.length; i++ ){params[i-optionCount] = args[i];}runJStackTool(mixed, locks, params);} else {// pass -l to thread dump operation to get extra lock infoString pid = args[optionCount];String params[];if (locks) {params = new String[] { "-l" };} else {params = new String[0];}runThreadDump(pid, params);}}
根據(jù)傳入?yún)?shù)的不同,有兩種實現(xiàn)機制,一種是基于SA,一種是通過attach。
下面是jmap部分代碼下面是用的最多的:
// Invoke SA tool with the given argumentsprivate static void runTool(String option, String args[]) throws Exception {String[][] tools = {{ "-pmap", "sun.jvm.hotspot.tools.PMap" },{ "-heap", "sun.jvm.hotspot.tools.HeapSummary" },{ "-heap:format=b", "sun.jvm.hotspot.tools.HeapDumper" },{ "-histo", "sun.jvm.hotspot.tools.ObjectHistogram" },{ "-clstats", "sun.jvm.hotspot.tools.ClassLoaderStats" },{ "-finalizerinfo", "sun.jvm.hotspot.tools.FinalizerInfo" },};
-------------------?
都是通過?executeCommand?來實現(xiàn)的,例如:datadump、threaddump、dumpheap、inspectheap、jcmd等,而最終的execute()在Linux上是由類LinuxVirtualMachine來完成。
public abstract class HotSpotVirtualMachine extends VirtualMachine {
...// --- HotSpot specific methods ---// same as SIGQUITpublic void localDataDump() throws IOException {executeCommand("datadump").close();}// Remote ctrl-break. The output of the ctrl-break actions can// be read from the input stream.public InputStream remoteDataDump(Object ... args) throws IOException {return executeCommand("threaddump", args);}// Remote heap dump. The output (error message) can be read from the// returned input stream.public InputStream dumpHeap(Object ... args) throws IOException {return executeCommand("dumpheap", args);}// Heap histogram (heap inspection in HotSpot)public InputStream heapHisto(Object ... args) throws IOException {return executeCommand("inspectheap", args);}// set JVM command line flagpublic InputStream setFlag(String name, String value) throws IOException {return executeCommand("setflag", name, value);}// print command line flagpublic InputStream printFlag(String name) throws IOException {return executeCommand("printflag", name);}public InputStream executeJCmd(String command) throws IOException {return executeCommand("jcmd", command);}// -- Supporting methods
-----------------------------------
jstack命令首先會attach到目標jvm進程,產(chǎn)生VirtualMachine類;
linux系統(tǒng)下,其實現(xiàn)類為LinuxVirtualMachine,調(diào)用其remoteDataDump方法,打印堆棧信息;
VirtualMachine是如何連接到目標JVM進程的呢?
具體的實現(xiàn)邏輯在sun.tools.attach.LinuxVirtualMachine的構(gòu)造函數(shù):
// The patch to the socket file created by the target VMString path;/*** Attaches to the target VM*/LinuxVirtualMachine(AttachProvider provider, String vmid)throws AttachNotSupportedException, IOException{super(provider, vmid);// This provider only understands pidsint pid;try {pid = Integer.parseInt(vmid);} catch (NumberFormatException x) {throw new AttachNotSupportedException("Invalid process identifier");}// Find the socket file. If not found then we attempt to start the// attach mechanism in the target VM by sending it a QUIT signal.// Then we attempt to find the socket file again.path = findSocketFile(pid);if (path == null) {File f = createAttachFile(pid);try {// On LinuxThreads each thread is a process and we don't have the// pid of the VMThread which has SIGQUIT unblocked. To workaround// this we get the pid of the "manager thread" that is created// by the first call to pthread_create. This is parent of all// threads (except the initial thread).if (isLinuxThreads) {int mpid;try {mpid = getLinuxThreadsManager(pid);} catch (IOException x) {throw new AttachNotSupportedException(x.getMessage());}assert(mpid >= 1);sendQuitToChildrenOf(mpid);} else {sendQuitTo(pid);}// give the target VM time to start the attach mechanismint i = 0;long delay = 200;int retries = (int)(attachTimeout() / delay);do {try {Thread.sleep(delay);} catch (InterruptedException x) { }path = findSocketFile(pid);i++;} while (i <= retries && path == null);if (path == null) {throw new AttachNotSupportedException("Unable to open socket file: target process not responding " +"or HotSpot VM not loaded");}} finally {f.delete();}}// Check that the file owner/permission to avoid attaching to// bogus processcheckPermissions(path);// Check that we can connect to the process// - this ensures we throw the permission denied error now rather than// later when we attempt to enqueue a command.int s = socket();try {connect(s, path);} finally {close(s);}}/*** Detach from the target VM*/public void detach() throws IOException {synchronized (this) {if (this.path != null) {this.path = null;}}}
- 查找/tmp目錄下是否存在".java_pid"+pid文件;
- 如果文件不存在,則首先創(chuàng)建"/proc/" + pid + "/cwd/" + ".attach_pid" + pid文件,然后通過kill命令發(fā)送SIGQUIT信號給目標JVM進程;
- 目標JVM進程接收到信號之后,會在/tmp目錄下創(chuàng)建".java_pid"+pid文件
- 當發(fā)現(xiàn)/tmp目錄下存在".java_pid"+pid文件,LinuxVirtualMachine會通過connect系統(tǒng)調(diào)用連接到該文件描述符,后續(xù)通過該fd進行雙方的通訊;
JVM接受SIGQUIT信號的相關(guān)邏輯在os.cpp文件的os::signal_init方法:
jstack是通過調(diào)用remoteDataDump方法實現(xiàn)的,該方法就是通過往前面提到的fd中寫入threaddump指令,讀取返回結(jié)果,從而得到目標JVM的堆棧信息。
----------------------------------
jstack等命令會與jvm進程建立socket連接,發(fā)送對應(yīng)的指令(jstack發(fā)送了threaddump指令),然后再讀取返回的數(shù)據(jù)。
/*** Execute the given command in the target VM.*/InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {assert args.length <= 3; // includes null// did we detach?String p;synchronized (this) {if (this.path == null) {throw new IOException("Detached from target VM");}p = this.path;}// create UNIX socketint s = socket();// connect to target VMtry {connect(s, p);} catch (IOException x) {close(s);throw x;}IOException ioe = null;// connected - write request// <ver> <cmd> <args...>try {writeString(s, PROTOCOL_VERSION);writeString(s, cmd);for (int i=0; i<3; i++) {if (i < args.length && args[i] != null) {writeString(s, (String)args[i]);} else {writeString(s, "");}}} catch (IOException x) {ioe = x;}// Create an input stream to read replySocketInputStream sis = new SocketInputStream(s);// Read the command completion statusint completionStatus;try {completionStatus = readInt(sis);} catch (IOException x) {sis.close();if (ioe != null) {throw ioe;} else {throw x;}}if (completionStatus != 0) {// read from the stream and use that as the error messageString message = readErrorMessage(sis);sis.close();// In the event of a protocol mismatch then the target VM// returns a known error so that we can throw a reasonable// error.if (completionStatus == ATTACH_ERROR_BADVERSION) {throw new IOException("Protocol mismatch with target VM");}// Special-case the "load" command so that the right exception is// thrown.if (cmd.equals("load")) {throw new AgentLoadException("Failed to load agent library");} else {if (message == null) {throw new AttachOperationFailedException("Command failed in target VM");} else {throw new AttachOperationFailedException(message);}}}// Return the input stream so that the command output can be readreturn sis;}
?
-----------------
下面是C++部分
\openjdk\hotspot\src\share\vm\services\attachListener.hpp
// Table to map operation names to functions.// names must be of length <= AttachOperation::name_length_max
static AttachOperationFunctionInfo funcs[] = {{ "agentProperties", get_agent_properties },{ "datadump", data_dump },{ "dumpheap", dump_heap },{ "load", JvmtiExport::load_agent_library },{ "properties", get_system_properties },{ "threaddump", thread_dump },{ "inspectheap", heap_inspection },{ "setflag", set_flag },{ "printflag", print_flag },{ "jcmd", jcmd },{ NULL, NULL }
};
\openjdk\hotspot\src\os\linux\vm\attachListener_linux.cpp
偵聽socket
// The attach mechanism on Linux uses a UNIX domain socket. An attach listener
// thread is created at startup or is created on-demand via a signal from
// the client tool. The attach listener creates a socket and binds it to a file
// in the filesystem. The attach listener then acts as a simple (single-
// threaded) server - it waits for a client to connect, reads the request,
// executes it, and returns the response to the client via the socket
// connection.
//
// As the socket is a UNIX domain socket it means that only clients on the
// local machine can connect. In addition there are two other aspects to
// the security:
// 1. The well known file that the socket is bound to has permission 400
// 2. When a client connect, the SO_PEERCRED socket option is used to
// obtain the credentials of client. We check that the effective uid
// of the client matches this process.....
// Initialization - create a listener socket and bind it to a fileint LinuxAttachListener::init() {char path[UNIX_PATH_MAX]; // socket filechar initial_path[UNIX_PATH_MAX]; // socket file during setupint listener; // listener socket (file descriptor)// register function to cleanup::atexit(listener_cleanup);int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",os::get_temp_directory(), os::current_process_id());if (n < (int)UNIX_PATH_MAX) {n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);}if (n >= (int)UNIX_PATH_MAX) {return -1;}// create the listener socketlistener = ::socket(PF_UNIX, SOCK_STREAM, 0);if (listener == -1) {return -1;}// bind socketstruct sockaddr_un addr;addr.sun_family = AF_UNIX;strcpy(addr.sun_path, initial_path);::unlink(initial_path);int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));if (res == -1) {::close(listener);return -1;}// put in listen mode, set permissions, and rename into placeres = ::listen(listener, 5);if (res == 0) {RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);if (res == 0) {res = ::rename(initial_path, path);}}if (res == -1) {::close(listener);::unlink(initial_path);return -1;}set_path(path);set_listener(listener);return 0;
}
....
再就是一個個命令對應(yīng)去看具體代碼實現(xiàn),以dumpheap為例:
\openjdk\hotspot\src\share\vm\services\heapDumper.cpp
// The VM operation that dumps the heap. The dump consists of the following
// records:
//
// HPROF_HEADER
// [HPROF_UTF8]*
// [HPROF_LOAD_CLASS]*
// [[HPROF_FRAME]*|HPROF_TRACE]*
// [HPROF_GC_CLASS_DUMP]*
// HPROF_HEAP_DUMP
//
// The HPROF_TRACE records represent the stack traces where the heap dump
// is generated and a "dummy trace" record which does not include
// any frames. The dummy trace record is used to be referenced as the
// unknown object alloc site.
//
// The HPROF_HEAP_DUMP record has a length following by sub-records. To allow
// the heap dump be generated in a single pass we remember the position of
// the dump length and fix it up after all sub-records have been written.
// To generate the sub-records we iterate over the heap, writing
// HPROF_GC_INSTANCE_DUMP, HPROF_GC_OBJ_ARRAY_DUMP, and HPROF_GC_PRIM_ARRAY_DUMP
// records as we go. Once that is done we write records for some of the GC
// roots.// HPROF_TRACE記錄表示堆轉(zhuǎn)儲的堆棧跟蹤
//生成并且“虛擬跟蹤”記錄不包括
//任何幀 虛擬跟蹤記錄用于引用
//未知對象分配站點。
//
// HPROF_HEAP_DUMP記錄的子記錄長度如下。 允許
//在一次傳遞中生成堆轉(zhuǎn)儲,我們記住了它的位置
//轉(zhuǎn)儲長度并在寫完所有子記錄后修復它。
//為了生成子記錄,我們迭代堆,寫
// HPROF_GC_INSTANCE_DUMP,HPROF_GC_OBJ_ARRAY_DUMP和HPROF_GC_PRIM_ARRAY_DUMP
//我們?nèi)サ挠涗洝?完成后,我們會為某些GC編寫記錄
//根
void VM_HeapDumper::doit() {HandleMark hm;CollectedHeap* ch = Universe::heap();ch->ensure_parsability(false); // must happen, even if collection does// not happen (e.g. due to GC_locker)if (_gc_before_heap_dump) {if (GC_locker::is_active()) {warning("GC locker is held; pre-heapdump GC was skipped");} else {ch->collect_as_vm_thread(GCCause::_heap_dump);}}// At this point we should be the only dumper active, so// the following should be safe.set_global_dumper();set_global_writer();// Write the file header - use 1.0.2 for large heaps, otherwise 1.0.1size_t used = ch->used();const char* header;if (used > (size_t)SegmentedHeapDumpThreshold) {set_segmented_dump();header = "JAVA PROFILE 1.0.2";} else {header = "JAVA PROFILE 1.0.1";}// header is few bytes long - no chance to overflow intwriter()->write_raw((void*)header, (int)strlen(header));writer()->write_u1(0); // terminatorwriter()->write_u4(oopSize);writer()->write_u8(os::javaTimeMillis());// HPROF_UTF8 recordsSymbolTableDumper sym_dumper(writer());SymbolTable::symbols_do(&sym_dumper);// write HPROF_LOAD_CLASS recordsClassLoaderDataGraph::classes_do(&do_load_class);Universe::basic_type_classes_do(&do_load_class);// write HPROF_FRAME and HPROF_TRACE records// this must be called after _klass_map is built when iterating the classes above.dump_stack_traces();// write HPROF_HEAP_DUMP or HPROF_HEAP_DUMP_SEGMENTwrite_dump_header();// Writes HPROF_GC_CLASS_DUMP recordsClassLoaderDataGraph::classes_do(&do_class_dump);Universe::basic_type_classes_do(&do_basic_type_array_class_dump);check_segment_length();// writes HPROF_GC_INSTANCE_DUMP records.// After each sub-record is written check_segment_length will be invoked. When// generated a segmented heap dump this allows us to check if the current// segment exceeds a threshold and if so, then a new segment is started.// The HPROF_GC_CLASS_DUMP and HPROF_GC_INSTANCE_DUMP are the vast bulk// of the heap dump.HeapObjectDumper obj_dumper(this, writer());Universe::heap()->safe_object_iterate(&obj_dumper);// HPROF_GC_ROOT_THREAD_OBJ + frames + jni localsdo_threads();check_segment_length();// HPROF_GC_ROOT_MONITOR_USEDMonitorUsedDumper mon_dumper(writer());ObjectSynchronizer::oops_do(&mon_dumper);check_segment_length();// HPROF_GC_ROOT_JNI_GLOBALJNIGlobalsDumper jni_dumper(writer());JNIHandles::oops_do(&jni_dumper);check_segment_length();// HPROF_GC_ROOT_STICKY_CLASSStickyClassDumper class_dumper(writer());SystemDictionary::always_strong_classes_do(&class_dumper);// fixes up the length of the dump record. In the case of a segmented// heap then the HPROF_HEAP_DUMP_END record is also written.end_of_dump();// Now we clear the global variables, so that a future dumper might run.clear_global_dumper();clear_global_writer();
}
void VM_HeapDumper::dump_stack_traces() {// write a HPROF_TRACE record without any frames to be referenced as object alloc sitesDumperSupport::write_header(writer(), HPROF_TRACE, 3*sizeof(u4));writer()->write_u4((u4) STACK_TRACE_ID);writer()->write_u4(0); // thread numberwriter()->write_u4(0); // frame count_stack_traces = NEW_C_HEAP_ARRAY(ThreadStackTrace*, Threads::number_of_threads(), mtInternal);int frame_serial_num = 0;for (JavaThread* thread = Threads::first(); thread != NULL ; thread = thread->next()) {oop threadObj = thread->threadObj();if (threadObj != NULL && !thread->is_exiting() && !thread->is_hidden_from_external_view()) {// dump thread stack traceThreadStackTrace* stack_trace = new ThreadStackTrace(thread, false);stack_trace->dump_stack_at_safepoint(-1);_stack_traces[_num_threads++] = stack_trace;// write HPROF_FRAME records for this thread's stack traceint depth = stack_trace->get_stack_depth();int thread_frame_start = frame_serial_num;int extra_frames = 0;// write fake frame that makes it look like the thread, which caused OOME,// is in the OutOfMemoryError zero-parameter constructorif (thread == _oome_thread && _oome_constructor != NULL) {int oome_serial_num = _klass_map->find(_oome_constructor->method_holder());// the class serial number starts from 1assert(oome_serial_num > 0, "OutOfMemoryError class not found");DumperSupport::dump_stack_frame(writer(), ++frame_serial_num, oome_serial_num,_oome_constructor, 0);extra_frames++;}for (int j=0; j < depth; j++) {StackFrameInfo* frame = stack_trace->stack_frame_at(j);Method* m = frame->method();int class_serial_num = _klass_map->find(m->method_holder());// the class serial number starts from 1assert(class_serial_num > 0, "class not found");DumperSupport::dump_stack_frame(writer(), ++frame_serial_num, class_serial_num, m, frame->bci());}depth += extra_frames;// write HPROF_TRACE record for one threadDumperSupport::write_header(writer(), HPROF_TRACE, 3*sizeof(u4) + depth*oopSize);int stack_serial_num = _num_threads + STACK_TRACE_ID;writer()->write_u4(stack_serial_num); // stack trace serial numberwriter()->write_u4((u4) _num_threads); // thread serial numberwriter()->write_u4(depth); // frame countfor (int j=1; j <= depth; j++) {writer()->write_id(thread_frame_start + j);}}}
}
// dump the heap to given path.
PRAGMA_FORMAT_NONLITERAL_IGNORED_EXTERNAL
int HeapDumper::dump(const char* path) {assert(path != NULL && strlen(path) > 0, "path missing");// print message in interactive caseif (print_to_tty()) {tty->print_cr("Dumping heap to %s ...", path);timer()->start();}// create the dump writer. If the file can be opened then bailDumpWriter writer(path);if (!writer.is_open()) {set_error(writer.error());if (print_to_tty()) {tty->print_cr("Unable to create %s: %s", path,(error() != NULL) ? error() : "reason unknown");}return -1;}// generate the dumpVM_HeapDumper dumper(&writer, _gc_before_heap_dump, _oome);if (Thread::current()->is_VM_thread()) {assert(SafepointSynchronize::is_at_safepoint(), "Expected to be called at a safepoint");dumper.doit();} else {VMThread::execute(&dumper);}// close dump file and record any error that the writer may have encounteredwriter.close();set_error(writer.error());// print message in interactive caseif (print_to_tty()) {timer()->stop();if (error() == NULL) {char msg[256];sprintf(msg, "Heap dump file created [%s bytes in %3.3f secs]",JLONG_FORMAT, timer()->seconds());
PRAGMA_DIAG_PUSH
PRAGMA_FORMAT_NONLITERAL_IGNORED_INTERNALtty->print_cr(msg, writer.bytes_written());
PRAGMA_DIAG_POP} else {tty->print_cr("Dump file is incomplete: %s", writer.error());}}return (writer.error() == NULL) ? 0 : -1;
}
?
說明:本文參考了《jstack是如何獲取threaddump的》和《java attach機制源碼閱讀》這兩篇都是java部分的缺少C++,然后C++部分是我加上的。
--------------------
《OpenJDK源碼學習-加載本地庫》
從整個加載本地庫的流程來看,基本上還是調(diào)用和平臺有關(guān)的函數(shù)來完成的,并在加載和卸載的時候分別調(diào)用了兩個生命周期回調(diào)函數(shù) JNI_OnLoad 和 JNI_OnUnLoad 。
以linux平臺為例,簡單總結(jié)一下整個so庫的加載流程:
- 首先
System.loadLibrary()被調(diào)用,開始整個加載過程。 - 其中調(diào)用
ClassLoader對象來完成主要工作,將每個本地庫封裝成NativeLibrary對象,并以靜態(tài)變量存到已經(jīng)加載過的棧中。 - 執(zhí)行
NativeLibrary類的loadnative方法,來交給native層去指向具體的加載工作。 - native層
ClassLoader.c中的Java_java_lang_ClassLoader_00024NativeLibrary_load函數(shù)被調(diào)用。 - 在native load函數(shù)中首先使用
dlopen來加載so本地庫文件,并將返回的handle保存到NativeLibrary對象中。 - 接著查找已經(jīng)加載的so本地庫中的
JNI_OnLoad函數(shù),并執(zhí)行它。 - 整個so本地庫的加載流程完畢。
只有在 NativeLibrary 對象被GC回收的時候,其 finalize 方法被調(diào)用了,對應(yīng)加載的本地庫才會被 unload 。這種情況一般來說并不會發(fā)生,因為 NativeLibrary 對象是以靜態(tài)變量的形式被保存的,而靜態(tài)變量是 GC roots,一般來說都不會被回收掉的。
總結(jié)
以上是生活随笔為你收集整理的JDK源码研究Jstack,JMap,threaddump,dumpheap的原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 猪脚焖花生的家常做法大全怎么做好
- 下一篇: 哪里能免费观看我和我的祖国电影?