1.通过HSDB观察堆栈引用信息
栈区域:
用来存放基本数据类型和引用数据类型的实例的(也就是实例对象的在堆中的首地址)还有就是堆栈是线程独享的。每一个线程都有自己的线程栈。
局部变量在栈内存中,JVM为每一个类分配一个栈帧,然后引用类型的局部变量指向堆内存中的地址),但是堆是内存中共享的区域,所以要考虑线程安全的问题。
堆区域:
用来存放程序动态生成的数据。(new 出来的对象的实例存储在堆中,但是仅仅存储的是成员变量,也就是平时所说的实例变量,成员变量的值则存储在常量池中。成员方法是此类所实现实例共享的,并不是每一次new 都会创建成员方法。成员方法被存储在方法区,并不是存储在第一个创建的对象中,因为那样的话,第一个对象被回收,后面创建的对象也就没有方法引用了。静态变量也存储在方法区中。
方法区(非堆):
在堆中为其分配的一部分内存):里面存储的是一些。类类型加载的东西(也就是反射中的.class之后的Class),用于存储已经被虚拟机加载的类的信息、常量、静态变量等。与堆一样,是被线程共享的内存区域,要注意线程安全问题。
所有的内存区域在一起,构成了JVM运行时内存分配:
以下列Java测试为例显示堆栈引用的查看:
package tool;
public class UserService {
public String name;
public UserService(String name) {
this.name = name;
}
public void debugMsg(){
System.out.println("name:"+name);
}
public static void main(String[] args) {
UserService user = new UserService("Job");
// 子线程循环调用 user 函数
Thread thread = new Thread(()->{
while (true){
try {
user.debugMsg();
Thread.sleep(1000);
}
catch (Exception e){
e.printStackTrace();
}
}
});
// 线程名称
thread.setName("debugMsgThread");
thread.start();
}
}
使用 HSDB 链接目标应用后,上面五个按钮分别功能如下,我们可以清楚的了解堆栈之间的引用关系,首先查看栈信息(Show the stack memory for the current thread):
这里选择目标线程 debugMsgThread,因为栈是在内存上分配的逻辑结构,实际上分析栈实际上就是分析栈帧中结构,看每个地址上面保存的信息。
其中最左第一列:
是栈的内存地址,按照从上到下由低到高分配。
第二列:
是栈这个内存地址上保存的值(大多数是其他对象的地址,表示栈这个内存地址上引用了其他的对象)
最右边彩色:
是 hotspot 对左侧栈内存地址上的说明。
Interpreted frame 表示一个栈帧这里共,
PSYoungGen tool/UserService 引用的一个对象,地址是 0x000000076da4ae80,这个地址可以去堆上面查找。
然后点击查看堆信息:
Tools->Object Histogram ,打开右边的Object Histogram面板,输入类路径查找后双击可以打开,会显示这个类创建了一个实例,并且地址是 0x000000076da4ae80:
2. Handler 句柄
栈帧:
栈帧(frame)在线程执行时和运行过程中用于保存线程的上下文数据,是垃圾回收中的最重要的根。
GC时通常第一步就是遍历根,java线程栈帧就是根元素之一,遍历整个栈帧的方式就是用过StackFrameStream,
其中封装了一个next指针,通过sender获得调用者的栈帧。我们将java的栈帧作为根来遍历堆,对其中的对象进行标记并收集垃圾。
在Java本地方法栈中,Java线程使用一个对象句柄存储块JNIHandleBlock来为其在本地方法中申请的临时对象创建对应的句柄。
每个JNIHandleBlock里面有一个oop数组,长度为32,如果超过数组长度则申请新的Block并通过next指针形成链表。
还有一个_pop_frame_link属性,用来保存Java线程切换方法时分配本地对象句柄的上下文从而形成调用handle的链表。
句柄
一个线程可以执行Java代码也可以执行Java的本地代码,如果Java本地代码(JVM内部的本地代码,JNI代码也算)需要引用堆里面的对象应该如何处理?
JVM并没区分本地方法栈和java栈(前面分析过Java函数调用,call_helper()函数为分界线,前面是 native栈,后面是java栈),如果通过栈进行处理必须要区分这两种情况。
因此JVM设计了另一个概念:HandleAera,
一块线程资源区,在这个区域分配句柄,并且管理所有的句柄。如果函数还在调用中,那么句柄有效,句柄关联的对象也就是活跃对象。
为了管理句柄的生命周期,引入了HandleMark,通常标记是分配在线程栈上的,在创建HandleMark的时候标记HandleArea对象有效。
当HandleMark对象析构的时候,从HandleArea中删除对象的引用。由于所有的句柄 handler 都形成了一个链表。因此通过整个链表可以获得本地代码执行中对堆对象的引用。
句柄和OOP对象关联,在HandleArea中有一个slot用于指向OOP对象。这个 HandArea 实际上就是
JNI句柄:
对于本地代码,并不归JVM直接管理,在执行JNI代码的时候,也有可能访问堆中的OOP对象。所以也需要一个机制进行管理,JVM同样引入类似的句柄机制,JNIHandle。
分为两种,全局和局部对象引用。局部对象引用最终调用了JNIHandleBlock来管理,因为JNIHandle没有设计一个JNIHandleMark的机制,所以创建时需要明确调用make_local,回收时也需要明确调用destory_local。
对于全局对象,在编译任务compilerTask中会访问method对象,需要把这些对象设置为全局的,否在可能在GC的时候被回收。
这两个部分的垃圾回收时的处理也是不同的,局部JNIhandle是通过线程,全局JNIhandle则是通过全局变量开始的
这里将 handler 直接理解为对象句柄,每个C++对象都会创建这个句柄,GC时对象的地址可能变化,那么直 接通过句柄去访问对象就不用管他的地址是否发生改变。handler的继承关系如图:
以下展示直接访问 Oop和 Klass。于通过 handler 句柄简介访问的区别:
当Oop地址发生变化时,为了避免修改直接引用时造成的多个引用更新,而直接将 handle 引用的 Oop 进行更新。Handle 类实际上就是对每个 Oop 的封装:
class Handle VALUE_OBJ_CLASS_SPEC {
private:
oop* _handle;
protected:
oop obj() const { return _handle == NULL ? (oop)NULL : *_handle; }
oop non_null_obj() const { assert(_handle != NULL, "resolving NULL handle"); return *_handle; }
public:
// Constructors
Handle() { _handle = NULL; }
Handle(oop obj);
Handle(Thread* thread, oop obj);
// General access
oop operator () () const { return obj(); }
oop operator -> () const { return non_null_obj(); }
bool operator == (oop o) const { return obj() == o; }
bool operator == (const Handle& h) const { return obj() == h.obj(); }
// Null checks
bool is_null() const { return _handle == NULL; }
bool not_null() const { return _handle != NULL; }
// Debugging
void print() { obj()->print(); }
// Direct interface, use very sparingly.
// Used by JavaCalls to quickly convert handles and to create handles static data structures.
// Constructor takes a dummy argument to prevent unintentional type conversion in C++.
Handle(oop *handle, bool dummy) { _handle = handle; }
// Raw handle access. Allows easy duplication of Handles. This can be very unsafe
// since duplicates is only valid as long as original handle is alive.
oop* raw_value() { return _handle; }
static oop raw_resolve(oop *handle) { return handle == NULL ? (oop)NULL : *handle; }
};
3.HandlerMark + HandlerArea + Chunk 维护引用信息
而 HandlerMark 用于在一个Java函数调用时对里面涉及到的 Oop 对象引用的生命周期进行管理,会绑定当前调用目标Java函数的线程。
HandleMark的定义在hotspot/src/share/vm/runtime/handles.hpp中,主要用于记录当前线程的HandleArea的内存地址top,执行方法调用后,HandleMark实例自动销毁。
在HandleMark的析构函数中会将HandleArea的当前内存地址到方法调用前的内存地址top之间的所有分配的oop都销毁掉,然后恢复当前线程的HandleArea的内存地址top到方法调用前的状态。
class HandleMark {
private:
Thread *_thread; // 表示拥有此HandleMark实例的Thread
HandleArea *_area; // 此HandleMark实例保存的HandleArea
Chunk *_chunk; // 保存HandleArea的Chunk
char *_hwm, *_max; // 保存的HandleArea的信息
size_t _size_in_bytes; // 保存的HandleArea的大小
HandleMark* _previous_handle_mark; // 指向当前线程的上一个活跃的HandleMark实例
void initialize(Thread* thread); // 初始化,将当前线程的_area的属性保存到新的HandleMark实例中。将当前HandleMark实例同线程关联起
void set_previous_handle_mark(HandleMark* mark) { _previous_handle_mark = mark; } //
HandleMark* previous_handle_mark() const { return _previous_handle_mark; }
size_t size_in_bytes() const { return _size_in_bytes; }
public:
HandleMark(); // see handles_inline.hpp
HandleMark(Thread* thread) { initialize(thread); }
~HandleMark();
// Functions used by HandleMarkCleaner
// called in the constructor of HandleMarkCleaner
void push();
// called in the destructor of HandleMarkCleaner
void pop_and_restore();
// overloaded operators
void* operator new(size_t size) throw();
void* operator new [](size_t size) throw();
void operator delete(void* p);
void operator delete[](void* p);
};
HandlerMark 中封装了 HandlerArea(这两个一起得到一组,他们是相互引用的),同样位于hotspot/src/share/vm/runtime/handles.hpp中,表示一片专门用于分配Handle的内存区域。也就是说 handler 都记录在一个 HandlerArea 中。
HandlerArea 自己定义了 _prev 将栈帧中的 HandlerArea 进行链接。另外三个函数就是用来保存使用到的所有对象 Oop 对应的 handler 信息。
* allocate_handle():用于分配保存oop的内存
* oops_do():垃圾回收时遍历对应引用
* used():获取已经使用的内存大小
HandlerArea 定义如下:
class HandleArea: public Arena {
friend class HandleMark;
friend class NoHandleMark;
friend class ResetNoHandleMark;
#ifdef ASSERT
int _handle_mark_nesting;
int _no_handle_mark_nesting;
#endif
HandleArea* _prev; // link to outer (older) area
public:
// Constructor
HandleArea(HandleArea* prev) : Arena(mtThread, Chunk::tiny_size) {
debug_only(_handle_mark_nesting = 0);
debug_only(_no_handle_mark_nesting = 0);
_prev = prev;
}
// Handle allocation
private:
oop* real_allocate_handle(oop obj) {
#ifdef ASSERT
oop* handle = (oop*) (UseMallocOnly ? internal_malloc_4(oopSize) : Amalloc_4(oopSize));
#else
oop* handle = (oop*) Amalloc_4(oopSize);
#endif
*handle = obj;
return handle;
}
public:
#ifdef ASSERT
oop* allocate_handle(oop obj);
#else
oop* allocate_handle(oop obj) { return real_allocate_handle(obj); }
#endif
// Garbage collection support
void oops_do(OopClosure* f);
// Number of handles in use
size_t used() const { return Arena::used() / oopSize; }
debug_only(bool no_handle_mark_active() { return _no_handle_mark_nesting > 0; })
};
也就是说 HandlerMark + HandlerArea + _chunk 这三个形成一组信息,保存当前线程对所有对象引用信息,其中Chunk是真正用来存储东西的区域,在堆中。
Chunk自己只有两个属性,_next记录下一个Chunk,_len记录长度,剩下一大片空白是额外分配来真真正正存储东西的区域。
_first 记录了第一个chunk的起始地址,_chunk记录了当前chunk的起始地址
_max记录了当前chunk的可用极限地址,_hwm是已经使用了的部分的顶部也就是水位线。
注意 Chunk 只是记录了一块一块内存分配的最小单位,但是实际上每个 Oop(Handler)真正使用了多大内存这个信息是在内存分配时才通过 chunk 维护的。
代码体现在
HandleArea 的两个函数中:
HandleArea real_allocate_handle(oop obj) => 这个函数就是传入一个 oop 对象,将 oop 封装成 handler 后返回,并调用 Amalloc_4
分配内存并记录 chunk 使用信息,分配从 hwm 值位置开始。
HandleArea allocate_handle(oop obj) => 传入一个 oop 对象。返回一个封装好的 handler,调用上面的函数进行分配。
oop* real_allocate_handle(oop obj) {
#ifdef ASSERT
oop* handle = (oop*) (UseMallocOnly ? internal_malloc_4(oopSize) : Amalloc_4(oopSize));
#else
oop* handle = (oop*) Amalloc_4(oopSize);
#endif
*handle = obj;
return handle;
}
oop* allocate_handle(oop obj) {
return real_allocate_handle(obj);
}
4.HandleMarkCleaner 清理
首先看Java普通方法最终调用的函数 call_helper:
JavaCalls::call_helper 整个代码如下:
void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
// 省略
// 检查目标Java方法是否是开启了首次执行必须被编译,是的话调用JIT编译器去编译目标方法
// 如果配置了-Xint选项就是解释模式执行,也就是需要运行时才进行编译那么就不会走这里
// 如果配置了编译执行则会走这里
assert(!thread->is_Compiler_thread(), "cannot compile from the compiler");
if (CompilationPolicy::must_be_compiled(method)) {
CompileBroker::compile_method(method, InvocationEntryBci,
CompilationPolicy::policy()->initial_compile_level(),
methodHandle(), 0, "must_be_compiled", CHECK);
}
// 获取保存在 address 属性上的 entry_point
// 每种 Java方法对应的 entry_point 会在 generate_normal_entry() 中进行创建,初始化三个东西 1.局部变量表 2.方法栈信息 3.操作数栈信息
// 创建的这些 entry_point 通过 _entry_table 进行缓存用于快速查找
// 在方法连接时 Method::link_method() 会调用 Interpreter::entry_for_method() 获取 Java 方法入口 entry_point
// 然后得到方法入口 entry_point 后调用 set_interpreter_entry() 进行保存,表存在属性 address 上
// 而这里就是将这个 entry_point 取出来,直接从 address 属性上获取
address entry_point = method->from_interpreted_entry();
if (JvmtiExport::can_post_interpreter_events() && thread->is_interp_only_mode()) {
entry_point = method->interpreter_entry();
}
BasicType result_type = runtime_type_from(result);
bool oop_result_flag = (result->get_type() == T_OBJECT || result->get_type() == T_ARRAY);
intptr_t* result_val_address = (intptr_t*)(result->get_value_addr());
Handle receiver = (!method->is_static()) ? args->receiver() : Handle();
if (thread->stack_yellow_zone_disabled()) {
thread->reguard_stack();
}
// to Java
if (!os::stack_shadow_pages_available(THREAD, method)) {
Exceptions::throw_stack_overflow_exception(THREAD, __FILE__, __LINE__, method);
return;
} else {
os::bang_stack_shadow_pages();
}
// 调用 Java Method
{ JavaCallWrapper link(method, receiver, result, CHECK);
{ HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
// StubRoutines::call_stub()函数返回一个函数指针,通过指针来调用这个函数
// Linux X86架构下的C/C++函数调用约定,在这个约定下,以下寄存器用于传递参数,六个参数以内用寄存器传递,操作六个则使用调用栈来传递额外的参数
// 第1个参数:rdi c_rarg0
// 第2个参数:rsi c_rarg1
// 第3个参数:rdx c_rarg2
// 第4个参数:rcx c_rarg3
// 第5个参数:r8 c_rarg4
// 第6个参数:r9 c_rarg5
StubRoutines::call_stub()(
(address)&link, // 类型为JavaCallWrapper
// (intptr_t*)&(result->_value),
result_val_address, // 函数返回值地址
result_type,
method(), // 当前要执行的 Java Method,这里获取出来是元数据信息,里面包含字节码信息
entry_point, // 用于后续帧开辟, entry_point 会从 method() 中获取出 Java Method 的第一个字节码命令(Opcode),也就是整个 Java Method 的调用入口
args->parameters(), // 就是 Java Method 的函数参数
args->size_of_parameters(), // 用函数调用Caller栈来传递,这个就是Java Method 的函数参数大小 size,占用内存大小(字节)
CHECK // 用函数调用Caller栈来传递,CHECK是宏,定义的线程对象 thread
);
result = link.result(); // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
}
if (oop_result_flag) {
result->set_jobject((jobject)thread->vm_result());
thread->set_vm_result(NULL);
}
}
在调用 StubRoutines::call_stub() 前会先构建一个JavaCallWrapper实例,然后再构造一个HandleMark实例。查看注释:
HandleMark used by HandleMarkCleaner,可以知道最终的 HandlerMarkCleaner 会负责 HandlerMark 中被标记了的 handler 引用。
HandleMarkCleaner的角色类似于JavaCallWrapper,通过HandleMarkCleaner的构造和析构方法在每次进入虚拟机前执行某些动作,是HandleMark的强化版,它依赖于JavaCalls::call_helper方法中在执行方法调用前会创建一个新的HandleMark。其定义在hotspot/src/share/vm/runtime/interfaceSurpport.hpp中,只有一个私有属性Thread* _thread,表示关联的线程。
定义如下:
class HandleMarkCleaner: public StackObj {
private:
Thread* _thread;
public:
HandleMarkCleaner(Thread* thread) {
_thread = thread;
_thread->last_handle_mark()->push();
}
~HandleMarkCleaner() {
_thread->last_handle_mark()->pop_and_restore();
}
private:
inline void* operator new(size_t size, void* ptr) throw() {
return ptr;
}
};
其中 HandleMarkCleaner构造时调用 HandlerMark 的 push(),在销毁时调用 HandlerMark 的 pop_and_restore()。