JVM原理(1) - 基础体系结构

时间:2021-07-08 09:08:11

概述

基本概念

  1. JVM:全称是Java Virtual Machine,是一种能够执行java bytecode(字节码)的虚拟机。
  2. JIT:全称为Just-in-time compiler,即时编译编译器,用于将java字节码转换成可以直接发送给处理器的指令程序,采用该技术的虚拟机,所有执行过的代码都必然会被编译过。
  3. java字节码:java虚拟机执行的一种指令格式,对应为java的class文件,具体可以通过javac命令进行源码编译来生成
  4. Garbage Collector:即垃圾回收,简写为GC;专用于heap内存的自动回收和垃圾清理,能有效防止内存泄露和溢出问题
  5. PC Register:全称为:Program Counter Register,即程序计数器,是用于指示下一条指令的所在地址,每当完成指令操作后,PC就会下跳。
  6. java动态性:jvm对于类的加载,并不是一次性全部load,而是运行时”动态地”加载该类及相关class文件,当然还包括动态的初始化类、对类进行动态链接
  7. SecurityManager:用来检查应用程序是否能访问一些有限的资源,例如文件、套接字(socket)等,可用在哪些具有高安全性要求的应用程序中。
  8. Hotspot:全称为:Hotspot VM,目前主流的JVM虚拟机技术。属于一款混合模式的执行引擎,包括了解释器和自适应编译期。与JIT技术不同,hotspot在大多数情况下效率都会比JIT高,原于其热点编译的特性,即代码默认情况是由解释器执行,只有当调用次数和循环次数达到较高频率(热度)时,才会针对热点代码进行局部编译,由此提高性能而得名。
    PS:对Hotspot和JIT感兴趣的,可以看下这篇文章:https://www.ibm.com/developerworks/cn/java/j-lo-just-in-time

jvm的存在价值

      谈起java语言,不得不说的就是jvm虚拟机。主要由于jvm实现了“平台无关性”。编译后的java程序指令并不能直接在硬件系统CPU上执行,而是由JVM执行。JVM屏蔽了与具体平台相关的信息,使得java语言编译程序只需要生成在JVM上运行的目标字节码(.class),就可以在多种平台上不加修改地运行。java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此jvm是java程序能在多平台进行无缝移植的可靠保证,同时jvm也是java程序安全检查的检测引擎。

jvm的生命周期

概念区分-jvm实例和jvm执行引擎实例

  • JVM实例对应了一个独立运行的java程序,它是进程级别。
  • JVM执行引擎实例则对应了属于用户运行程序的线程,它是线程级别的。
    PS:jvm的生命周期,和jvm对象的生命周期是不一样的,对象生命周期后续可以专门文章来做下分享哈。

jvm实例生命周期

  • 启动阶段:启动一个java程序,jvm实例就诞生;任何一个拥有main方法的class类都可以作为jvm实例运行的起点。
    PS:同一机器运行多个main应用,则会生成多个jvm实例
  • 运行阶段:main作为程序初始线程的起点,其他线程均由该线程启动;jvm内部有两种线程,守护线程和非守护线程,main属于非守护线程,守护线程由jvm自己使用。
  • 消亡阶段:当程序中的所有非守护线程都终止时,jvm就会退出;若安全管理器允许,程序也可以使用Runtime类或System.exit退出。

jvm体系结构

结构图

JVM原理(1) - 基础体系结构
PS:图片摘自javapapers网站:http://javapapers.com

体系模块划分

  • ClassLoader Subsystem,加载器子系统
    PS:专用来加载.class文件,注意只载入需要的类文件到JVM中的运行时数据区域中,但不负责类的执行与否

  • Runtime Data Area,运行时数据区
    PS:提供程序运行时所需要的数据,包含方法区、堆、java栈、PC寄存器、本地方法栈

  • Execution Engine,执行引擎
    PS:核心模块为Hotspot,其用于执行字节码,或执行本地方法;执行过程采用的是自定义的一套指令系统。

  • 垃圾收集子系统
    PS:简称GC,是jvm进行自动内存管理的一套服务,主要能保障自动释放一些没有用的对象。

  • Native Method Interface和Library,本地方法接口和本地方法库,主要提供了非java语言的其他接口服务给jvm进行调用
    PS:如类java.lang.Thread的setPriority方法是用java实现的,但它实际调用的是该类里的本地方方法setPriority0(),该本地方法是由C实现的,并被植入JVM内部;具体在一个类被加载时,若方法描述符带有native字样,那就会有一个指向该方法实现的指针,当然这些实现是在一些动态链接库DLL内的,其会被操作系统加载到java程序的地址空间。当一个native方法被加载时,相关DLL和指针尚未被设置;而实际在方法被调用前,才会通过java.system.loadLibrary()来加载DLL。

