在heap上查找class的对象实例

本文内容

  1. 如何使用 JNI
  2. 如何在 JNI 中调用 JVMTI 在heap上根据class查找对象
  3. 对于如何在实践中使用本篇内容,可以参考:
    3.1 使用Instrumentation和Javassist修改web应用字节码
    3.2 查找WebServer中各个App的ClassLoader

前言

  1. 在 java 中,获取对象的class很容易,但是反过来,要获取class所产生的对象却是没有直接办法的。
  2. 为什么需要查找class的对象实例呢?因为很多时候,我们需要在运行时,动态修改对象的字节码来完成一些有趣的事情,但往往我们只知道这些对象的类型,也就是class,却缺少查找它们的手段。
  3. 其实JVM提供了一套丰富的API给我们来使用,也就是JVMTI(JVM Tool Interface)。这套API可以在Instrumentation中来使用,也可以通过JNI来调用。本文将介绍如何通过JNI来调用JVMTI,从而在heap上根据class来查找对象。
  4. 可能会有人担心使用了JNI会不会丧失了平台可移植性,答案是肯定的。但是对于同样的系统,其实可移植性还在,如果是不同的系统,那就只能重新编译了。

生成native方法的头文件

java代码:

package test.jni;

public class JvmtiTest {
    public native Object findObjectByClass(Class<?> clazz);
}

在某个目录输入以下命令:

~/test_jni/jvmti_test $ javac -h . $YourJavaDir/JvmtiTest.java

就会在该目录下生成一个 .h 文件

drwxrwxr-x 2 helowken helowken 4096 Jul 23 23:14 ./
drwxrwxr-x 6 helowken helowken 4096 Jul 23 23:14 ../
-rw-rw-r-- 1 helowken helowken  486 Jul 23 23:14 test_jni_JvmtiTest.h

你可以查看它的内容:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class test_jni_JvmtiTest */

#ifndef _Included_test_jni_JvmtiTest
#define _Included_test_jni_JvmtiTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     test_jni_JvmtiTest
 * Method:    findObjectByClass
 * Signature: (Ljava/lang/Class;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_test_jni_JvmtiTest_findObjectByClass
  (JNIEnv *, jobject, jclass);

#ifdef __cplusplus
}
#endif
#endif

如果你没接触过JNI,可能会觉得有些迷糊,但不用担心,我们需要关注的东西不多,只要了解:

  1. .h 文件中包含了对应java类方法的native方法
    a) java 类方法:findObjectByClass(Class<?> clazz)
    b) native方法:Java_test_jni_JvmtiTest_findObjectByClass()
  2. 类型的映射
    a) java的Object -> native的 jobject
    b) java的Class -> native的 jclass
  3. native方法的参数
    a) 第一个参数是 JNIEnv 的指针,指向包含所有虚方法的结构
    b) 第二个参数类型是 jobject,对应的是 java 中的 this,如果java方法为static,则这个参数为NULL

生成native方法的实现文件

在同一个目录下增加 .h 的实现文件:test_jni_JvmtiTest.c
当前实现先留空

#include "jvmti.h"
#include "test_jni_JvmtiTest.h"

JNIEXPORT jobject JNICALL 
Java_test_jni_JvmtiTest_findObjectByClass(JNIEnv* env, jobject thisObj, jclass targetClass) {
	return NULL;
}

其中 jvmti.h 头文件是存放在 $JAVA_HOME/include 目录下的,这里用 “” 而不是 <>,是因为它不在系统搜索路径上,编译的时候需要自行加入。
这里附带一份 jvmti demo 的 makefile:

#
# Copyright (c) 2004, 2012, Oracle and/or its affiliates. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#   - Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
#
#   - Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#
#   - Neither the name of Oracle nor the names of its
#     contributors may be used to endorse or promote products derived
#     from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

########################################################################
#
# Sample GNU Makefile for building JVMTI Demo 
#
#  Example uses:    
#       gnumake JDK=<java_home> OSNAME=solaris [OPT=true] [LIBARCH=sparc]
#       gnumake JDK=<java_home> OSNAME=solaris [OPT=true] [LIBARCH=sparcv9]
#       gnumake JDK=<java_home> OSNAME=linux   [OPT=true]
#       gnumake JDK=<java_home> OSNAME=win32   [OPT=true]
#
########################################################################

# Source lists
LIBNAME=$(FILE)
SOURCES=$(FILE).c

