文章目录
一、虚拟机
1.1、JVM
定义
JVM本质上就是一个软件,是计算机硬件的一层软件抽象,在这之上才能够运行Java程序,JAVA在编译后会生成类似于汇编语言的.class字节码文件;
JVM的作用是把平台无关的.class里面的字节码翻译成平台相关的机器码,来实现跨平台。
组成
JVM由三个主要的子系统构成:
类加载子系统:装载具有适合名称的类或接口
运行时数据区(内存结构):包含方法区、Java堆、Java栈、本地方法栈、指令计数器及其他隐含寄存器
执行引擎:负责执行包含在已装载的类或接口中的指令
1.2、DVM(Dalvik)
定义
DVM也是实现了JVM规范的一个虚拟器,默认使用CMS垃圾回收器,但是与JVM运行 Class 字节码不同,DVM执行 Dex
1.3 ART
ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,Android 4.4之前默认采用的还是DVM,系统会提供一个选项来开启ART模式。在Android 5.0时,默认采用ART,DVM从此退出历史舞台。
1.4、JVM与DVM区别
JVM:
- java虚拟机运行的是Java字节码;传统的Java程序经过编译,生成Java字节码保存在class文件中,java虚拟机通过解码class文件中的内容来运行程序。
- java虚拟机基于栈架构,程序在运行时虚拟机需要频繁的从栈上读取或写入数据。这过程需要更多的指令分派与内存访问次数,会耗费不少CPU时间。
DVM:
- Dalvik虚拟机运行的是Dalvik字节码,所有的Dalvik字节码由Java字节码转换而来,并被打包到一个DEX(Dalvik Executable)可执行文件中Dalvik虚拟机通过解释Dex文件来执行这些字节码
- Dalvik虚拟机基于寄存器架构,数据的访问通过寄存器间直接传递,这样的访问方式比基于栈方式快的多
1.5、DVM与ART区别
DVM
运行时动态地将执行频率很高的dex字节码翻译成本地机器码,然后在执行,但是将dex字节码翻译成本地机器码是发生在应用程序的运行过程中,并且应用程序每一次重新运行的时候,都要重新做这个翻译工作.
ART
在安装应用的时候,dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码。移除了运行时的解释执行,效率更高,启动更快。(安卓在4.4中发布了ART运行时); 但也会造成 ①更大的存储空间占用,可能增加10%-20% ②更长的应用安装时间
二、类的加载
2.1 什么是类的加载
类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口
2.2 类的生命周期
而 JVM 虚拟机执行 class 字节码的过程可以分为七个阶段:加载、验证、准备、解析、初始化、使用、卸载。
- 加载
VM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。 - 验证
当 JVM 加载完 Class 字节码文件并在方法区创建对应的 Class 对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行
JVM规范校验。JVM 会对字节流进行文件格式校验,判断其是否符合 JVM 规范,是否能被当前版本的虚拟机处理。例如:文件是否是以 0x cafe bene开头,主次版本号是否在当前虚拟机处理范围之内等。
代码逻辑校验。JVM 会对代码组成的数据流和控制流进行校验,确保 JVM 运行该字节码文件后不会出现致命错误。例如一个方法要求传入 int 类型的参数,但是使用它的时候却传入了一个 String 类型的参数。 - 准备(重点)
当完成字节码文件的校验之后,JVM 便会开始为类变量分配内存并初始化。这里需要注意两个关键点,即内存分配的对象以及初始化的类型。
内存分配的对象 :
Java 中的变量有「类变量」和「类成员变量」两种类型,「类变量」指的是被 static 修饰的变量,而其他所有类型的变量都属于「类成员变量」。在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
初始化的类型:
在准备阶段,JVM 会为类变量分配内存,并为其初始化。但是这里的初始化指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
static final 会直接被复制,而 static 变量会被赋予零值
例如:
public static int sector = 3;
public static final int number = 3;
初始化完成后, sector = 0 ; number = 3 ;
-
解析
VM 针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成直接其在内存中的直接引用。 -
初始化(重点)
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化,一般来说当 JVM 遇到下面 5 种情况的时候会触发初始化:
(了解)
(1)遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
(2)生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
(3)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
(4)当使用 JDK1.7 动态语言支持时,如果一个 java.lang.invoke.MethodHandle实例最后的解析结果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。 -
使用
当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码 -
卸载
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class 对象,最后负责运行的 JVM 也退出内存
总结一个类的执行顺序大概可以按照如下步骤:
- 确定类变量的初始值。在类加载的准备阶段,JVM 会为类变量初始化零值,这时候类变量会有一个初始的零值。如果是被 final 修饰的类变量,则直接会被初始成用户想要的值
- 初始化入口方法。当进入类加载的初始化阶段后,JVM 会寻找整个 main 方法入口,从而初始化 main 方法所在的整个类。当需要对一个类进行初始化时,会首先初始化类构造器( ),之后初始化对象构造器( )。
- 初始化类构造器。JVM 会按顺序收集类变量的赋值语句、静态代码块,最终组成类构造器由 JVM 执行
- 初始化对象构造器。JVM 会按照收集成员变量的赋值语句、普通代码块,最后收集构造方法,将它们组成对象构造器,最终由 JVM 执行。
- 如果在初始化 main 方法所在类的时候遇到了其他类的初始化,那么就先加载对应的类,加载完成之后返回。如此反复循环,最终返回 main 方法所在类。
2.3 类加载器
- 启动类加载器:Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库
- 扩展类加载器:Extension ClassLoader,该加载器sun.misc.Launcher$ExtClassLoader实现,它负责加载DK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
- 应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器
2.4 类加载机制
- 全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入
- 父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类
所有父ClassLoader无法加载Class时,则会调用自己的 findClass 方法
如果你不想使用双亲委托,则重写loadClass 修改其实现。而重写 findClass 则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class - 缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效。
三、JVM内存管理
3.1 JVM内存结构(运行时数据区)
方法区和堆是所有线程共享的内存区域;
java栈、本地方法栈和程序计数器是运行时线程私有的内存区域。
3.1.1 堆区
(1)特点
- 内存最大,内存共享
- 存放对象实例(new创建的对象实例)
- 堆满时抛出OutOfMemoryEroor异常
- 垃圾回收管理的主要区域
(2)模型
Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。
(3)新生代,老年代,持久代
优点
不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。
(1)新生代Young Generation(1/3堆空间)
几乎所有新生成的对象首先都是放在年轻代的。新生代内存按照8:1:1的比例分为一个Eden区和两个Survivor(Survivor0,Survivor1)区。大部分对象在Eden区中生成。当新对象生成,Eden Space申请失败(因为空间不足等),则会发起一次GC(Scavenge GC)。回收时先将Eden区存活对象复制到一个Survivor0区,然后清空Eden区,当这个Survivor0区也存放满了时,则将Eden区和Survivor0区存活对象复制到另一个Survivor1区,然后清空Eden和这个Survivor0区,此时Survivor0区是空的,然后将Survivor0区和Survivor1区交换,即保持Survivor1区为空, 如此往复。当Survivor1区不足以存放 Eden和Survivor0的存活对象时,就将存活对象直接存放到老年代。当对象在Survivor区躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。若是老年代也满了就会触发一次Full GC:也就是新生代、老年代都进行回收。新生代大小可以由-Xmn来控制,也可以用-XX:SurvivorRatio来控制Eden和Survivor的比例。
(2)老年代Old Generation(2/3堆空间)
在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。一般来说,大对象会被直接分配到老年代。所谓的大对象是指需要大量连续存储空间的对象。
(3)持久代PermGen& 元空间MetaSpace(直接内存JDK1.8后)
PermGen(持久代)包含哪些信息
- JVM中类的元数据在Java堆中的存储区域。
- Java类对应的HotSpot虚拟机中的内部表示也存储在这里。
- 类的层级信息,字段,名字。
- 方法的编译信息及字节码。
- 变量
- 常量池和符号解析
持久代的大小与控制
- 它的上限是MaxPermSize,默认是64M
- 持久代用完后,会抛出OutOfMemoryError "PermGen space"异常。解决方案:应用程序清理引用来触发类卸载;增加MaxPermSize的大小
- 需要多大的持久代空间取决于类的数量,方法的大小,以及常量池的大小
为什么移除持久代
- 它的大小是在启动时固定好的——很难进行调优。-XX:MaxPermSize,设置成多少好呢?
- java.lang.OutOfMemoryError: PermGen 空间问题将不复存在
- 提升对于 元数据管理,用专门的元数据迭代器,提升GC效率;
元空间MetaSpace的组成
- Klass Metaspace 存储 klass,klass 是我们熟知的 class 文件在 jvm 里的运行时数据结构,大小可通过 -XX:CompressedClassSpaceSize=1G 参数来控制。
- NoKlass Metaspace 由多块内存组合,可看作不连续的空间。存klass相关的其他的内容,比如 method,constantPool 等;
元空间的特点
- 符合 JLS 规范:类及相关的元数据的生命周期与 ClassLoader 一致
- 虽然整个 Metaspace 被共享,但是每个 ClassLoader 可以独立管理专属的存储空间
- 只进行线性分配
- 不会独立回收某个类
- 由于从 Heap 中移出,省掉 GC 扫描和压缩的时间
- 元空间里的对象的位置是固定的
- 如果GC发现某个类加载器不再存活了,会把相关的空间整个回收掉
内存管理
- 大部分类元数据都在本地内存中分配。
- 元空间虚拟机 以 组块分配 的方式 负责元空间的分配。
- 组块中的块是线性分配(指针碰撞)
- 类和其元数据的生命周期和其对应的 ClassLoader 是相同的;
- 每个 ClassLoader 的存储区域都称作一个元空间,JVM 中 Metaspace 就是并集
调优
MetaSpace 初始大小为 21 M,这是 GC 的初始阈值,超过则进行 FGC,对类进行回收。
如果启动后 GC 过于频繁,可以通过 -XX:MaxMetaspaceSize 设置元空间的最大值,以便推迟GC的执行时间
对GC性能的优化
- Full GC中,元数据指向元数据的那些指针都不用再扫描了。很多复杂的元数据扫描的代码(尤其是CMS里面的那些)都删除了。
- 元空间只有少量的指针指向Java堆。这包括:类的元数据中指向java/lang/Class实例的指针;数组类的元数据中,指向java/lang/Class集合的指针。
- 没有元数据压缩的开销
- 减少了根对象的扫描(不再扫描虚拟机里面的已加载类的字典以及其它的内部哈希表)
- 减少了Full GC的时间
- G1回收器中,并发标记阶段完成后可以进行类的卸载
- 由于 Metaspace VM 使用 组块分配,且目前不支持压缩操作。区块大小由 ClassLoader 决定,而类信息并不是固定大小。因此,有可能分配的空闲区块和类需要的区块大小不同,这种情况下可能导致 碎片存在
** PermGen 与MetaSpace 区别**
(1)前者是 JVM 的规范,而后者则是 JVM 规范的一种实现。
(2)方法区主要存储类元数据,所以对于动态生成类的情况比较容易出现永久代的 OOM
MetaSpace 替代 PermGen 变为方法区的实现,期间还有字符串常量池 从 PermGen 分离 与类元数据分开,放入 Heap 中,方法区移至Metaspace,提升类元数据的独立性。
(3)两者最大的区别:Metaspace 直接使用本地内存。但不设置 -XX:MaxMetaspaceSize 可能导致 swap 内存被耗尽,进程被 kill。
(4)Class对象是存放在堆区的,不是方法区,这点很多人容易犯错。类的元数据才是存在方法区的(元数据并不是类的 Class 对象。Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)
3.1.2 方法区
所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,线程共享的内存区域。
3.1.3 栈区
线程私有
Java栈描述的是Java方法执行的内存模型:一个线程对应一个栈,每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。不存在垃圾回收问题,只要线程已结束栈就出栈,生命周期与线程一致。
栈中信息
基础数据类型直接在栈空间分配
方法的形式参数直接在栈空间分配,方法调用完成后从栈空间回收
引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量。栈中的地址空间指向堆空间的对象区。
方法的引用参数,在栈空间分配一个地址空间,指向堆空间的对象区,方法调用完成后从栈空间回收。
创建new的局部变量,在栈中和堆中分配空间,当局部变量生命周期结束后,栈空间立刻回收,堆空间区域等待GC回收。
字符串常量,static静态变量在方法区分配空间。
3.1.4 本地方法栈
线程私有,可理解为java中jni调用。用于支持native方法执行,存储了每个native方法调用的状态
本地方法栈则是为虚拟机使用到的Native方法服务。执行引擎通过本地方法接口,利用本地方法库(C语言库)执行。
3.1.5 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。他是线程私有的。
四、GC算法,垃圾回收
1、定义
垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存
Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
2、对象的生命周期
(1)创建阶段(Created)
在创建阶段系统通过下面的几个步骤来完成对象的创建过程:
(1)为对象分配存储空间
(2)开始构造对象
(3)从超类到子类对static成员进行初始化
(4)超累成员变量按顺序初始化,递归调用超累的构造方法
(5)子类成员变量按顺序初始化,子类构造方法调用
一旦对象被创建,并被分派给某些变量赋值,这个对象的状态就切换到应用状态
(2)应用阶段(In Use)
对象至少被一个强引用持有。
(3)不可见阶段(Invisible)
当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该这些引用仍然是存在着的。简单说就是程序的执行已经超出了该对象的作用域了。
(4)不可达阶段(Unreachable)
对象处于不可达阶段是指该对象不再被任何强引用所持有。
与“不可见阶段”相比,“不可见阶段”是指程序不再持有该对象的任何强引用,这种情况下,该对象仍可能被JVM等系统下的某些已装载的静态变量或线程或JNI等强引用持有着,这些特殊的强引用被称为”GC root”。存在着这些GC root会导致对象的内存泄露情况,无法被回收。
(5)收集阶段(Collected)
当垃圾回收器发现该对象已经处于“不可达阶段”并且垃圾回收器已经对该对象的内存空间重新分配做好准备时,则对象进入了“收集阶段”。如果该对象已经重写了finalize()方法,则会去执行该方法的终端操作。
(6)终结阶段(Finalized)
当对象执行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。
(7)对象空间重分配阶段(De-allocated)
垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间重新分配阶段”。
3、判断对象是否是垃圾的算法
3.1 引用计数算法
堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1(a = b, b被引用,则b引用的对象计数+1)。当引用失效时(一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时),计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。
3.1 根搜索算法(可达算法)
(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。
GC根对象
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。
4、GC算法
4.1 标记 -清除算法(Mark-Sweep)
过程
第一:标记,标记从树根可达的对象(途中水红色),第二:清除(清楚不可达的对象)。标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。
缺点:
递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;
4.2 复制算法
过程
把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。
缺点
速度快但耗费空间,
4.3 标记-压缩算法
过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
4.4 分代收集算法
把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
(1)分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。
(2) 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域
5、GC调优
每一次Full GC都会使JVM停止运行–>使Full GC不执行,使Minor GC尽可能少地执行
5.1 调优命令
5.2 调优工具
Android Profiler
https://developer.android.com/studio/profile/memory-profiler?hl=zh-cn#profiler-memory-leak-detection
6、减少GC开销的措施
- (1)不要显式调用System.gc()
此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。 - (2)尽量减少临时对象的使用
临时对象在跳出函数调用后,会成为垃圾,少用临时变量就相当于减少了垃圾的产生,从而延长了出现上述第二个触发条件出现的时间,减少了主GC的机会。 - (3)对象不用时最好显式置为Null
一般而言,为Null的对象都会被作为垃圾处理,所以将不用的对象显式地设为Null,有利于GC收集器判定垃圾,从而提高了GC的效率。 - (4)尽量使用StringBuffer,而不用String来累加字符串
由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。 - (5)能用基本类型如Int,Long,就不用Integer,Long对象
基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。 - (6)尽量少用静态对象变量
静态变量属于全局变量,不会被GC回收,它们会一直占用内存。 - (7)分散对象创建或删除的时间
集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。