类加载器原理

什么是类加载器

      类加载器是一个用来加载类文件的类,名为ClassLoader。java源代码通过javac编译器编译成class文件,然后由jvm的执行引擎来负责程序实行。那么类加载器负责加载文件系统、网络或其他来源的类文件,默认有三种类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(也叫APPClassLoader)。

  • Bootstrap类加载器:也称为初始类加载器。对应JRE/lib/rt.jar
    PS:负责加载rt.jar中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出NUllPointerException异常。

  • Extension类加载器:对应JRE/lib/ext或java.ext.dirs指向的目录。
    PS:负责将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader实现。

  • APPClassLoader类加载器:对应ClassPath环境变量,或由-classpath或-cp选项定义,或者是JAR中的Manifest的classpath属性定义.
    PS:其负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath属性。Application类加载器是Extension类加载器的子加载器。通过sun.misc.Launcher$AppClassLoader实现。

  • 用户自定义类加载器:该加载器是留给程序员拓展用的,主要实现是通过java.lang.ClassLoader来继承实现的,基于自定义的ClassLoader可用于加载非Classpath中的jar以及目录。

工作特性-三机制

委托机制

main函数在启动加载类资源时,若没有自定义类加载器,则将由APPClassLoader来负责加载类,此时会先将加载请求提交给父类加载器,如果父类加载器不能找到或加载该类,则由自己进行加载。
PS:在部分资料中,也将该机制叫做”双亲委派模型”,我们可通过如下代码来测试类加载器的委派机制:

public class Test {
public static void main(String [] args) {
ClassLoader appClassLoader = Test.class.getClassLoader();
System.out.println(appClassLoader);
//结果为:sun.misc.Launcher$AppClassLoader@74a14482

ClassLoader extClassLoader = appClassLoader.getParent();
System.out.println(extClassLoader);
//结果为:sun.misc.Launcher$ExtClassLoader@2503dbd3

System.out.println(extClassLoader.getParent());
//结果为:null,ExtClassLoader的父类加载器是null,即BootStrap,由C语言实现
}
}

另外,类加载器还存在另外一种概念性的区分,系统类加载器和线程上下文类加载器

public class Test {
public static void main(String [] args) {
ClassLoader appClassLoader = Test.class.getClassLoader();
System.out.println(appClassLoader);
//结果为:sun.misc.Launcher$AppClassLoader@14dad5dc

ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(sysClassLoader);
//结果为:sun.misc.Launcher$AppClassLoader@14dad5dc

new Thread(new Runnable() {

public void run() {
ClassLoader threadcontextClassLosder = Thread.currentThread().getContextClassLoader();
System.out.println(threadcontextClassLosder);
//结果为:sun.misc.Launcher$AppClassLoader@14dad5dc
}
}).start();
}
}

如上实例可见,系统默认的类加载器是APPClassLoader,而无论是某个类的加载器或某个线程内的加载器,本质默认都是系统类加载器
PS:可通过如下方式来修改线程运行时的类加载器:thread.setContextClassLoader(new ClassLoader() {});

可见性机制

指的是子类加载器可看见父类加载器加载的类,而父类加载器看不到子类加载器加载的类。

举个例子说明,如下A类是我们自定义的某个类,该类本质是隶属于APPClassLoader来加载的

public class Test {
public static void main(String [] args) {
try {
Class.forName("A", true,
Test.class.getClassLoader().getParent());
System.out.println("用类加载器" + Test.class.getClassLoader().getParent() + "加载的话,A类被加载");
} catch (ClassNotFoundException e) {
System.out.println("用类加载器" + Test.class.getClassLoader().getParent() + "加载的话,A类不能被加载");
}
try {
Class.forName("A", true,
Test.class.getClassLoader());
System.out.println("用类加载器" + Test.class.getClassLoader() + "加载的话,A类被加载");
} catch (ClassNotFoundException e) {
System.out.println("用类加载器" + Test.class.getClassLoader() + "加载的话,A类不能被加载");
}
}
}
//运行结果如下所示:
//用类加载器sun.misc.Launcher$ExtClassLoader@1ee0005加载的话,A类不能被加载
//用类加载器sun.misc.Launcher$AppClassLoader@14dad5dc加载的话,A类被加载