# Solaris Sun C Compiler Version 5.5
ifeq ($(OSNAME), solaris)
    # Sun Solaris Compiler options needed
    COMMON_FLAGS=-mt -KPIC
    # Options that help find errors
    COMMON_FLAGS+= -Xa -v -xstrconst -xc99=%none
    # Check LIBARCH for any special compiler options
    LIBARCH=$(shell uname -p)
    ifeq ($(LIBARCH), sparc)
        COMMON_FLAGS+=-xarch=v8 -xregs=no%appl
    endif
    ifeq ($(LIBARCH), sparcv9)
        COMMON_FLAGS+=-xarch=v9 -xregs=no%appl
    endif
    ifeq ($(OPT), true)
        CFLAGS=-xO2 $(COMMON_FLAGS) 
    else
        CFLAGS=-g $(COMMON_FLAGS)
    endif
    # Object files needed to create library
    OBJECTS=$(SOURCES:%.c=%.o)
    # Library name and options needed to build it
    LIBRARY=lib$(LIBNAME).so
    LDFLAGS=-z defs -ztext
    # Libraries we are dependent on
    LIBRARIES= -lc
    # Building a shared library
    LINK_SHARED=$(LINK.c) -G -o $@
endif

# Linux GNU C Compiler
ifeq ($(OSNAME), linux)
    # GNU Compiler options needed to build it
    COMMON_FLAGS=-fno-strict-aliasing -fPIC -fno-omit-frame-pointer
    # Options that help find errors
    COMMON_FLAGS+= -W -Wall  -Wno-unused -Wno-parentheses
    ifeq ($(OPT), true)
        CFLAGS=-O2 $(COMMON_FLAGS) 
    else
        CFLAGS=-g $(COMMON_FLAGS) 
    endif
    # Object files needed to create library
    OBJECTS=$(SOURCES:%.c=%.o)
    # Library name and options needed to build it
    LIBRARY=lib$(LIBNAME).so
    LDFLAGS=-Wl,-soname=$(LIBRARY) -static-libgcc
    # Libraries we are dependent on
    LIBRARIES=-lc
    # Building a shared library
    LINK_SHARED=$(LINK.c) -z noexecstack -shared -o $@
endif

$(info $(SOURCES))
$(info $(OBJECTS))

# Windows Microsoft C/C++ Optimizing Compiler Version 12
ifeq ($(OSNAME), win32)
    CC=cl
    # Compiler options needed to build it
    COMMON_FLAGS=-Gy -DWIN32
    # Options that help find errors
    COMMON_FLAGS+=-W0 -WX
    ifeq ($(OPT), true)
        CFLAGS= -Ox -Op -Zi $(COMMON_FLAGS) 
    else
        CFLAGS= -Od -Zi $(COMMON_FLAGS) 
    endif
    # Object files needed to create library
    OBJECTS=$(SOURCES:%.c=%.obj)
    # Library name and options needed to build it
    LIBRARY=$(LIBNAME).dll
    LDFLAGS=
    # Libraries we are dependent on
    LIBRARIES=
    # Building a shared library
    LINK_SHARED=link -dll -out:$@
endif

# Common -I options
CFLAGS += -I.
CFLAGS += -I$(JDK)/include -I$(JDK)/include/$(OSNAME)

# Default rule
all: $(LIBRARY)

# Build native library
$(LIBRARY): $(OBJECTS)
	$(LINK_SHARED) $(OBJECTS) $(LIBRARIES)

# Cleanup the built bits
clean:
	rm -f $(LIBRARY) $(OBJECTS)

# Simple tester
test: all
	LD_LIBRARY_PATH=`pwd` $(JDK)/bin/java -agentlib:$(LIBNAME) -version

# Compilation rule only needed on Windows
ifeq ($(OSNAME), win32)
%.obj: %.c
	$(COMPILE.c) $<
endif

你可以不用管它的内容,甚至不用看明白它,如果你想知道怎么使用,可以看看它的注释:Example uses。
为了方便地运行,增加以下脚本 run_make

#!/bin/bash

FILE=$1
args="-f ../makefile JDK=/usr/jdk1.8 OSNAME=linux FILE=$FILE"


make clean $args
echo ------------------------------
make $args

我的目录结构:

~/test_jni $ tree
.
├── jvmti_test
│   ├── test_jni_JvmtiTest.c
│   └── test_jni_JvmtiTest.h
├── makefile
└── run_make

在目录 jvmti_test 里面运行命令:

