Skip to content

Commit b034e4c

Browse files
async-profiler#1174: Detect JVM in non-Java application and attach to it (async-profiler#1192)
Co-authored-by: Andrei Pangin <noreply@pangin.pro>
1 parent 39f4300 commit b034e4c

File tree

8 files changed

+509
-1
lines changed

8 files changed

+509
-1
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ build-test-bins:
220220
@mkdir -p $(TEST_BIN_DIR)
221221
gcc -o $(TEST_BIN_DIR)/malloc_plt_dyn test/test/nativemem/malloc_plt_dyn.c
222222
gcc -o $(TEST_BIN_DIR)/native_api -Isrc test/test/c/native_api.c -ldl
223+
$(CXX) -o $(TEST_BIN_DIR)/non_java_app $(INCLUDES) $(CPP_TEST_INCLUDES) test/test/nonjava/non_java_app.cpp $(LIBS)
223224

224225
test-cpp: build-test-cpp
225226
echo "Running cpp tests..."

src/profiler.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,11 @@ Error Profiler::start(Arguments& args, bool reset) {
10771077
return Error("Profiler already started");
10781078
}
10791079

1080+
// If profiler is started from a native app, try to detect a running JVM and attach to it
1081+
if (!VM::loaded()) {
1082+
VM::tryAttach();
1083+
}
1084+
10801085
Error error = checkJvmCapabilities();
10811086
if (error) {
10821087
return error;

src/vmEntry.cpp

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,15 @@ int VM::_hotspot_version = 0;
3232
bool VM::_openj9 = false;
3333
bool VM::_zing = false;
3434

35+
GetCreatedJavaVMs VM::_getCreatedJavaVMs = NULL;
36+
3537
jvmtiError (JNICALL *VM::_orig_RedefineClasses)(jvmtiEnv*, jint, const jvmtiClassDefinition*);
3638
jvmtiError (JNICALL *VM::_orig_RetransformClasses)(jvmtiEnv*, jint, const jclass* classes);
3739

3840
AsyncGetCallTrace VM::_asyncGetCallTrace;
3941
JVM_MemoryFunc VM::_totalMemory;
4042
JVM_MemoryFunc VM::_freeMemory;
4143

42-
4344
static bool isVmRuntimeEntry(const char* blob_name) {
4445
return strcmp(blob_name, "_ZNK12MemAllocator8allocateEv") == 0
4546
|| strncmp(blob_name, "_Z22post_allocation_notify", 26) == 0
@@ -106,6 +107,30 @@ static void* resolveMethodIdEnd() {
106107
return NULL;
107108
}
108109

110+
// Workaround for JDK-8308341: since JNI_GetCreatedJavaVMs may return an uninitialized JVM,
111+
// we verify the readiness of the JVM by presence of "VM Thread" and "Service Thread".
112+
bool VM::hasJvmThreads() {
113+
char thread_name[32];
114+
int threads_found = 0;
115+
116+
ThreadList* list = OS::listThreads();
117+
while (list->hasNext() && threads_found != 3) {
118+
if (!OS::threadName(list->next(), thread_name, sizeof(thread_name))) {
119+
continue;
120+
}
121+
122+
// On macOS, Java thread names start with "Java: "
123+
int thread_name_offset = strncmp(thread_name, "Java: ", 6) == 0 ? 6 : 0;
124+
125+
if (strcmp(thread_name + thread_name_offset, "VM Thread") == 0) {
126+
threads_found |= 1;
127+
} else if (strcmp(thread_name + thread_name_offset, "Service Thread") == 0) {
128+
threads_found |= 2;
129+
}
130+
}
131+
132+
return threads_found == 3;
133+
}
109134

110135
bool VM::init(JavaVM* vm, bool attach) {
111136
if (_jvmti != NULL) return true;
@@ -275,6 +300,38 @@ bool VM::init(JavaVM* vm, bool attach) {
275300
return true;
276301
}
277302

303+
// Try to find a running JVM instance and attach to it
304+
void VM::tryAttach() {
305+
if (_getCreatedJavaVMs == NULL) {
306+
void* lib_handle = dlopen(OS::isLinux() ? "libjvm.so" : "libjvm.dylib", RTLD_LAZY | RTLD_NOLOAD);
307+
if (lib_handle != NULL) {
308+
_getCreatedJavaVMs = (GetCreatedJavaVMs)dlsym(lib_handle, "JNI_GetCreatedJavaVMs");
309+
dlclose(lib_handle);
310+
}
311+
if (_getCreatedJavaVMs == NULL) {
312+
return;
313+
}
314+
}
315+
316+
JavaVM* vm;
317+
jsize nVMs;
318+
if (_getCreatedJavaVMs(&vm, 1, &nVMs) != JNI_OK || nVMs != 1) {
319+
return;
320+
}
321+
322+
JNIEnv* env;
323+
jint get_env_result = vm->GetEnv((void**)&env, JNI_VERSION_1_6);
324+
if (get_env_result == JNI_OK) {
325+
// Current thread already belongs to the running JVM
326+
VM::init(vm, true);
327+
} else if (get_env_result == JNI_EDETACHED) {
328+
// There is a running JVM, but we need to check it is initialized
329+
if (hasJvmThreads() && vm->AttachCurrentThreadAsDaemon((void**)&env, NULL) == JNI_OK) {
330+
VM::init(vm, true);
331+
}
332+
}
333+
}
334+
278335
// Run late initialization when JVM is ready
279336
void VM::ready() {
280337
Profiler::setupSignalHandlers();

src/vmEntry.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ typedef void (*AsyncGetCallTrace)(ASGCT_CallTrace*, jint, void*);
8585

8686
typedef jlong (*JVM_MemoryFunc)();
8787

88+
typedef jint (*GetCreatedJavaVMs)(JavaVM**, jsize, jsize*);
89+
8890
typedef struct {
8991
void* unused1[86];
9092
jvmtiError (JNICALL *RedefineClasses)(jvmtiEnv*, jint, const jvmtiClassDefinition*);
@@ -102,13 +104,16 @@ class VM {
102104
static bool _openj9;
103105
static bool _zing;
104106

107+
static GetCreatedJavaVMs _getCreatedJavaVMs;
108+
105109
static jvmtiError (JNICALL *_orig_RedefineClasses)(jvmtiEnv*, jint, const jvmtiClassDefinition*);
106110
static jvmtiError (JNICALL *_orig_RetransformClasses)(jvmtiEnv*, jint, const jclass* classes);
107111

108112
static void ready();
109113
static void applyPatch(char* func, const char* patch, const char* end_patch);
110114
static void loadMethodIDs(jvmtiEnv* jvmti, JNIEnv* jni, jclass klass);
111115
static void loadAllMethodIDs(jvmtiEnv* jvmti, JNIEnv* jni);
116+
static bool hasJvmThreads();
112117

113118
public:
114119
static AsyncGetCallTrace _asyncGetCallTrace;
@@ -117,6 +122,8 @@ class VM {
117122

118123
static bool init(JavaVM* vm, bool attach);
119124

125+
static void tryAttach();
126+
120127
static bool loaded() {
121128
return _jvmti != NULL;
122129
}

test/one/profiler/test/TestProcess.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ public class TestProcess implements Closeable {
3333
public static final String LIBPROF = "%lib";
3434
public static final String TESTBIN = "%testbin";
3535

36+
private static final String JAVA_HOME = System.getProperty("java.home");
37+
3638
private static final Pattern filePattern = Pattern.compile("(%[a-z]+)(\\.[a-z]+)?");
3739

3840
private static final MethodHandle pid = getPidHandle();
@@ -86,6 +88,7 @@ public TestProcess(Test test, Os currentOs, String logDir) throws Exception {
8688
pb.environment().put(keyValue[0], substituteFiles(keyValue[1]));
8789
}
8890
}
91+
pb.environment().put("TEST_JAVA_HOME", JAVA_HOME);
8992

9093
this.p = pb.start();
9194

test/test/nonjava/JavaClass.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright The async-profiler authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package test.nonjava;
7+
8+
public class JavaClass {
9+
10+
public static double cpuHeavyTask() {
11+
double sum = 0;
12+
for (int i = 0; i < 100000; i++) {
13+
sum += Math.sqrt(Math.random());
14+
sum += Math.pow(Math.random(), Math.random());
15+
}
16+
return sum;
17+
}
18+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright The async-profiler authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package test.nonjava;
7+
8+
import one.profiler.test.Output;
9+
import one.profiler.test.Test;
10+
import one.profiler.test.TestProcess;
11+
12+
public class NonjavaTests {
13+
14+
// jvm is loaded before the profiling session is started
15+
@Test(sh = "%testbin/non_java_app 1 %s.html", output = true)
16+
public void jvmFirst(TestProcess p) throws Exception {
17+
p.waitForExit();
18+
assert p.exitCode() == 0;
19+
20+
Output out = p.readFile("%s");
21+
assert out.contains(".cpuHeavyTask");
22+
}
23+
24+
// jvm is loaded after the profiling session is started
25+
@Test(sh = "%testbin/non_java_app 2 %s.html", output = true)
26+
public void profilerFirst(TestProcess p) throws Exception {
27+
p.waitForExit();
28+
assert p.exitCode() == 0;
29+
30+
Output out = p.readFile("%s");
31+
assert !out.contains(".cpuHeavyTask");
32+
}
33+
34+
// jvm is loaded between two profiling sessions
35+
@Test(sh = "%testbin/non_java_app 3 %f.html %s.html", output = true)
36+
public void jvmInBetween(TestProcess p) throws Exception {
37+
p.waitForExit();
38+
assert p.exitCode() == 0;
39+
40+
Output out = p.readFile("%f");
41+
assert !out.contains(".cpuHeavyTask");
42+
43+
out = p.readFile("%s");
44+
assert out.contains(".cpuHeavyTask");
45+
}
46+
47+
// jvm is loaded before the profiling session is started on a different thread
48+
@Test(sh = "%testbin/non_java_app 4 %s.html", output = true)
49+
public void differentThread(TestProcess p) throws Exception {
50+
p.waitForExit();
51+
assert p.exitCode() == 0;
52+
53+
Output out = p.readFile("%s");
54+
assert out.contains(".cpuHeavyTask");
55+
}
56+
}

0 commit comments

Comments
 (0)