JVM的方法执行引擎-entry point栈帧

接着上一篇去讲,回到JavaCalls::call_helper()中:

address entry_point = method->from_interpreted_entry();

entry_point是从当前要执行的Java方法中获取的,定义如下:

源代码位置:/openjdk/hotspot/src/share/vm/oops/method.hpp
volatile address from_interpreted_entry() const{ 
      return (address)OrderAccess::load_ptr_acquire(&_from_interpreted_entry); 
}

那么_from_interpreted_entry是何时赋值的?之前在介绍方法连接时简单介绍过,在method.hpp中有这样一个set方法:

void set_interpreter_entry(address entry) { 
    _i2i_entry = entry;  
    _from_interpreted_entry = entry; 
}

在连接方法时通过如下的方法调用上面的方法:

// Called when the method_holder is getting linked. Setup entrypoints so the method
// is ready to be called from interpreter, compiler, and vtables.
void Method::link_method(methodHandle h_method, TRAPS) {
  // ...
  address entry = Interpreter::entry_for_method(h_method);
  assert(entry != NULL, "interpreter entry must be non-null");
  // Sets both _i2i_entry and _from_interpreted_entry
  set_interpreter_entry(entry);
  // ...
}

根据注释都可以得知,当方法连接时,会去设置方法的entry_point,entry_point是通过调用Interpreter::entry_for_method()方法得到,这个方法的实现如下:

static address entry_for_method(methodHandle m)  { 
     return entry_for_kind(method_kind(m)); 
}

首先通过method_kind()拿到方法类型,然后调用entry_for_kind()方法根据方法类型获取方法入口entry point。调用的entry_for_kind()方法如下:

static address entry_for_kind(MethodKind k){ 
      return _entry_table[k]; 
}

这里直接返回了_entry_table数组中对应方法类型的entry_point地址。给数组中元素赋值专门有个方法:

void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {
  _entry_table[kind] = entry;
}

那么何时会调用set_entry_for_kind ()呢,答案就在TemplateInterpreterGenerator::generate_all()中,generate_all()会调用generate_method_entry()去生成每种方法的entry_point,所有Java方法的执行,都会通过对应类型的entry_point例程来辅助。下面来详细介绍一下generate_all()方法的实现逻辑。 

HotSpot在启动时,会为所有字节码创建在特定目标平台上运行的机器码,并存放在CodeCache中,在解释执行字节码的过程中,就会从CodeCache中取出这些本地机器码并执行。

在启动虚拟机阶段会调用init_globals()方法初始化全局模块,在这个方法中通过调用interpreter_init()方法初始化模板解释器,调用栈如下:

TemplateInterpreter::initialize()    templateInterpreter.cpp
interpreter_init()                   interpreter.cpp
init_globals()                       init.cpp
Threads::create_vm()                 thread.cpp
JNI_CreateJavaVM()                   jni.cpp
InitializeJVM()                      java.c
JavaMain()                           java.c
start_thread()                       pthread_create.c	

interpreter_init()方法主要是通过调用TemplateInterpreter::initialize()方法来完成逻辑,initialize()方法的实现如下:

源代码位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateInterpreter::initialize() {
  if (_code != NULL) 
       return;

  // 抽象解释器AbstractInterpreter的初始化,AbstractInterpreter是基于汇编模型的解释器的共同基类,
  // 定义了解释器和解释器生成器的抽象接口
  AbstractInterpreter::initialize();

  // 模板表TemplateTable的初始化,模板表TemplateTable保存了各个字节码的模板
  TemplateTable::initialize();

  // generate interpreter
  {
     ResourceMark rm;
     int code_size = InterpreterCodeSize;
     // CodeCache的Stub队列StubQueue的初始化
     _code = new StubQueue(new InterpreterCodeletInterface, code_size, NULL,"Interpreter");
     //  实例化模板解释器生成器对象TemplateInterpreterGenerator
     InterpreterGenerator g(_code);
  }

  // initialize dispatch table
  _active_table = _normal_table;
}