~/test_jni/jvmti_test $ ../run_make test_jni_JvmtiTest

输出:

test_jni_JvmtiTest.c
test_jni_JvmtiTest.o
rm -f libtest_jni_JvmtiTest.so test_jni_JvmtiTest.o
------------------------------
test_jni_JvmtiTest.c
test_jni_JvmtiTest.o
cc -g -fno-strict-aliasing -fPIC -fno-omit-frame-pointer -W -Wall  -Wno-unused -Wno-parentheses  -I. -I/usr/jdk1.8/include -I/usr/jdk1.8/include/linux   -c -o test_jni_JvmtiTest.o test_jni_JvmtiTest.c
cc -g -fno-strict-aliasing -fPIC -fno-omit-frame-pointer -W -Wall  -Wno-unused -Wno-parentheses  -I. -I/usr/jdk1.8/include -I/usr/jdk1.8/include/linux  -Wl,-soname=libtest_jni_JvmtiTest.so -static-libgcc  -z noexecstack -shared -o libtest_jni_JvmtiTest.so test_jni_JvmtiTest.o -lc

目录下会多出两个文件:
test_jni_JvmtiTest.o
libtest_jni_JvmtiTest.so (这个就是我们将要使用的库文件)

测试JNI的调用

修改 JvmtiTest.java的代码:

package test.jni;

public class JvmtiTest {
    static {
        System.loadLibrary("test_jni_JvmtiTest");
    }

    public native Object findObjectByClass(Class<?> clazz);

    public static void main(String[] args) {
        System.out.println(new JvmtiTest().findObjectByClass(JvmtiTest.class));
    }
}

注意:static 代码区在运行时会加载名为 “test_jni_JvmtiTest” 的库文件
运行会报错:

Exception in thread "main" java.lang.UnsatisfiedLinkError: no test_jni_JvmtiTest in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)
	at test.jni.JvmtiTest.<clinit>(JvmtiTest.java:5)

其实你也会好奇:jvm怎么知道 “test_jni_JvmtiTest” 这个库文件在哪里?
确实,jvm是不知道的,所以我们需要告诉它从哪里加载。
加入jvm启动参数,把库文件所在的目录传递给它:

-Djava.library.path=/home/helowken/test_jni/jvmti_test

再次运行,发现没有报错,返回结果:null,那是因为我们的 native 方法实现直接返回了 NULL。
上面这种加载方式,需要jvm启动的时候就指定库文件目录,不够灵活,你可以换一种更好的方式,修改 JvmtiTest.java:

package test.jni;

public class JvmtiTest {
    public native Object findObjectByClass(Class<?> clazz);

    public static void main(String[] args) {
        System.load("/home/helowken/test_jni/jvmti_test/libtest_jni_JvmtiTest.so");
        JvmtiTest jvmtiTest = new JvmtiTest();
        System.out.println(jvmtiTest);
        System.out.println(jvmtiTest.findObjectByClass(JvmtiTest.class));
    }
}

改用 System.load 方法,传入库文件的绝对路径,就可以实现动态加载。

在native方法中调用JVMTI

回到 test_jni_JvmtiTest.c 文件,增加以下实现代码:

#include "jvmti.h"
#include "test_jni_JvmtiTest.h"

static jint JNICALL
TagObjectByClass(jlong classTag, jlong size, jlong* tagPtr, jint length, void* userData) {
	*tagPtr = 1;
	return JVMTI_VISIT_ABORT;
}

JNIEXPORT jobject JNICALL 
Java_test_jni_JvmtiTest_findObjectByClass(JNIEnv* env, jobject thisObj, jclass targetClass) {
	JavaVM* vm;
	(*env)->GetJavaVM(env, &vm);

	jvmtiEnv* jvmti;
	(*vm)->GetEnv(vm, (void**) &jvmti, JVMTI_VERSION_1_0);

	jvmtiCapabilities capabilities = {0};
	capabilities.can_tag_objects = 1;
	(*jvmti)->AddCapabilities(jvmti, &capabilities);

	jvmtiHeapCallbacks callbacks = {};
	callbacks.heap_iteration_callback = TagObjectByClass;
	(*jvmti)->IterateThroughHeap(jvmti, 0, targetClass, &callbacks, NULL);

	jlong tag = 1;
	jint count;
	jobject* instances;
	(*jvmti)->GetObjectsWithTags(jvmti, 1, &tag, &count, &instances, NULL);

	if (instances != NULL) {
		jobject result = *instances;
		(*jvmti)->Deallocate(jvmti, (unsigned char*) instances);
		return result;
	}
	return NULL;
}

