diff --git a/README.md b/README.md index cfb75baa..31d0049c 100644 --- a/README.md +++ b/README.md @@ -361,6 +361,10 @@ The following is a complete list of the command-line options accepted by * `--alloc N` - allocation profiling interval in bytes or in other units, if N is followed by `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). +* `--live` - retain allocation samples with live objects only + (object that have not been collected by the end of profiling session). + Useful for finding Java heap memory leaks. + * `--lock N` - lock profiling threshold in nanoseconds (or other units). In lock profiling mode, record contended locks that the JVM has waited for longer than the specified duration. diff --git a/profiler.sh b/profiler.sh index 30cb520b..73b8790a 100755 --- a/profiler.sh +++ b/profiler.sh @@ -36,6 +36,7 @@ usage() { echo "" echo " --loop time run profiler in a loop" echo " --alloc bytes allocation profiling interval in bytes" + echo " --live build allocation profile from live objects only" echo " --lock duration lock profiling threshold in nanoseconds" echo " --total accumulate the total value (time, bytes, etc.)" echo " --all-user only include user-mode events" @@ -242,6 +243,9 @@ while [ $# -gt 0 ]; do --sched) PARAMS="$PARAMS,sched" ;; + --live) + PARAMS="$PARAMS,live" + ;; --cstack|--call-graph) PARAMS="$PARAMS,cstack=$2" shift diff --git a/src/allocTracer.cpp b/src/allocTracer.cpp index b309438d..3070be45 100644 --- a/src/allocTracer.cpp +++ b/src/allocTracer.cpp @@ -79,6 +79,10 @@ void AllocTracer::recordAllocation(void* ucontext, int event_type, uintptr_t rkl } Error AllocTracer::check(Arguments& args) { + if (args._live) { + return Error("'live' option is supported on OpenJDK 11+"); + } + if (_in_new_tlab.entry() != 0 && _outside_tlab.entry() != 0) { return Error::OK; } diff --git a/src/arguments.cpp b/src/arguments.cpp index d755ed58..51ec2aae 100644 --- a/src/arguments.cpp +++ b/src/arguments.cpp @@ -64,6 +64,7 @@ static const Multiplier UNIVERSAL[] = {{'n', 1}, {'u', 1000}, {'m', 1000000}, {' // version[=full] - display the agent version // event=EVENT - which event to trace (cpu, wall, cache-misses, etc.) // alloc[=BYTES] - profile allocations with BYTES interval +// live - build allocation profile from live objects only // lock[=DURATION] - profile contended locks longer than DURATION ns // collapsed - dump collapsed stacks (the format used by FlameGraph script) // flamegraph - produce Flame Graph in HTML format @@ -292,6 +293,9 @@ Error Arguments::parse(const char* args) { CASE("sched") _sched = true; + CASE("live") + _live = true; + CASE("allkernel") _ring = RING_KERNEL; diff --git a/src/arguments.h b/src/arguments.h index 386b2af7..4648c5e8 100644 --- a/src/arguments.h +++ b/src/arguments.h @@ -154,6 +154,7 @@ class Arguments { bool _loop; bool _threads; bool _sched; + bool _live; bool _fdtransfer; const char* _fdtransfer_path; int _style; @@ -199,6 +200,7 @@ class Arguments { _loop(false), _threads(false), _sched(false), + _live(false), _fdtransfer(false), _fdtransfer_path(NULL), _style(0), diff --git a/src/callTraceStorage.cpp b/src/callTraceStorage.cpp index 18df04c8..cc19027e 100644 --- a/src/callTraceStorage.cpp +++ b/src/callTraceStorage.cpp @@ -276,9 +276,27 @@ u32 CallTraceStorage::put(int num_frames, ASGCT_CallFrame* frames, u64 counter) slot = (slot + step) & (capacity - 1); } - CallTraceSample& s = table->values()[slot]; - atomicInc(s.samples); - atomicInc(s.counter, counter); + if (counter != 0) { + CallTraceSample& s = table->values()[slot]; + atomicInc(s.samples); + atomicInc(s.counter, counter); + } return capacity - (INITIAL_CAPACITY - 1) + slot; } + +void CallTraceStorage::add(u32 call_trace_id, u64 counter) { + if (call_trace_id == OVERFLOW_TRACE_ID) { + return; + } + + call_trace_id += (INITIAL_CAPACITY - 1); + for (LongHashTable* table = _current_table; table != NULL; table = table->prev()) { + if (call_trace_id >= table->capacity()) { + CallTraceSample& s = table->values()[call_trace_id - table->capacity()]; + atomicInc(s.samples); + atomicInc(s.counter, counter); + break; + } + } +} diff --git a/src/callTraceStorage.h b/src/callTraceStorage.h index 814c7b4a..4a5d8254 100644 --- a/src/callTraceStorage.h +++ b/src/callTraceStorage.h @@ -80,6 +80,7 @@ class CallTraceStorage { void collectSamples(std::map& map); u32 put(int num_frames, ASGCT_CallFrame* frames, u64 counter); + void add(u32 call_trace_id, u64 counter); }; #endif // _CALLTRACESTORAGE diff --git a/src/j9ObjectSampler.cpp b/src/j9ObjectSampler.cpp index b3d5375f..ae0742e5 100644 --- a/src/j9ObjectSampler.cpp +++ b/src/j9ObjectSampler.cpp @@ -22,14 +22,14 @@ void J9ObjectSampler::JavaObjectAlloc(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jobject object, jclass object_klass, jlong size) { if (_enabled && updateCounter(_allocated_bytes, size, _interval)) { - recordAllocation(jvmti, BCI_ALLOC, object_klass, size); + recordAllocation(jvmti, jni, BCI_ALLOC, object, object_klass, size); } } void J9ObjectSampler::VMObjectAlloc(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jobject object, jclass object_klass, jlong size) { if (_enabled && updateCounter(_allocated_bytes, size, _interval)) { - recordAllocation(jvmti, BCI_ALLOC_OUTSIDE_TLAB, object_klass, size); + recordAllocation(jvmti, jni, BCI_ALLOC_OUTSIDE_TLAB, object, object_klass, size); } } @@ -37,6 +37,9 @@ Error J9ObjectSampler::check(Arguments& args) { if (J9Ext::InstrumentableObjectAlloc_id < 0) { return Error("InstrumentableObjectAlloc is not supported on this JVM"); } + if (args._live) { + return Error("'live' option is supported on OpenJDK 11+"); + } return Error::OK; } @@ -47,6 +50,7 @@ Error J9ObjectSampler::start(Arguments& args) { } _interval = args._alloc > 0 ? args._alloc : DEFAULT_ALLOC_INTERVAL; + _live = false; _allocated_bytes = 0; jvmtiEnv* jvmti = VM::jvmti(); diff --git a/src/objectSampler.cpp b/src/objectSampler.cpp index df74b31e..098095e9 100644 --- a/src/objectSampler.cpp +++ b/src/objectSampler.cpp @@ -20,17 +20,110 @@ u64 ObjectSampler::_interval; +bool ObjectSampler::_live; volatile u64 ObjectSampler::_allocated_bytes; +class LiveRefs { + private: + enum { MAX_REFS = 1024 }; + + SpinLock _lock; + jweak _refs[MAX_REFS]; + jlong _sizes[MAX_REFS]; + u32 _traces[MAX_REFS]; + + static inline bool collected(jweak w) { + return *(void**)((uintptr_t)w & ~(uintptr_t)1) == NULL; + } + + void set(u32 index, jweak w, jlong size, u32 call_trace_id) { + _refs[index] = w; + _sizes[index] = size; + _traces[index] = call_trace_id; + } + + public: + LiveRefs() : _lock(1) { + } + + void init() { + memset(_refs, 0, sizeof(_refs)); + memset(_sizes, 0, sizeof(_sizes)); + memset(_traces, 0, sizeof(_traces)); + + _lock.unlock(); + } + + void add(JNIEnv* jni, jobject object, jlong size, u32 call_trace_id) { + jweak wobject = jni->NewWeakGlobalRef(object); + if (wobject == NULL) { + return; + } + + if (_lock.tryLock()) { + jlong min_size = size; + u32 min_index = 0; + + u32 start = (((uintptr_t)object >> 4) * 31 + ((uintptr_t)jni >> 4) + call_trace_id) & (MAX_REFS - 1); + u32 i = start; + do { + jweak w = _refs[i]; + if (w == NULL) { + set(i, wobject, size, call_trace_id); + _lock.unlock(); + return; + } else if (collected(w)) { + jni->DeleteWeakGlobalRef(w); + set(i, wobject, size, call_trace_id); + _lock.unlock(); + return; + } else if (_sizes[i] < min_size) { + min_size = _sizes[i]; + min_index = i; + } + } while ((i = (i + 1) & (MAX_REFS - 1)) != start); + + if (min_size < size) { + jni->DeleteWeakGlobalRef(_refs[min_index]); + set(min_index, wobject, size, call_trace_id); + _lock.unlock(); + return; + } + + _lock.unlock(); + } + + jni->DeleteWeakGlobalRef(wobject); + } + + void dump(JNIEnv* jni) { + _lock.lock(); + + for (u32 i = 0; i < MAX_REFS; i++) { + jweak w = _refs[i]; + if (w != NULL) { + if (!collected(w)) { + Profiler::instance()->callTraceStorage()->add(_traces[i], _sizes[i]); + } + jni->DeleteWeakGlobalRef(w); + } + } + } +}; + +static LiveRefs live_refs; + + void ObjectSampler::SampledObjectAlloc(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jobject object, jclass object_klass, jlong size) { if (_enabled) { - recordAllocation(jvmti, BCI_ALLOC, object_klass, size); + recordAllocation(jvmti, jni, BCI_ALLOC, object, object_klass, size); } } -void ObjectSampler::recordAllocation(jvmtiEnv* jvmti, int event_type, jclass object_klass, jlong size) { +void ObjectSampler::recordAllocation(jvmtiEnv* jvmti, JNIEnv* jni, int event_type, + jobject object, jclass object_klass, jlong size) { AllocEvent event; event._class_id = 0; event._total_size = size > _interval ? size : _interval; @@ -46,7 +139,12 @@ void ObjectSampler::recordAllocation(jvmtiEnv* jvmti, int event_type, jclass obj jvmti->Deallocate((unsigned char*)class_name); } - Profiler::instance()->recordSample(NULL, size, event_type, &event); + if (_live) { + u32 call_trace_id = Profiler::instance()->recordSample(NULL, 0, event_type, &event); + live_refs.add(jni, object, size, call_trace_id); + } else { + Profiler::instance()->recordSample(NULL, size, event_type, &event); + } } Error ObjectSampler::check(Arguments& args) { @@ -63,6 +161,11 @@ Error ObjectSampler::start(Arguments& args) { } _interval = args._alloc > 0 ? args._alloc : DEFAULT_ALLOC_INTERVAL; + _live = args._live; + + if (_live) { + live_refs.init(); + } jvmtiEnv* jvmti = VM::jvmti(); jvmti->SetHeapSamplingInterval(_interval); @@ -74,4 +177,8 @@ Error ObjectSampler::start(Arguments& args) { void ObjectSampler::stop() { jvmtiEnv* jvmti = VM::jvmti(); jvmti->SetEventNotificationMode(JVMTI_DISABLE, JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL); + + if (_live) { + live_refs.dump(VM::jni()); + } } diff --git a/src/objectSampler.h b/src/objectSampler.h index cd88caf5..0a196d22 100644 --- a/src/objectSampler.h +++ b/src/objectSampler.h @@ -25,9 +25,11 @@ class ObjectSampler : public Engine { protected: static u64 _interval; + static bool _live; static volatile u64 _allocated_bytes; - static void recordAllocation(jvmtiEnv* jvmti, int event_type, jclass object_klass, jlong size); + static void recordAllocation(jvmtiEnv* jvmti, JNIEnv* jni, int event_type, + jobject object, jclass object_klass, jlong size); public: const char* title() { diff --git a/src/profiler.cpp b/src/profiler.cpp index b32273bc..e1d36b99 100644 --- a/src/profiler.cpp +++ b/src/profiler.cpp @@ -569,7 +569,7 @@ void Profiler::fillFrameTypes(ASGCT_CallFrame* frames, int num_frames, NMethod* } } -void Profiler::recordSample(void* ucontext, u64 counter, jint event_type, Event* event) { +u32 Profiler::recordSample(void* ucontext, u64 counter, jint event_type, Event* event) { atomicInc(_total_samples); int tid = OS::threadId(); @@ -585,7 +585,7 @@ void Profiler::recordSample(void* ucontext, u64 counter, jint event_type, Event* // Need to reset PerfEvents ring buffer, even though we discard the collected trace PerfEvents::resetBuffer(tid); } - return; + return 0; } ASGCT_CallFrame* frames = _calltrace_buffer[lock_index]->_asgct_frames; @@ -639,6 +639,7 @@ void Profiler::recordSample(void* ucontext, u64 counter, jint event_type, Event* _jfr.recordEvent(lock_index, tid, call_trace_id, event_type, event, counter); _locks[lock_index].unlock(); + return call_trace_id; } void Profiler::recordExternalSample(u64 counter, Event* event, int tid, int num_frames, ASGCT_CallFrame* frames) { @@ -861,8 +862,8 @@ Engine* Profiler::selectEngine(const char* event_name) { } } -Engine* Profiler::selectAllocEngine(long alloc_interval) { - if (VM::canSampleObjects() && (alloc_interval > 0 || VM::hotspot_version() == 0)) { +Engine* Profiler::selectAllocEngine(long alloc_interval, bool live) { + if (VM::canSampleObjects() && (alloc_interval > 0 || live || VM::hotspot_version() == 0)) { return &object_sampler; } else if (VM::isOpenJ9()) { return &j9_object_sampler; @@ -1009,7 +1010,7 @@ Error Profiler::start(Arguments& args, bool reset) { } if (_event_mask & EM_ALLOC) { - _alloc_engine = selectAllocEngine(args._alloc); + _alloc_engine = selectAllocEngine(args._alloc, args._live); error = _alloc_engine->start(args); if (error) { goto error2; @@ -1097,7 +1098,7 @@ Error Profiler::check(Arguments& args) { error = _engine->check(args); } if (!error && args._alloc >= 0) { - _alloc_engine = selectAllocEngine(args._alloc); + _alloc_engine = selectAllocEngine(args._alloc, args._live); error = _alloc_engine->check(args); } if (!error && args._lock >= 0) { diff --git a/src/profiler.h b/src/profiler.h index 458112a3..60b98de7 100644 --- a/src/profiler.h +++ b/src/profiler.h @@ -137,7 +137,7 @@ class Profiler { bool excludeTrace(FrameName* fn, CallTrace* trace); void mangle(const char* name, char* buf, size_t size); Engine* selectEngine(const char* event_name); - Engine* selectAllocEngine(long alloc_interval); + Engine* selectAllocEngine(long alloc_interval, bool live); Engine* activeEngine(); Error checkJvmCapabilities(); @@ -191,6 +191,7 @@ class Profiler { Dictionary* classMap() { return &_class_map; } ThreadFilter* threadFilter() { return &_thread_filter; } + CallTraceStorage* callTraceStorage() { return &_call_trace_storage; } Error run(Arguments& args); Error runInternal(Arguments& args, std::ostream& out); @@ -204,7 +205,7 @@ class Profiler { void printUsedMemory(std::ostream& out); void switchThreadEvents(jvmtiEventMode mode); int convertNativeTrace(int native_frames, const void** callchain, ASGCT_CallFrame* frames); - void recordSample(void* ucontext, u64 counter, jint event_type, Event* event); + u32 recordSample(void* ucontext, u64 counter, jint event_type, Event* event); void recordExternalSample(u64 counter, Event* event, int tid, int num_frames, ASGCT_CallFrame* frames); void writeLog(LogLevel level, const char* message); void writeLog(LogLevel level, const char* message, size_t len);