模板解释器的初始化包括如下几个方面:

(1)抽象解释器AbstractInterpreter的初始化,AbstractInterpreter是基于汇编模型的解释器的共同基类,定义了解释器和解释器生成器的抽象接口。

(2)模板表TemplateTable的初始化,模板表TemplateTable保存了各个字节码的模板(目标代码生成函数和参数);

(3)CodeCache的Stub队列StubQueue的初始化;

(4)解释器生成器InterpreterGenerator的初始化。

在执行InterpreterGenerator g(_code)代码时,调用InterpreterGenerator的构造函数,如下:

InterpreterGenerator::InterpreterGenerator(StubQueue* code) : TemplateInterpreterGenerator(code) {
   generate_all(); // down here so it can be "virtual"
}

调用的generate_all()方法将生成一系列HotSpot运行过程中所执行的一些公共代码的入口和所有字节码的InterpreterCodelet。这些入口包括:

  • error exits:出错退出处理入口
  • 字节码追踪入口(配置了-XX:+TraceBytecodes)
  • 函数返回入口
  • JVMTI的EarlyReturn入口
  • 逆优化调用返回入口
  • native调用返回值处理handlers入口
  • continuation入口
  • safepoint入口
  • 异常处理入口
  • 抛出异常入口
  • 方法入口(native方法和非native方法)
  • 字节码入口

部分重要的入口实现逻辑会在后面详细介绍,这里只看为非native方法入口(也就是普通的、没有native关键字修饰的Java方法)生成入口的逻辑。generate_all()方法中有如下调用语句:

#define method_entry(kind)                                                                    \
  {                                                                                           \
    CodeletMark cm(_masm, "method entry point (kind = " #kind ")");                           \
    Interpreter::_entry_table[Interpreter::kind] = generate_method_entry(Interpreter::kind);  \
  }  

method_entry(zerolocals)

其中method_entry是宏,扩展后如上的调用语句变为如下的形式:

Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);

_entry_table变量定义在AbstractInterpreter类中,如下:

// method entry points
static address    _entry_table[number_of_method_entries];     // entry points for a given method

number_of_method_entries表示方法类型的总数,使用方法类型做为数组下标就可以获取对应的方法入口。调用generate_method_entry()方法为各个类型的方法生成对应的方法入口,实现如下:

address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) {
  // determine code generation flags
  bool                   synchronized = false;
  address                entry_point = NULL;
  InterpreterGenerator*  ig_this = (InterpreterGenerator*)this;

  switch (kind) { // 根据方法类型kind生成不同的入口
  case Interpreter::zerolocals             : // zerolocals表示普通方法类型
	  break;
  case Interpreter::zerolocals_synchronized: // zerolocals表示普通的、同步方法类型
	  synchronized = true;
	  break;
  // ...
  }

  if (entry_point) {
     return entry_point;
  }

  return ig_this->generate_normal_entry(synchronized);
}

zerolocals表示正常的Java方法调用(包括Java程序的主函数),对于zerolocals来说,会调用ig_this->generate_normal_entry()方法生成入口。generate_normal_entry()方法会为执行的方法生成堆栈,而堆栈由局部变量表(用来存储传入的参数和被调用函数的局部变量)、帧数据和操作数栈这三大部分组成,所以方法会创建这3部分来辅助Java方法的执行。

之前在介绍CallStub栈帧时讲到过,如果要执行entry_point,那么栈帧的状态就如下图所示。

 

/src/cpu/x86/vm/templateInterpreter_x86_64.cpp文件中generate_normal_entry()方法在通过CallStub调用时,各个寄存器的状态如下:

rbx -> Method*
r13 -> sender sp
rsi -> entry point  

generate_normal_entry()方法的实现如下:

