将class加载到内存,class在内存如何存储
加载、链接(验证、准备、解析)、初始化(静态字段)
何时初始化:
public class Singleton {
private Singleton() {}
private static class LazyHolder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return LazyHolder.INSTANCE;
}
}
由于类初始化是线程安全的,并且仅被执行一次
存放对象实例,垃圾收集器管理的区域
1.8以前方法区的实现
堆区
1.8以后方法区的实现
直接内存(os内存)
gc算法简单,oom(cglib、反射能生成代码),堆区太小
字符串 1.6 永久代
字符串 1.7以后 堆区
虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码
存放类信息(类名、访问修饰符、常量池、字段描述、方法描述)
// 方法区里一个类的大致类信息
type Class struct {
accessFlags uint16
name string
superClassName string
interfaceNames []string
constantPool *ConstantPool // 运行时常量池
fields []*Field
methods []*Method
loader *ClassLoader
superClass *Class
interfaces []*Class
instanceSlotCount uint
staticSlotCount uint
staticVars Slots
}
运行时常量池主要存放两类信息:字面量和符号引用。字面量包括整数、浮点数和字符串字面量;符号引用包括类符号引用、字段符号引用、方法符号引用和接口方法符号引用
class文件中常量池 静态
文件里
运行时常量池 动态(hsdb可以查看)
instanceKlass的一个属性
constantPool
方法区
字符串常量池 StringTable
堆区
class InstanceKlass: public Klass {
...
// Constant pool for this class.
ConstantPool* _constants;
}
一个String对象有两个oop,一个是String,另一给是char[]
s2先从字符串常量池中找,找到即把String赋值给他
字符串常量池即一个hashtable,key为string的hashcode,然后取模作为index,放到数组中,用链表解决冲突
// string_or_null 字符串对象
// name 字符串原始指针
// len 字符串长度
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
// 获取字符串的 hash 值
unsigned int hashValue = hash_string(name, len);
// 算出 hash table 桶下标
int index = the_table()->hash_to_index(hashValue);
// 看字符串在 hash table 中有没有
oop found_string = the_table()->lookup(index, name, len, hashValue);
// 如果有,直接返回(避免重复加入)
if (found_string != NULL) {
// 确保该字符串对象没有被垃圾回收
ensure_string_alive(found_string);
return found_string;
}
debug_only(StableMemoryChecker smc(name, len * sizeof(name[0])));
assert(!Universe::heap()->is_in_reserved(name),
"proposed name of symbol must be stable");
Handle string;
// try to reuse the string if possible
if (!string_or_null.is_null()) {
string = string_or_null;
} else {
// 根据 unicode 创建【字符串对象 string】
string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
}
#if INCLUDE_ALL_GCS
if (G1StringDedup::is_enabled()) {
// Deduplicate the string before it is interned. Note that we should never
// deduplicate a string after it has been interned. Doing so will counteract
// compiler optimizations done on e.g. interned string literals.
G1StringDedup::deduplicate(string());
}
#endif
// Grab the StringTable_lock before getting the_table() because it could
// change at safepoint.
oop added_or_found;
{
MutexLocker ml(StringTable_lock, THREAD);
// 将【字符串对象 string】加入 hash table
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
ensure_string_alive(added_or_found);
return added_or_found;
}
// index 桶下标
// name 字符串原始指针
// len 字符串长度
// hash 哈希码
oop StringTable::lookup(int index, jchar* name,
int len, unsigned int hash) {
int count = 0;
for (HashtableEntry<oop, mtSymbol>* l = bucket(index); l != NULL; l = l->next()) {
count++;
if (l->hash() == hash) {
if (java_lang_String::equals(l->literal(), name, len)) {
return l->literal();
}
}
}
// 如果链表过长,需要 rehash
if (count >= rehash_count && !needs_rehashing()) {
_needs_rehashing = check_rehash_table(count);
}
return NULL;
}
将源文件解析为抽象语法树;
调用已注册的注解处理器;
生成字节码。
如果在第 2 步调用注解处理器过程中生成了新的源文件,那么编译器将重复第 1、2 步,解析并且处理新生成的源文件
注解处理器主要有三个用途。
一是定义编译规则,并检查被编译的源文件。
二是修改已有源代码。
三是生成新的源代码
// Bar.java
package test;
import java.util.function.IntBinaryOperator;
import foo.Adapt;
public class Bar {
@Adapt(IntBinaryOperator.class)
public static int add(int a, int b) {
return a + b;
}
}
根据处理器可以生成如下类
package test;
import java.util.function.IntBinaryOperator;
public class Bar_addAdapter implements IntBinaryOperator {
@Override
public int applyAsInt(int arg0, int arg1) {
return Bar.add(arg0, arg1);
}
}
动态代理能实现相同功能
HotSpot 虚拟机将为标注了@HotSpotIntrinsicCandidate注解的方法额外维护一套高效实现
高效实现通常依赖于具体的 CPU 指令,而这些 CPU 指令不好在 Java 源程序中表达。再者,换了一个体系架构,说不定就没有对应的 CPU 指令,也就无法进行 intrinsic 优化了
intrinsic 的实现有两种。一是不大常见的桩程序,可以在解释执行或者即时编译生成的代码中使用。二是特殊的 IR 节点。即时编译器将在方法内联过程中,将对 intrinsic 的调用替换为这些特殊的 IR 节点,并最终生成指定的 CPU 指令
最新版本的 HotSpot 虚拟机定义了三百多个 intrinsic。在这三百多个 intrinsic 中,有三成以上是Unsafe类的方法
对象头
实例数据
对齐填充
创建对象内存空间
// 解析字节码new关键字
CASE(_new): {
HeapWord* compare_to = *Universe::heap()->top_addr();
HeapWord* new_top = compare_to + obj_size;
if (new_top <= *Universe::heap()->end_addr()) {
if (Atomic::cmpxchg(new_top, Universe::heap()->top_addr(), compare_to) != compare_to) {
goto retry;
}
result = (oop) compare_to;
}
指针碰撞方法:采用cas,申请空间。如果当前堆的最大位置和上次取的位置相同,则设置新的最大位置。新的最大位置即对象大小+当前最大位置,用于存放对象。 申请失败,重试。disruptor队列的空间申请也是采用该方法。
引用计数(java没用)
可达性分析算法(java使用)
标记-清除算法
复制算法(现在商业虚拟机用该算法回收新生代)
标记-整理算法(现在商业虚拟机用该算法回收老生代)
分代收集算法(现在商业虚拟机采用以上两个算法)
jps:虚拟机进程状况工具
jstat:gc信息,内存相关
jinfo:java配置信息工具
jmap:堆中对象信息
jhat:堆转储快照分析工具
jstack:堆栈跟踪工具
-vmargs 说明后面是VM的参数,所以后面的其实都是JVM的参数了
-Xms128m JVM初始分配的堆内存,默认是物理内存的1/64
-Xmx512m JVM最大允许分配的堆内存,默认是物理内存的1/4
-XX:PermSize=64M JVM初始分配的非堆内存,默认是物理内存的1/64
-XX:MaxPermSize=128M JVM最大允许分配的非堆内存,默认是物理内存的1/4
常量池:Class文件开头有常量池入口,主要存放字面量,符号引用(类和接口、字段、方法的名字和描述符)
虚拟机加载class文件的时候进行动态链接,所以class文件不会保存各个方法、字段的最终内存布局信息
字段表、方法表有指向常量池的指针,用常量池来描述。
jvm用c/c++编写,可以直接调用机器代码
magicNumber
version
常量池constant_pool_count
constant_pool[]
Access_flag
this_class
super_class
Interfaces
interfaces_count
interfaces[]
fields_count
fields[]
methods_count
void
method[]
attributes_count
attributes[]
class Klass : public Metadata {
friend class VMStructs;
protected:
// note: put frequently-used fields together at start of klass structure
// for better cache behavior (may not make much of a difference but sure won't hurt)
enum { _primary_super_limit = 8 };
jint _layout_helper;
juint _super_check_offset;
Symbol* _name;
Klass* _secondary_super_cache;
// Array of all secondary supertypes
Array<Klass*>* _secondary_supers;
// Ordered list of all primary supertypes
Klass* _primary_supers[_primary_super_limit];
// java/lang/Class instance mirroring this class
oop _java_mirror;
// Superclass
Klass* _super;
// First subclass (NULL if none); _subklass->next_sibling() is next one
Klass* _subklass;
// Sibling link (or NULL); links all subklasses of a klass
Klass* _next_sibling;
Klass* _next_link;
// The VM's representation of the ClassLoader used to load this class.
// Provide access the corresponding instance java.lang.ClassLoader.
ClassLoaderData* _class_loader_data;
jint _modifier_flags; // Processed access flags, for use by Class.getModifiers.
AccessFlags _access_flags; // Access flags. The class/interface distinction is stored here.
// Biased locking implementation and statistics
// (the 64-bit chunk goes first, to avoid some fragmentation)
jlong _last_biased_lock_bulk_revocation_time;
markOop _prototype_header; // Used when biased locking is both enabled and disabled for this type
jint _biased_lock_revocation_count;
TRACE_DEFINE_KLASS_TRACE_ID;
// Remembered sets support for the oops in the klasses.
jbyte _modified_oops; // Card Table Equivalent (YC/CMS support)
jbyte _accumulated_modified_oops; // Mod Union Equivalent (CMS support)
....
}
普通的Java类在JVM中对应的是instanceKlass类的实例,再来说下它的三个子类
Java中的数组不是静态数据类型,是动态数据类型,即是运行期生成的,Java数组的元信息用ArrayKlass的子类来表示:
InstanceKlass 保存class元信息,存放在方法区
InstanceMirrorKlass的实例即Class对象,可以用于反射,存放在堆中
// An InstanceMirrorKlass is a specialized InstanceKlass for java.lang.Class instances.
静态变量放在class对象中,也在堆中
(JDK7以上版本,静态域存储于定义类型的Class对象中,Class对象如同堆中其他对象一样,存在于GC堆中)逻辑上属于方法区?
klass保存类元信息,保存在perm永久区(1.8之后是元空间),oop保存类实例,保存在heap堆区
klass是class在jvm的表现形式
class Klass : public Metadata {
// java/lang/Class instance mirroring this class
oop _java_mirror;
}
这个属性代表的就是本class的Class对象。举例来说,如果JVM加载了Main这个类,那么除了为Main创建了一个名为”Main”的Klass,还默默地背后创建一个object,并且把这个object 挂到了 Klass 的 _java_mirror
属性上了。
就是Main的class对象
Class m = Main.class;
Class m = Class.forName("Main");
这两种方法都能访问到Main的Class object,也就是Klass
上那个不起眼的_java_mirror
。那么这个_java_mirror上定义的 newInstance 方法,其实最终也是通过JVM中的方法来创建真正的对象:
JVM_ENTRY(jobject, JVM_NewInstanceFromConstructor(JNIEnv *env, jobject c, jobjectArray args0))
JVMWrapper("JVM_NewInstanceFromConstructor");
oop constructor_mirror = JNIHandles::resolve(c);
objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));
oop result = Reflection::invoke_constructor(constructor_mirror, args, CHECK_NULL);
jobject res = JNIHandles::make_local(env, result);
if (JvmtiExport::should_post_vm_object_alloc()) {
JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);
}
return res;
JVM_END
只要知道JVM可以通过java_mirror
找到真正的Klass
,然后再用这个Klass
创建一个真正的对象就可以了。
oop是对象在jvm的表现形式
加载
验证
文件格式、元数据验证、字节码验证、符号引用验证
准备
解析
初始化
不同加载器加载的class在各自的方法区区域
java程序调用c、c++函数
例如:
线程的一种实现:
java thread 通过jvm调用os的pthread_create,创建线程,然后jni反向调用java thread的run函数
start — native start0 — pthread_create(java_start)
java_start 反向调用 run函数
JNI的步骤
装载库,保证JVM在启动的时候就会装载,故而一般是也给static
System.loadLibrary( "HelloNative" );
编译成class文件
javac xxxx
生成.h头文件
javah 报名+类名
生存的。h文件需要放到包当中
编写C文件,C文件的方法需要参考.h文件 NIEnv *env, jobject c1
编译一个动态链接库
gcc -fPIC -I /usr/lib/jvm/java-1.8.0-openjdk/include -I /usr/lib/jvm/java-1.8.0-openjdk/include/linux -shared -o liblubanNet.so server.c
把这个库所在的目录添加到path
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxxxx
在调用 native 方法前,Java 虚拟机需要将该 native 方法链接至对应的 C 函数上
两种方式
第一种是让 Java 虚拟机自动查找符合默认命名规范的 C 函数,并且链接起来。
例如
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class org_example_Foo */
#ifndef _Included_org_example_Foo
#define _Included_org_example_Foo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: org_example_Foo
* Method: foo
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_org_example_Foo_foo
(JNIEnv *, jclass);
/*
* Class: org_example_Foo
* Method: bar
* Signature: (IJ)V
*/
JNIEXPORT void JNICALL Java_org_example_Foo_bar__IJ
(JNIEnv *, jobject, jint, jlong);
/*
* Class: org_example_Foo
* Method: bar
* Signature: (Ljava/lang/String;Ljava/lang/Object;)V
*/
JNIEXPORT void JNICALL Java_org_example_Foo_bar__Ljava_lang_String_2Ljava_lang_Object_2
(JNIEnv *, jobject, jstring, jobject);
#ifdef __cplusplus
}
#endif
#endif
第二种链接方式则是在 C 代码中主动链接
// 注:Object类的registerNatives方法的实现位于java.base模块里的C代码中
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
字节码被模版解释器解析成机器码
call_stub是实现jvm c程序调用java机器码的第一步,在jvm执行java主程序对应的第一条字节码指令之前,必须经过call_stub函数指针进入对应的例程,在目标例程中触发对java主函数第一条字节码指令的调用.
所以是先到例程,再到java函数,两个都是机器指令,例程是jvm启动时候生成的一段机器指定
CallStub是一个函数指针。
// Calls to Java
typedef void (*CallStub)(
address link,
intptr_t* result,
BasicType result_type,
Method* method,
address entry_point,
intptr_t* parameters,
int size_of_parameters,
TRAPS
);
static CallStub call_stub() { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }
_call_stub_entry在之前已经初始化过
StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);
jvm启动过程
java.c: main()
java_md.c: LoadJavaVM()
jni.c: JNI_CreateJavaVM()
Threads.c: create_vm()
init.c: init_globals()
StubRoutines.cpp: stubRoutines_init1()
StubRoutines.cpp: initialize1()
StubGenerator_x86_x32.cpp: StubGenerator_generate()
StubGenerator_x86_x32.cpp: StubCodeGenerator()
StubGenerator_x86_x32.cpp: generate_initial()
StubGenerator_x86_x32.cpp: generate_call_stub()
_call_stub_entry例程
address generate_call_stub(address& return_address) {
StubCodeMark mark(this, "StubRoutines", "call_stub");
address start = __ pc();
// stub code parameters / addresses
assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
bool sse_save = false;
const Address rsp_after_call(rbp, -4 * wordSize); // same as in generate_catch_exception()!
const int locals_count_in_bytes (4*wordSize);
const Address mxcsr_save (rbp, -4 * wordSize);
const Address saved_rbx (rbp, -3 * wordSize);
const Address saved_rsi (rbp, -2 * wordSize);
const Address saved_rdi (rbp, -1 * wordSize);
const Address result (rbp, 3 * wordSize);
const Address result_type (rbp, 4 * wordSize);
const Address method (rbp, 5 * wordSize);
const Address entry_point (rbp, 6 * wordSize);
const Address parameters (rbp, 7 * wordSize);
const Address parameter_size(rbp, 8 * wordSize);
const Address thread (rbp, 9 * wordSize); // same as in generate_catch_exception()!
sse_save = UseSSE > 0;
// stub code
__ enter();
__ movptr(rcx, parameter_size); // parameter counter
__ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes
__ addptr(rcx, locals_count_in_bytes); // reserve space for register saves
__ subptr(rsp, rcx);
__ andptr(rsp, -(StackAlignmentInBytes)); // Align stack
// save rdi, rsi, & rbx, according to C calling conventions
__ movptr(saved_rdi, rdi);
__ movptr(saved_rsi, rsi);
__ movptr(saved_rbx, rbx);
// save and initialize %mxcsr
if (sse_save) {
Label skip_ldmx;
__ stmxcsr(mxcsr_save);
__ movl(rax, mxcsr_save);
__ andl(rax, MXCSR_MASK); // Only check control and mask bits
ExternalAddress mxcsr_std(StubRoutines::addr_mxcsr_std());
__ cmp32(rax, mxcsr_std);
__ jcc(Assembler::equal, skip_ldmx);
__ ldmxcsr(mxcsr_std);
__ bind(skip_ldmx);
}
// make sure the control word is correct.
__ fldcw(ExternalAddress(StubRoutines::addr_fpu_cntrl_wrd_std()));
_call_stub_entry例程的作用就是一段机器代码,做的事情是进入java主函数机器代码之前,把栈环境配置好,因为是两段独立环境的代码,jvm和java,调用函数前会有很多栈的操作(函数入参压栈,调用前的环境压栈),需要要手动配置栈。而同一个环境例如java函数之间的调用,编译器会帮我们配置栈环境。
所以通过jvm事先写好的例程,使得程序调用可以在jvm和java之间穿梭,jvm可以调用java,java可以调用jvm。
https://bboyjing.github.io/2017/02/08/自己动手写JVM十八【类和对象(一)】/
https://blog.csdn.net/m0_45406092/article/details/109822572
https://zhuanlan.zhihu.com/p/110307661
https://zhuanlan.zhihu.com/p/111809384