看不懂没关系,保存后,重新运行命令:

~/test_jni/jvmti_test $ ../run_make test_jni_JvmtiTest

然后运行 JvmTest.java,你会看到:

test.jni.JvmtiTest@4dd8dc3
test.jni.JvmtiTest@4dd8dc3

表明 native 方法已经成功从heap上根据class来查找对象实例了。

JVMTI代码分析

  1. 获取 jvmtiEnv 的指针,它指向一个包含了所有 jvmti 虚方法的结构
	JavaVM* vm;
	(*env)->GetJavaVM(env, &vm);

	jvmtiEnv* jvmti;
	(*vm)->GetEnv(vm, (void**) &jvmti, JVMTI_VERSION_1_0);
  1. IterateThroughHeap api 的调用需要 can_tag_objects 的 capability,其他功能我们暂时不需要,所以 capabilities 初始化为 {0}。
	jvmtiCapabilities capabilities = {0};
	capabilities.can_tag_objects = 1;
	(*jvmti)->AddCapabilities(jvmti, &capabilities);
  1. 设置在heap上遍历对象时的callback方法
	jvmtiHeapCallbacks callbacks = {};
	callbacks.heap_iteration_callback = TagObjectByClass;

callback方法在命中对象时,给对象打上一个标记,用于后续根据标记获取对象,这里打上的标记为1。
JVMTI_VISIT_ABORT 的意思是终止后续的遍历,因为我们的例子只有一个对象,所以只要callback方法被调用,就可以结束heap的遍历了。

static jint JNICALL
TagObjectByClass(jlong classTag, jlong size, jlong* tagPtr, jint length, void* userData) {
	*tagPtr = 1;
	return JVMTI_VISIT_ABORT;
}

方法原型:

typedef jint (JNICALL *jvmtiHeapIterationCallback)
    (jlong class_tag, 
     jlong size, 
     jlong* tag_ptr, 
     jint length, 
     void* user_data);

参数说明:
class_tag: 对象的class的标记
size: 对象所占内存的字节数
tag_ptr: 用于给对象打标记的指针
length: 如果对象是数组,则此为数组长度,否则为-1
user_data: 用户在遍历heap时传入的自定义参数

  1. 开始遍历heap,在这里我们遍历所有对象,只对类型为 targetClass 的对象应用callback方法,不传递用户自定义数据。
(*jvmti)->IterateThroughHeap(jvmti, 0, targetClass, &callbacks, NULL);

方法原型:

jvmtiError
IterateThroughHeap(jvmtiEnv* env,
            jint heap_filter,
            jclass klass,
            const jvmtiHeapCallbacks* callbacks,
            const void* user_data)

参数说明:
heap_filter: heap 过滤标志,可根据class和对象有没有被标记过,从而过滤掉不需要的对象
klass: callback只应用于类型为 klass 的对象,不会应用于 klass 的子类对象,如果 klass 是接口,那么 callback 不起作用。如果 klass 为 NULL,则 callback 方法应用于所有对象。
callbacks: 指向callback方法表的指针
user_data: 传递给callback的用户自定义参数

  1. 根据标记查找对象,标记是在 callback 方法中打上的,只打了一个标记,且值为1。
	jlong tag = 1;
	jint count;
	jobject* instances;
	(*jvmti)->GetObjectsWithTags(jvmti, 1, &tag, &count, &instances, NULL);

方法原型:

jvmtiError
GetObjectsWithTags(jvmtiEnv* env,
            jint tag_count,
            const jlong* tags,
            jint* count_ptr,
            jobject** object_result_ptr,
            jlong** tag_result_ptr)

参数说明:
tag_count: 标记数量,限制 tags 的遍历
tags:指向标记数组的指针
count_ptr: 指向“返回的对象指针数组的长度”的指针,限制 object_result_ptr 的遍历
object_result_ptr: 指向对象指针数组的指针
tag_result_ptr: 对于返回的每个对象,指向对象标记对应标记数组中的index

以上的代码省略了错误的处理,如果需要完全看懂上面 JVMTI 的代码,我建议你参考官方文档,有很详细的说明

参考资料

  1. Java Native Interface Specification Contents
  2. JVMTM Tool Interface
  3. Guide to JNI (Java Native Interface)
  4. JNI, How to list all the current instances?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值