// Generic interpreted method entry to (asm) interpreter
address InterpreterGenerator::generate_normal_entry(bool synchronized) {
  // determine code generation flags
  bool inc_counter  = UseCompiler || CountCompiledCalls;
 
  // 执行如下方法前的寄存器中保存的值如下:
  // ebx: Method*
  // r13: sender sp  
  address entry_point = __ pc();   // entry_point函数的代码入口地址
 
  // 当前rbx中存储的是指向Method的指针,通过Method*找到ConstMethod*
  const Address constMethod(rbx, Method::const_offset()); 
  // 通过Method*找到AccessFlags
  const Address access_flags(rbx, Method::access_flags_offset()); 
  // 通过ConstMethod*得到parameter的大小
  const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset());
  // 通过ConstMethod*得到local变量的大小
  const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());
 
  // 上面已经说明了获取各种方法元数据的计算方式,但并没有执行计算,下面会生成对应的汇编来执行计算
  // get parameter size (always needed)
  __ movptr(rdx, constMethod);                     // 计算ConstMethod*,保存在rdx里面
  __ load_unsigned_short(rcx, size_of_parameters); // 计算parameter大小,保存在rcx里面
  //rbx:保存基址;rcx:保存循环变量;rdx:保存目标地址;rax:保存返回地址(下面用到)
 
  // 此时的各个寄存器中的值如下:
  // rbx: Method*
  // rcx: size of parameters
  // r13: sender_sp (could differ from sp+wordSize if we were called via c2i ) 即调用者的栈顶地址
  // 计算local变量的大小,保存到rdx
__ load_unsigned_short(rdx, size_of_locals);
  // 由于局部变量表用来存储传入的参数和被调用函数的局部变量,所以rdx减去rcx后就是被调用函数的局部变量可使用的大小 
  __ subl(rdx, rcx); 
 
 
  // see if we've got enough room on the stack for locals plus overhead.
  generate_stack_overflow_check();
 
  //返回地址是在call_stub中保存的,如果不弹出堆栈到rax,那么局部变量区就如下面的样子:
  // [parameter 1]
  // [parameter 2]
  // ......
  // [parameter n]
  // [return address]
  // [local 1]
  // [local 2]
  // ...
  // [local n]
  // 显然中间有个return address使的局部变量表不是连续的,这会导致其中的局部变量计算方式不一致,所以暂时将返回地址存储到rax中
  // get return address
  __ pop(rax);
 
  // compute beginning of parameters (r14)
  // 计算第1个参数的地址:当前栈顶地址 + 变量大小 * 8 - 一个字大小。
  // 这儿注意,因为地址保存在低地址上,而堆栈是向低地址扩展的,所以只需加n-1个变量大小就可以得到第1个参数的地址。
  __ lea(r14, Address(rsp, rcx, Address::times_8, -wordSize));
 
  // 把函数的局部变量全置为0,也就是做初始化,防止之前遗留下的值影响
  // rdx:被调用函数的局部变量可使用的大小
  // allocate space for locals
  // explicitly initialize locals
  {
    Label exit, loop;
    __ testl(rdx, rdx);
    __ jcc(Assembler::lessEqual, exit); // do nothing if rdx <= 0
    __ bind(loop);
    __ push((int) NULL_WORD); // initialize local variables
    __ decrementl(rdx); // until everything initialized
    __ jcc(Assembler::greater, loop);
    __ bind(exit);
  }
 
  // 生成固定桢
  // initialize fixed part of activation frame
  generate_fixed_frame(false);
 
  // 省略统计及栈溢出等逻辑,后面会详细介绍
 
  // check for synchronized methods
  // Must happen AFTER invocation_counter check and stack overflow check,
  // so method is not locked if overflows.
  if (synchronized) {
    // Allocate monitor and lock method
    lock_method();
  } else {
    // no synchronization necessary
  }

  // 跳转到目标Java方法的第一条字节码指令,并执行其对应的机器指令
   __ dispatch_next(vtos);
 
  // 省略统计相关逻辑,后面会详细介绍
 
  return entry_point;
}

要对偏移的计算进行研究,如下:

