網站首頁 編程語言 正文
前言
內存治理一直是每個開發者最關心的問題,我們在日常開發中會遇到各種各樣的內存問題,比如OOM,內存泄露,內存抖動等等,這些問題都有以下共性:
- 難發現,內存問題一般很難發現,業務開發中關系系數更少
- 治理困難,內存問題治理困難,比如oom,往往堆棧只是壓死駱駝的最后一根稻草
- 易復發,幾乎沒有一種方案,能夠杜絕內存問題,比如內存泄露幾乎是100%存在,只是不同項目影響的范圍不同而已
內存問題目前經過業內多年沉淀以及開發,已經有很多方案了,比如檢查內存泄露(LeakCanary,MIT,KOOM等)。相關文章已經有很多,所以我們從另一個角度出發,虛擬機側有沒有想過的方案檢測內存呢?有的,那就是JVMTI(Java Virtual Machine Tool Interface)即指 Java 虛擬機工具接口,它是一套由虛擬機直接提供的 native 接口,我們可以從這里面獲取虛擬機運行時的大部分信息。
友情提示:本文涉及native c層的代碼,如果讀者不熟悉也沒關系,已經盡量減少相關的代碼閱讀成本啦!沖就對啦!JVMTI在debug模式下有很多用處,當然release環境也可以通過hook方式開啟,但是不太建議,雖然jvmti有諸多限制,但是不妨礙我們多了解一個“黑科技”
JVMTI
JVMTI 簡介:
JVMTI,即由java虛擬機提供的面向虛擬機接口的一套監控api,雖然虛擬機中一直存在,但是在android中是在Android 8.0(API 級別 26)或更高版本的設備上才正式支持。jvmti的功能本質就是“埋點化”,把jvm的一些事件通過“監聽”的方式暴露給外部開發調試
jvmti監聽的事件包包含了虛擬機中線程、內存、堆、棧、類、方法、變量,事件、定時器,鎖等創建銷毀相關事件,本次我們從實戰的角度出發,看看如何實現一次內存分配的監聽。
native層開啟jvmti
前置準備
使用jvmti之前,我們需要創建一個native工程,同時我們需要使用jvmti的api,在native中就是頭文件了,我們需要復制一份jdk中的名叫jvmti.h的頭文件(在我們安裝的jdk/include目錄下),到我們的項目cpp根目錄即可
此時我們也自定義一個memory.cpp作為我們使用jvmti的函數載體。jvmti.h里面包含了我們所需要的一切函數定義與常量,當然,這個頭文件并不需要隨著native工程進行打包,因為在真正使用到jvmti相關的工具時,是由系統進行so依賴查找進行定位的,該so位于系統庫中(libopenjdkjvmtid.so、libopenjdkjvmti.so),所以我們不用關心具體的實現,接下來我們按照步驟進行即可,包括native層與java層
復寫Agent
作為第一步,我們需要復寫jvmti.h中的
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
這個是jvmti中的agent初始化的時候,由native回調,在這里我們可以拿到JavaVM環境,同時可以創建jvmtiEnv對象,該對象非常重要,用于native進行接下來的各種監聽處理
// 全局的jvmti環境變量
jvmtiEnv *mJvmtiEnv;
extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
//準備JVMTI環境,初始化mJvmtiEnv
vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2);
return JNI_OK;
}
開啟jvmtiCapabilities
默認時,jvmti中是不提供任何能力給我們使用的,我們可以通過jvmtiEnv,去查詢當前虛擬機實現的哪幾種jvmti回調
jvmtiError GetPotentialCapabilities(jvmtiCapabilities* capabilities_ptr) {
return functions->GetPotentialCapabilities(this, capabilities_ptr);
}
jvmtiError AddCapabilities(const jvmtiCapabilities* capabilities_ptr) {
return functions->AddCapabilities(this, capabilities_ptr);
}
可以看到,我們只需要傳入一個jvmtiCapabilities對象指針即可,之后的能力數據就會被填充到該對象,所以我們接下來在Agent_OnAttach函數中繼續補充以下代碼
//初始化工作
extern "C"
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
//準備JVMTI環境,初始化mJvmtiEnv
vm->GetEnv((void **) &mJvmtiEnv, JVMTI_VERSION_1_2);
//開啟JVMTI的能力:到這一步啦!!
jvmtiCapabilities caps;
mJvmtiEnv->GetPotentialCapabilities(&caps);
mJvmtiEnv->AddCapabilities(&caps);
__android_log_print(ANDROID_LOG_ERROR, "hello", "Agent_OnAttach");
return JNI_OK;
}
設置jvmtiEventCallbacks
我們已經查詢到了jvmti所支持的回調,這個時候就到了正式設置回調的環節,jvmti中支持以下幾種回調類型
typedef struct {
/* 50 : VM Initialization Event */
jvmtiEventVMInit VMInit;
/* 51 : VM Death Event */
jvmtiEventVMDeath VMDeath;
/* 52 : Thread Start */
jvmtiEventThreadStart ThreadStart;
/* 53 : Thread End */
jvmtiEventThreadEnd ThreadEnd;
/* 54 : Class File Load Hook */
jvmtiEventClassFileLoadHook ClassFileLoadHook;
/* 55 : Class Load */
jvmtiEventClassLoad ClassLoad;
/* 56 : Class Prepare */
jvmtiEventClassPrepare ClassPrepare;
/* 57 : VM Start Event */
jvmtiEventVMStart VMStart;
/* 58 : Exception */
jvmtiEventException Exception;
/* 59 : Exception Catch */
jvmtiEventExceptionCatch ExceptionCatch;
/* 60 : Single Step */
jvmtiEventSingleStep SingleStep;
/* 61 : Frame Pop */
jvmtiEventFramePop FramePop;
/* 62 : Breakpoint */
jvmtiEventBreakpoint Breakpoint;
/* 63 : Field Access */
jvmtiEventFieldAccess FieldAccess;
/* 64 : Field Modification */
jvmtiEventFieldModification FieldModification;
/* 65 : Method Entry */
jvmtiEventMethodEntry MethodEntry;
/* 66 : Method Exit */
jvmtiEventMethodExit MethodExit;
/* 67 : Native Method Bind */
jvmtiEventNativeMethodBind NativeMethodBind;
/* 68 : Compiled Method Load */
jvmtiEventCompiledMethodLoad CompiledMethodLoad;
/* 69 : Compiled Method Unload */
jvmtiEventCompiledMethodUnload CompiledMethodUnload;
/* 70 : Dynamic Code Generated */
jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
/* 71 : Data Dump Request */
jvmtiEventDataDumpRequest DataDumpRequest;
/* 72 */
jvmtiEventReserved reserved72;
/* 73 : Monitor Wait */
jvmtiEventMonitorWait MonitorWait;
/* 74 : Monitor Waited */
jvmtiEventMonitorWaited MonitorWaited;
/* 75 : Monitor Contended Enter */
jvmtiEventMonitorContendedEnter MonitorContendedEnter;
/* 76 : Monitor Contended Entered */
jvmtiEventMonitorContendedEntered MonitorContendedEntered;
/* 77 */
jvmtiEventReserved reserved77;
/* 78 */
jvmtiEventReserved reserved78;
/* 79 */
jvmtiEventReserved reserved79;
/* 80 : Resource Exhausted */
jvmtiEventResourceExhausted ResourceExhausted;
/* 81 : Garbage Collection Start */
jvmtiEventGarbageCollectionStart GarbageCollectionStart;
/* 82 : Garbage Collection Finish */
jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
/* 83 : Object Free */
jvmtiEventObjectFree ObjectFree;
/* 84 : VM Object Allocation */
jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;
我們需要監聽的是內存分配與銷毀的監聽即可,分別是VMObjectAlloc與ObjectFree,在jvmtiEventCallbacks設定我們想要監聽的事件之后,我們可以通過jvmtiEnv->SetEventCallbacks方法設定即可,所以我們可以繼續在Agent_OnAttach中補充以下代碼
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.VMObjectAlloc = &objectAlloc;
callbacks.ObjectFree = &objectFree;
//設置回調函數
mJvmtiEnv->SetEventCallbacks(&callbacks, sizeof(callbacks));
其中objectAlloc是我們自定義的監聽處理函數,如果jvm執行內存分配事件,就會回調此函數,該函數定義是
typedef void (JNICALL *jvmtiEventVMObjectAlloc)
(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jobject object,
jclass object_klass,
jlong size);
所以我們自定義的回調函數也要根據此定義進行編寫。因為這里會回調所有java層的對象創建事件,回調次數非常多,在實際中我們可能并不關心系統類是如何分配內存的,而是關心我們自己的項目中的類的內存情況,所以這里我們做一個過濾,只有是項目的類我們才進行記錄
void JNICALL objectAlloc(jvmtiEnv *jvmti_env, JNIEnv *jni_env, jthread thread,
jobject object, jclass object_klass, jlong size) {
jvmti_env->SetTag(object, tag);
tag+= 1;
char *classSignature;
// 獲取類簽名
jvmti_env->GetClassSignature(object_klass, &classSignature, nullptr);
// 過濾條件
if(strstr(classSignature, "com/test/memory") != nullptr){
__android_log_print(ANDROID_LOG_ERROR, "hello", "%s",classSignature);
myVM->AttachCurrentThread( ¤tEnv, nullptr);
// 這個list我們之后解釋
list.push_back(tag);
char str[500];
char *format = "%s: object alloc {Tag:%lld} \r\n";
sprintf(str, format, classSignature,
tag);
memoryFile->write(str, sizeof(char) * strlen(str));
}
jvmti_env->Deallocate((unsigned char *) classSignature);
}
我們可以看到,我們在中間做了一個jvmti_env->SetTag的操作,這個是給這個分配的對象進行了一個打標簽的動作(我們需要觀察該對象是否被銷毀,所以需要一個唯一標識符),我們會在釋放的時候用到。因為回調的操作可能會有很多,我們采用普通的io必定會導致native層的阻塞,所以這里就要靠我們的mmap登場了,通過mmap我們可以高效的處理頻繁的io,mmap不熟悉的可以看這篇,memoryFile->write是一個通過mmap的寫文件操作。
objectFree是我們的釋放內存的監聽,它的函數定義是
typedef void (JNICALL *jvmtiEventObjectFree)
(jvmtiEnv *jvmti_env,
jlong tag);
可以看到,我們在釋放內存的時候得到的信息非常有限,只有一個tag,也就是我們在分配內存時通過SetTag操作所得到的參數,如果有設置就就會為具體的tag數值。我們在這個函數中的業務邏輯就是記錄當次的釋放記錄即可
void JNICALL objectFree(jvmtiEnv *jvmti_env,
jlong tag) {
std::list<int>::iterator it = std::find(list1.begin(), list1.end(), tag);
if (it != list.end()) // 找到了
{
__android_log_print(ANDROID_LOG_ERROR, "hello", "release %lld",tag);
char str[500];
char *format = "release tag %lld\r\n";
//ALOGI(format, GetCurrentSystemTime().c_str(),threadInfo.name, classSignature, size, tag);
sprintf(str, format,tag);
memoryFile->write(str, sizeof(char) * strlen(str));
}
}
我們再回到上述代碼留下的疑問,list是個什么?其實就是記錄了我們在VMObjectAlloc階段所分配的屬于我們自定義的類的tag,因為ObjectFree提供給我們的信息非常有限,只有一個tag,如果不通過這個list保存分配內存時的tag的話,就會導致釋放的時候我們引入過多的不必要的釋放記錄。但是這里也帶來了一個問題,就是我們需要時刻同步list的狀態,因為jvmti是可以在多線程環境下回調,如果只是簡單操作list的話就會帶來同步問題(這里我們沒有處理,為了demo的簡單)真實操作上我們最好加入mutex鎖或者其他機制保證同步問題。
下面我們再給出memoryFile->write的代碼
currentSize 記錄當前大小 m_size 以頁為單位的默認大小
void MemoryFile::write(char *data, int dataLen) {
mtx.lock();
if(currentSize + dataLen >= m_size){
resize(currentSize+dataLen);
}
memcpy(ptr + currentSize, data, dataLen);
currentSize += dataLen;
mtx.unlock();
}
void MemoryFile::resize(int32_t needSize) {
// 如果mmap的大小不夠,就需要重新進行mmap操作,以頁為單位
int32_t oldSize = m_size;
do{
m_size *=2;
} while (m_size<needSize);
ftruncate(m_fd, m_size);
munmap(ptr, oldSize);
ptr = static_cast<int8_t *>(mmap(0,m_size,PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0));
}
開啟監聽
到這里,我們還沒有結束,我們需要真正的開啟監聽,前面只是設置監聽的操作,我們可以通過SetEventNotificationMode函數開啟真正監聽/關閉監聽
jvmtiError SetEventNotificationMode(jvmtiEventMode mode,
jvmtiEvent event_type,
jthread event_thread,
...) {
return functions->SetEventNotificationMode(this, mode, event_type, event_thread);
}
mode代表當前狀態,是個枚舉,event_type就是我們要開啟監聽的類型(這里我們指定為內存分配與釋放事件即可),event_thread可以指定某個線程的內存分配事件,null就是全局監聽,所以我們的業務代碼如下
//開啟監聽
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_VM_OBJECT_ALLOC, nullptr);
mJvmtiEnv->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_OBJECT_FREE, nullptr);
java層開啟agent
通過在native層設置了jvmti的監聽與實現,我們還要在java層通過Debug.attachJvmtiAgent(9.0)進行開啟,這里有細微差距
import android.content.Context
import android.os.Build
import android.os.Debug
import android.util.Log
import java.io.File
import java.nio.file.Files
import java.nio.file.Paths
import java.util.*
object MemoryMonitor {
private const val JVMTI_LIB_NAME = "libjvmti-monitor.so"
fun init(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//查找SO的路徑
val libDir: File = File(context.filesDir, "lib")
if (!libDir.exists()) {
libDir.mkdirs()
}
//判斷So庫是否存在,不存在復制過來
val libSo: File = File(libDir, JVMTI_LIB_NAME)
if (libSo.exists()) libSo.delete()
val findLibrary =
ClassLoader::class.java.getDeclaredMethod("findLibrary", String::class.java)
val libFilePath = findLibrary.invoke(context.classLoader, "jvmti-monitor") as String
Files.copy(
Paths.get(File(libFilePath).absolutePath), Paths.get(
libSo.absolutePath
)
)
//加載SO庫
val agentPath = libSo.absolutePath
System.load(agentPath)
//agent連接到JVMTI
attachAgent(agentPath, context.classLoader);
val logDir = File(context.filesDir, "log")
val path = "${logDir.absolutePath}/test.log"
initMemoryCallBack(path)
} else {
Log.e("memory", "jvmti 初始化異常")
}
}
//agent連接到JVMTI
private fun attachAgent(agentPath: String, classLoader: ClassLoader) {
//Android 9.0+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Debug.attachJvmtiAgent(agentPath, null, classLoader)
} else {
//android 9.0以下版本使用反射方式加載
val vmDebugClazz = Class.forName("dalvik.system.VMDebug")
val attachAgentMethod = vmDebugClazz.getMethod("attachAgent", String::class.java)
attachAgentMethod.isAccessible = true
attachAgentMethod.invoke(null, agentPath)
}
}
// 設置mmap的文件path
external fun initMemoryCallBack(path: String)
}
attachJvmtiAgent方法需要實現了jvmti 的so庫的絕對地址,那么我們如何查找一個so庫的地址呢?其實就是通過ClassLoader的findLibrary方法,我們可以獲取到so的絕對地址,不過這個絕對地址不能夠直接用,我們看一下源碼attachJvmtiAgent
public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
@Nullable ClassLoader classLoader) throws IOException {
Preconditions.checkNotNull(library);
Preconditions.checkArgument(!library.contains("="));
if (options == null) {
VMDebug.attachAgent(library, classLoader);
} else {
VMDebug.attachAgent(library + "=" + options, classLoader);
}
}
其中attachJvmtiAgent 會進行格式校驗Preconditions.checkArgument(!library.contains("=")),恰好我們得到的so的地址是包含=的,所以才需要一個File的copy操作(拷貝到一個不包含=的目錄下)
驗證分配數據
通過上面的jvmti操作,我們已經可以將數據保存到本地文件了,本地文件的保存可以自己定義,這里我保存在context.filesDir目錄中/log子目錄下,同時我們生成一個測試數據
package com.test.memory
data class TestData(val test:Int) {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.sampleText.text = "Hello World"
TestData(1)
}
運行后
我們就完成了一個內存的記錄,通過該記錄我們就能夠分析哪些類引起了內存問題(即存在分配tag不存在釋放tag)
總結
原文鏈接:https://juejin.cn/post/7150098925532545054
相關推薦
- 2022-04-27 python?數據挖掘算法的過程詳解_python
- 2022-05-07 MongoDB連接和創建數據庫的方法講解_MongoDB
- 2022-09-20 react-redux?action傳參及多個state處理的實現_React
- 2022-07-07 關于C++智能指針shared_ptr和unique_ptr能否互轉問題_C 語言
- 2022-11-22 在?React?項目中全量使用?Hooks的方法_React
- 2023-02-10 docker容器間互相訪問(docker?bridge網絡)_docker
- 2022-10-17 Python可視化程序調用流程解析_python
- 2022-03-30 Python機器學習應用之樸素貝葉斯篇_python
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支