而假如我们加载的类,像是java.util.ArrayList(如上demo,将A类替换即可测试),其实际上是隶属于BootStrap类加载器下的,那么最终结果将是ExtClassLoader和APPClassLoader都可以加载到

单一性机制

类仅加载一次,这是通过了委托机制确保子类加载器不会再次加载父类加载器加载过的类。
该机制此处我们结合jdk底层的ClassLoader源码来进行解读,详见如下代码:

    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

由此可见,findLoadedClass会检测该类是否已经加载过,若加载过则不会重复进行加载,由此保障了单一性。

核心工作原理

类加载的方式

  • 隐式装载:
    PS:通过new等途径生成的对象。
  • 显式装载:
    PS:有两种方式:由ClassLoader.loadClass()方法。另一种是通过Class.forName,即用反射方式来创建类实例
public static Class<?> forName(String name, boolean initialize,  ClassLoader loader) throws ClassNotFoundException  {  
if (loader == null) {
//获取安全管理器,一般是关闭的,可通过-Djava.security.manager,或用System.setSecurityManager()
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader ccl = ClassLoader.getCallerClassLoader();
if (ccl != null) {
sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader);
}

/** Called after security checks have been made. */
private static native Class forName0(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException;

类加载过程

类的加载,需要经历好几个环节,最终才能被程序员使用,如下所示:
JVM原理(1) - 基础体系结构

具体可分为三个加载的步骤:装载、链接、初始化:

  • 装载
    该过程负责找到二进制字节码并加载到JVM中,通过类名、类所在包,由ClassLoader来装载。如上图所示的第一个步骤,实际装载完毕后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在 Java 堆中也创建一个 java.lang.Class 类的对象,这样便可以通过该对象访问方法区中的这些数据。
    PS:HelloWorld类,javac后,执行java -classpath . HelloWorld,实际是根据类名找到类并进行装载。

    这里静态存储结构主要转为方法区运行时数据结构,更多指的是javac后的一些常量池信息
    例如java中static和final方法(private属于final方法),这些都是预先初始化好的,该过程叫前期绑定。
    PS:前期绑定是程序执行前根据编译时类型绑定,开销小,如static、final、private、构造方法等
    动态绑定是运行时根据对象类型进行绑定,又叫动态绑定。例如java中除了static和final外的方法,都是在运行时判断对象的类型进行动态绑定。

  • 链接
    该过程可细分为三个步骤:验证、准备、解析。

    验证步骤是为了确保Class文件中的字节流包含信息符合要求,不会危害虚拟机自身安全。
    具体四个验证环节主要如下所示:
    1、文件格式验证:字节流是否符合规范,保证了输入字节流能被解析并存储到方法区中。
    2、元数据验证:对类的各个数据类型进行语法校验,保证不存在不符合语法要求的元数据信息。
    3、字节码验证:对数据流和控制流分析,对类方法体做校验,保证类方法在运行时不会有危害虚拟机安全行为
    PS:会对类中所有的属性、方法进行验证,以确保其需要调用的属性、方法存在,包括具备对应的权限,如该方法的描述符(public/private/native权限),若有异常则NoSuchMethodError、NoSuchFiledError。
    4、符号引用验证: 对类自身以外的信息(特指常量池中的各种符号引号)进行匹配性的检测

    准备步骤是为类变量分配内存并设置类变量初始值阶段,这些内存都在方法区中分配。
    PS:内存分配仅包含类变量(static),不包含实例变量,实例变量是在对象实例化时堆中分配的。初始化通常是指数据类型默认的零值:0、0L、null、false等。

    解析步骤是将常量池中的符号引用转化为直接引用的过程。
    注意,解析过程可能会开始于初始化之前,或晚于初始化之后。解析动作主要针对类、接口、字段、类方法或接口方法等五类符号引用进行,对应于常量池为:CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info。
    1、类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
    2、字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。
    JVM原理(1) - 基础体系结构
    3、类方法解析:和字段解析差不多,只是多了判断方法所处是类还是接口,另外对类方法的匹配搜索,是先搜索父类,再搜索接口。
    4、接口方法解析:与类方法解析差不多,不过接口不会有父类,故只是递归向上检索父接口。

  • 初始化
    该过程主要是执行类中的静态初始化代码、构造器代码以及静态属性的初始化。包括四个场景会触发初始化:
    1、调用了new;2、反射调用了类中方法;3、子类调用了初始化;4、JVM启动过程中指定的初始化类。

类加载器的顺序

JVM原理(1) - 基础体系结构
对于JVM启动时,是由Bootstrap向CustomClassLoader方向进行加载。
而对于应用进行ClassLoader时,是由CustomClassLoader向Bootstrap方向查找类并进行加载的。

应用启动时,若用户自定义了CustomClassLoader,那会先尝试从自己的缓存中获取,当获取不到时则先代理给其父类加载器,由父类加载器尝试去加载这个类,以此类推。CustomClassLoader的父类加载器APPClassLoader会先从自己的缓存中获取该类,若获取不到,则又会代理给其父类加载器,即ExtClassLoader,当该ExtClassLoader也在缓存中获取不到,则会代理到由C实现的BootstrapClassLoader加载器进行加载。实例代码如下所示:

    protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查该类是否已经加载过,若加载过则从缓存中直接获取即可
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//若子类加载器没加载过,则将其抛给父类加载器
c = parent.loadClass(name, false);
} else {
//当一直往外抛到ExtClassLoader时,其父类加载器是null
//因其父类加载器是由C实现,此时会进入else分支,由Bootstrap进行查找和加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//当所有类加载器的缓存中都没有时,会先在ExtClassLoader进行find
//对于自定义类,从上往下一层层find不到时,最后会到达CustomClassLoader进行加载
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

对于由Jdk自带的类,如ArrayList,实际最终会在Bootstrap加载成功并放入其缓存;
而对于用户自定义类,本身Bootstrap是加载不到的,那么又会开启自顶向下来尝试类加载,经过一步步往下尝试加载,最终会在CustomClassLoader成功加载该类,并将其放置在缓存中。
基于类加载器的可见性,本身在Bootstrap或其他父类加载器中成功加载的类,对于CustomClassLoader是可见的,因此当再次进行类加载时,直接在用户自定义类加载器中进行加载即可。

PS:有兴趣大家可以自己实现一个自定义类加载器,只需继承ClassLoader即可。

运行时数据区

jvm本身是支持多线程的一套底层虚拟机服务,因此如下共说明了jmv运行时数据区的分布情况

JVM原理(1) - 基础体系结构
PS:对于括号内的共享和独享,特指对多线程的支持;独享意味着每个线程会有一套单独的资源。
而针对堆,因其是在内存区划分了一块专用于对象存储的空间,会存在多线程间的相互抢占,属于共享。

PC寄存器

也叫程序计数器(Program Counter Register),此处并非大家学习汇编时CPU的寄存器。但JVM中的PC寄存器其实也实现同样的功能,用来指示执行哪条指令,保存着程序当前执行的指令地址(或说为保存下一条指令的所在存储单元的地址)。

由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

在JVM规范中,如果线程执行的是非native方法,则程序计数器中保存的是当前需执行的指令地址;若线程执行的是native方法,则程序计数器中的值是undefined。
PS:程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变的。

JVM栈

该栈和线程是同时创建的,一个线程对应一个栈;栈是一个后进先出的,用于支持虚拟机进行方法调用和方法执行的数据结构。具体内部剖析图如下所示:
JVM原理(1) - 基础体系结构
如图,栈帧称之为”Stack Frame”,用于存储局部变量表、操作数栈、动态链接、方法出口等,每个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
PS:JVM栈内的每个栈帧,局部变量表会存放对应基本类型(如int、boolean、string等),但不会存储非基本类型对象的数据,实质上存放的是该对象的引用。

那么该栈帧中到底有哪些核心组成部分呢:
局部变量表:在编译期间便确定大小,在方法code属性的max_locals数据项中确定了其最大容量;由方法参数和方法内部定义的局部变量组成,以slot1为最小单位。
操作数栈:同样在编译期间由方法code属性的max_stacks确定栈的深度。主要发生在方法执行,由字节码指令往操作数栈进行写入和读取处理。
动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有该引用是为了支持方法调用过程中的动态连接。
方法返回地址:存放着调用该方法的PC寄存器的值。当方法正常退出时,则寄存器内值为该方法的地址;而当该方法异常退出时,返回地址要通过异常处理器表来确定,栈帧中不会保存该部分信息。

堆(heap)

该部分在这里就不详细展开了,内容会比较多,后续再拓扑。

堆,对于C来说,是程序员唯一可以管理的内存区域,具体通过malloc和free进行申请和释放。
而对java来说,程序员基本不用关心空间的释放问题,会有垃圾回收机制自动进行处理。

那么堆是用来存储对象本身的以及数组(当然对象引用和数组引用都是放在栈中的)
此外,堆是被所有线程共享的,一个JVM实例只会有一个堆。

方法区域(Method Area)

如下图,对方法区的内容进行大致的划分,提高系统化的理解:
JVM原理(1) - 基础体系结构

说起方法区,其是被线程共享的区域,在JVM启动时就创建好了。最直观的感受就是开发人员可通过Class对象中的getName、isInstance等方法来获取信息,这些信息都来自方法区。该区存储了每个类的信息(类名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

本身方法区的内容,都是取自javac后的class文件,例如类名称、字段、方法、接口等,另外还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用(如类固定的常量信息、方法和Field应用信息等)。它是每一个类或接口的常量信息在运行时的表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

实际该方法区,对于jdk7及以前,就是对应hotspot的Permanent持久代,可通过XX:PermSize指定大小。
PS:使用时,可能对大小的分配,会存在一个问题,即抛出”java.lang.OutofMemoryError:permGen space”异常。

而对应于JDK8,方法区又有了一套新的实现机制,采用Native Memory来管理,称之为元空间(Metaspace),则在实现上将数据转移到了java Heap内,或者Native Heap上。
PS:在JVM规范中,没有强制方法区必须实现垃圾回收,当然对于jdk8的策略而言,方法区就会随着heap进入垃圾回收的管理。

本地方法堆栈(Native Method Stacks)

跟JVM栈的作用相似,但JVM栈提供的是java方法服务的执行过程,而本地方法栈提供是的本地native方法使用到的服务,其内部存储着每个native方法调用的情况。

jvm执行引擎

引擎,顾名思义,就是在JVM当中,起到对class文件中代码对应的字节码指令的解读及执行的过程,因此执行引擎会操作运行时数据区,会操作堆和栈空间、会操作PC寄存器,甚至也会操作本地方法栈等。

主要的执行技术

解释,即时编译(JIT),自适应优化(如Hotspot)、芯片级直接执行

  • 解释:解释器执行,属于第一代JVM
    PS:解释器的执行过程,可大致抽象为:输入代码 -> 解释 执行 -> 执行结果

  • 即时编译JIT,称之为严格意义的纯编译技术,属于第二代JVM
    PS:JIT执行过程,可抽象为:输入代码 -> 编译 执行 -> 编译后class代码 -> 执行 -> 执行结果

  • 自适应优化(目前Sun的HotspotJVM采用这种技术)则吸取第一代JVM和第二代
    PS:也叫作热点技术,具体可理解为在代码运行的一开始,采用解释技术,在运行过程中记录那部分代码会存在热点运行,主要考察来自两个维度:调用次数和循环次数;若发现代码热点较高,则采用编译技术进行class编译,进而用字节码方式来进行执行,从而提高运行速度。

PS:有兴趣的同学可以看看大牛在知乎上的回答:
为什么JVM不用JIT全程编译(内附带其他问题传送门):https://www.zhihu.com/question/37389356

四种执行指令

  • invokestatic:调用类的static静态方法
  • invokevirtual:调用对象实例的方法,即虚方法
  • invokeinterface:将属性定义为接口来进行调用,即接口方法
  • invokespecial:JVM对于初始化对象(Java构造器的方法为:init)以及调用对象实例中的私有方法时。
  • invokedynamic:动态解析出需要调用的方法,然后执行
    PS:前四条固化在虚拟机内部,方法调用执行不可干预,而invokedynamic则支持由用户确定方法版本。

垃圾回收器GC

PS:该部分此处不详细展开,后续会单独专题来做深入的学习和探讨