// 当前rbx中存储的是指向Method的指针,通过Method*找到ConstMethod*
const Address constMethod(rbx, Method::const_offset()); 
// 通过Method*找到AccessFlags
const Address access_flags(rbx, Method::access_flags_offset()); 
// 通过ConstMethod*得到parameter的大小
const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset());
// 通过ConstMethod*得到local变量的大小
const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());

如果要打印这个方法生成的汇编代码,可以在方法的return语句之前添加如下2句打印代码:

address end =  __ pc();
Disassembler::decode(entry_point, end);

这样,在执行Disassembler::decode()方法时,会将此方法生成的机器码转换为汇编打印到控制台上。

调用generate_fixed_frame()方法之前生成的汇编代码如下:

Loaded disassembler from /home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/hsdis-amd64.so
[Disassembling for mach='i386:x86-64']
  0x00007fffe101e2e0: mov    0x10(%rbx),%rdx // 通过%rbx中保存的Method*找到ConstMethod并保存到%rdx
  0x00007fffe101e2e4: movzwl 0x2a(%rdx),%ecx // 通过ConstMethod*找到入参数量保存在%ecx
  0x00007fffe101e2e8: movzwl 0x28(%rdx),%edx // 通过ConstMethod*找到本地变量表大小保存在%edx
  0x00007fffe101e2ec: sub    %ecx,%edx       // 计算方法局部变量可使用的本地变量空间的大小并保存在%edx

  // ... 省略调用generate_stack_overflow_check()方法生成的汇编

  0x00007fffe101e43d: pop    %rax                   // 弹出返回地址
  0x00007fffe101e43e: lea    -0x8(%rsp,%rcx,8),%r14 // 计算第一个参数的地址
  // 为局部变量slot(不包括方法入参)分配堆栈空间并初始化为0
  //  循环进行本地变量表空间的开辟
  // -- loop --
  0x00007fffe101e443: test   %edx,%edx  
  0x00007fffe101e445: jle    0x00007fffe101e454 // 由于%edx的大小等于0,所以不需要额外分配,直接跳转到exit
  0x00007fffe101e44b: pushq  $0x0
  0x00007fffe101e450: dec    %edx
  0x00007fffe101e452: jg     0x00007fffe101e44b // 如果%edx的大小不等于0,跳转到loop 

现在栈的状态如下图所示。

 

 现在r14指向局部变量开始的位置,而argument和local variable都存储在了局部变量表,rbp指向了局部变量表结束位置。现在各个寄存器的状态如下: 

rax: return address  // %rax寄存器中存储的是返回地址return address
rbx: Method*
r14: pointer to locals
r13: sender sp

在InterpreterGenerator::generate_normal_entry()函数中,接下来会以这样的状态调用generate_fixed_frame()函数来创建Java方法运行时所需要的栈帧。generate_fixed_frame()函数会在下一篇详细介绍。

调用后栈帧变为如下的状态:

上图右边的栈状态随着具体方法的不同会显示不同的状态,不过大概的状态就是上图所示的样子。

调用完generate_fixed_frame()方法后一些寄存器中保存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第1个参数的地址

执行完generate_fixed_frame()方法后会继续执行InterpreterGenerator::generate_normal_entry()函数,如果是为同步方法生成机器码,那么还需要调用lock_method()方法,这个方法会改变当前栈的状态,添加同步所需要的一些信息,在后面介绍锁的实现时会详细介绍。

InterpreterGenerator::generate_normal_entry()函数最终会返回生成机器码的入口执行地址,然后通过变量_entry_table数组来保存,这样就可以使用方法类型做为数组下标获取对应的方法入口了。  

相关文章的链接如下:

1、在Ubuntu 16.04上编译OpenJDK8的源代码 

2、调试HotSpot源代码

3、HotSpot项目结构 

4、HotSpot的启动过程 

5、HotSpot二分模型(1)

6、HotSpot的类模型(2)  

7、HotSpot的类模型(3) 

8、HotSpot的类模型(4)

9、HotSpot的对象模型(5)  

10、HotSpot的对象模型(6) 

11、操作句柄Handle(7)

12、句柄Handle的释放(8)

13、类加载器 

14、类的双亲委派机制 

15、核心类的预装载

16、Java主类的装载  

17、触发类的装载  

18、类文件介绍 

19、文件流 

20、解析Class文件 

21、常量池解析(1) 

22、常量池解析(2)

23、字段解析(1)

24、字段解析之伪共享(2) 

25、字段解析(3)  

26、字段解析之OopMapBlock(4)

27、方法解析之Method与ConstMethod介绍  

28、方法解析

29、klassVtable与klassItable类的介绍  

30、计算vtable的大小 

31、计算itable的大小 

32、解析Class文件之创建InstanceKlass对象 

33、字段解析之字段注入 

34、类的连接  

35、类的连接之验证 

36、类的连接之重写(1) 

37、类的连接之重写(2)

38、方法的连接  

39、初始化vtable 

40、初始化itable  

41、类的初始化 

42、对象的创建  

43、Java引用类型 

44、Java引用类型之软引用(1)

45、Java引用类型之软引用(2)

46、Java引用类型之弱引用与幻像引用  

47、Java引用类型之最终引用

48、HotSpot的垃圾回收算法  

49、HotSpot的垃圾回收器   

作者持续维护的个人博客  classloading.com

关注公众号,有HotSpot源码剖析系列文章!

   

 

原文地址:https://www.cnblogs.com/mazhimazhi/p/13516634.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


jinfo 命令可以用来查看 Java 进程运行的 JVM 参数,命令如下:[root@admin ~]# jinfo --helpUsage: jinfo [option] &lt;pid&gt; (to connect to running process) jinfo [option] &lt;executable &lt;core&gt; (to connect to a core file) jinfo [option] [serve
原文链接:https://www.cnblogs.com/niejunlei/p/5987611.htmlJava Virtual Machine Stacks,线程私有,生命周期与线程相同,描述的是Java方法执行的内存模型:每一个方法执行的同时都会创建一个栈帧(Stack Frame),由于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法的执行就对应着栈帧在虚拟机栈中的入栈,出栈...
java 语言, 开发者不能直接控制程序运行内存, 对象的创建都是由类加载器一步步解析, 执行与生成与内存区域中的; 并且jvm有自己的垃圾回收器对内存区域管理, 回收; 但是我们已经可以通过一些工具来在程序运行时查看对应的jvm内存使用情况, 帮助更好的分析与优化我们的代码;jps查看系统中有哪些java进程jps 命令类似与 linux 的 ps 命令,但是它只列出系统中所有的 Java 应用程序。 通过 jps 命令可以方便地查看 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息
1.jvm的简单抽象模型:  2.类加载机制     双亲委派模型是为了防止jdk核心类库被篡改,如果需要打破可以重写Classloader.loadClass方法。r 双亲委派模型:一个类加载器收到一个类的加载请求,他会先判断自身是否已存在该类,如果不存在上抛给上一级类加载器ClassLoad
堆外内存JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外内存,比如Netty,广泛使用了堆外内存,但是这部分的内存并不归JVM管理,GC算法并不会对它们进行回收,所以在使用堆外内存时,要格外小心,防止内存一直得不到释放,造成线上故障。堆外内存的申请和释放JDK的ByteBuffe
1.springboot和tomcat2.springcloud的请求如何通过网关鉴权?3.springmvc启动时组件的加载顺序?4.mybatis如何同时更新三条记录5.hibernate实现级联更新6.一个web程序应用程序启动时的加载流程7.如何向www.baidu.com地址发出请求时,并获取相应?8.???9.谈谈你对tcp/iptelnetudp协
堆设置-Xms256M:初始堆大小256M,默认为物理内存的1/64-Xmx1024M:最大堆大小1024M,默认为物理内存的1/4,等于与-XX:MaxHeapSize=64M-Xmn64M:年轻代大小为64M(JDK1.4后支持),相当于同时设置NewSize和MaxNewSize为64M-XX:NewSize=64M:初始年轻代大小-XX:MaxNewSize=256M:最大年轻代大小(默认
一.概述收集算法(JVM之垃圾回收-垃圾收集算法)是内存回收的抽象策略,垃圾收集器就是内存回收的具体实现。JVM规范对于垃圾收集器的应该如何实现没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。就像没有最好的算法一样,垃圾收集器
Java中的堆是JVM所管理的最大的一块内存空间,主要用于存放各种类的实例对象,如下图所示: 在Java中,堆被划分成两个不同的区域:新生代(Young)、老年代(Old)。新生代(Young)又被划分为三个区域:Eden、S0、S1。 这样划分的目的是为了使JVM能够更好的管理堆内存中的对象,包
JVM深入理解JVM(4)——如何优化JavaGC「译」 PostedbyCrowonAugust21,2017本文翻译自SangminLee发表在Cubrid上的”BecomeaJavaGCExpert”系列文章的第三篇《HowtoTuneJavaGarbageCollection》,本文的作者是韩国人,写在JDK1.8发布之前,虽然有些地
 JVM深入理解JVM(2)——GC算法与内存分配策略 PostedbyCrowonAugust10,2017说起垃圾收集(GarbageCollection,GC),想必大家都不陌生,它是JVM实现里非常重要的一环,JVM成熟的内存动态分配与回收技术使Java(当然还有其他运行在JVM上的语言,如Scala等)程序员在提升开
运行时数据区  线程独有本地方法栈、虚拟机栈、程序计数器这些与线程对应的数据区会随着线程开始和结束创建和销毁  整体公有元数据区(又称方法区)、堆区会随着虚拟机启动而创建,随着虚拟机退出而销毁 
java整个堆大小设置:Xmx和Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。永久代PermSize和MaxPermSize设置为老年代存活对象的1.2-1.5倍年轻代Xmx的设置为老年代存活对象的1-1.5倍老年代的内存大小设置为老年代存活对象的2-3倍BTW: Sun官方建议年轻代
栈顶缓存(Top-of-StackCashing)技术基于栈式架构得虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存读写次数 由于操作数是存储在内存重的,因此频繁地执行内存读/写操作必然影响速度。 综上
自用。同样的代码在不同的平台生成的机器码是不一样的,为什么java代码生成的字节码文件,能在不同的平台运行?因为不同版本的jdk里面的虚拟机会屏蔽不同操作系统在底层硬件与指令上的区别。栈:线程栈,局部变量存放栈内存区域。线程(分配一个栈)运行分配栈将局部变量放入内存。怎么放:栈
jconsole监控:1.java启动命令加上参数java-Djava.rmi.server.hostname=172.16.17.247-Dcom.sun.management.jmxremote-Dcom.sun.management.jmxremote.port=2099-Dcom.sun.management.jmxremote.authenticate=false-Dcom.sun.management.jmxremote.ssl=false -XX:+Unlock
类加载器分类publicclassStackStruTest{publicstaticvoidmain(String[]args){//对用户自定义个类来说:默认使用系统类加载器进行加载-----AppClassLoaderClassLoaderclassLoader=StackStruTest.class.getClassLoader();System.out.p
堆体系结构一个JVM实例只存在一个堆内存,堆内存的大小是可调节的。类加载器读取类文件后,需要把类、方法、常量、变量放在堆内存中,保存所有引用类型的真实信息,以方便执行器指向,堆内存分为三个部分:年轻代、老年代、永久代。Java7之前,堆内存在逻辑上分为:年轻代、老年代、永久代。物
JVM深入理解JVM(5)——虚拟机类加载机制 PostedbyCrowonAugust21,2017在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机中,而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机中会发生什么变化?本文将逐步解答这
保存(持久化)对象及其状态到内存或者磁盘Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在