8.2JVM内存回收清理机制
目录介绍
- 01.厘清内存回收
- 1.1 先思考三个问题
- 1.2 什么是垃圾回收
- 02.理解JVM分配内存
- 2.1 静态内存和动态内存
- 2.2 JVM内存分配与回收
- 04.识别那些内存是垃圾
- 4.1 堆内存回收
- 4.2 方法区内存回收
- 4.3 局部变量会回收吗
- 05.用什么方式回收
- 5.1 设置对象null
- 5.2 包一个软引用
- 06.垃圾回收有关函数
- 07.调用finalize()
- 08.GC回收原理分析
01.厘清内存回收
1.1 先思考三个问题
- 在了解回收机制之前,必须要了解内存
- 程序计数器、虚拟机栈、本地方法栈随线程而生,也随线程而灭;栈帧随着方法的开始而入栈,随着方法的结束而出栈。这几个区域的内存分配和回收都具有确定性,在这几个区域内不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收。
- 对于 Java 堆和方法区,我们只有在程序运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾收集器所关注的正是这部分内存。
- 先思考三个问题
- JVM是怎么分配内存的
- 识别哪些内存是垃圾需要回收
- 最后才是用什么方式回收
- 栈与堆
- 栈的内存管理是顺序分配的,而且定长,不存在内存回收问题;
- 堆,则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题
1.2 什么是垃圾回收
- 什么是垃圾回收?
- 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
- 注意:回收是无引用占据的内存空间而非对象本身。
- 注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,可以理解为一种文字游戏。
- 分析一下
- 引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(引用都有哪些?对垃圾回收又有什么影响?)
- 垃圾:无任何对象引用的对象(怎么通过算法找到这些对象呢?)。
- 回收:清理“垃圾”占用的内存空间而非对象本身(怎么通过算法实现回收呢?)。
- 发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中(堆内存为了配合垃圾回收有什么不同区域划分,各区域有什么不同?)。
- 发生时间:程序空闲时间不定时回收(回收的执行机制是什么?是否可以通过显示调用函数的方式来确定的进行回收过程?)
03.理解JVM分配内存
2.1 静态内存和动态内存
- 简单描述一下
- Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
- java一般内存申请有两种:
- 静态内存和动态内存。
- 很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;
- 动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。
2.2 JVM内存分配与回收
- Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。
- 同时,Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。
- JDK1.8之前的堆内存示意图:
image - 从上图可以看出堆内存的分为新生代、老年代和永久代。新生代又被进一步分为:Eden 区+Survior1 区+Survior2 区。
- 分代回收算法
- 目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
- 大多数情况下,对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
- Minor Gc和Full GC 有什么不同呢?
- 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。
03.识别那些内存是垃圾
4.1 堆内存回收
- 回收那些对象
- 无任何引用的对象
- 主要是针对堆中对象
- 创建一个对象,会在堆内存中分配内存空间。那么回收对象,则会清理掉对象所占的内存空间。
4.2 方法区内存回收
- 方法区中存放生命周期较长的类信息、常量、静态变量,每次垃圾收集只有少量的垃圾被清除。方法区中主要清除两种垃圾:
- 废弃常量
- 无用的类
4.3 局部变量会回收吗
- 实验代码,代码如下所示
public class GcTest { private static final int SIZE = 6 * 1024 * 1024; public static void localVarGc1() { byte[] b = new byte[SIZE]; System.gc(); } public static void localVarGc2() { byte[] b = new byte[SIZE]; b = null; System.gc(); } public static void localVarGc3() { { byte[] b = new byte[SIZE]; } System.gc(); } public static void localVarGc4() { { byte[] b = new byte[SIZE]; } int c = 0; System.gc(); } public static void localVarGc5() { localVarGc1(); System.gc(); } public static void main(String[] args) { // localVarGc1(); // 没有GC // localVarGc2(); // GC // localVarGc3(); // 没有GC // localVarGc4(); // GC // localVarGc5(); // GC } }
- 在main中分别执行localVarGc【1-5】方法,得到如下5次gc日志:
[GC (Allocation Failure) 512K->374K(130560K), 0.0006220 secs] [GC (Allocation Failure) 886K->600K(130560K), 0.0011130 secs] [GC (Allocation Failure) 1112K->752K(130560K), 0.0006960 secs] [GC (Allocation Failure) 1264K->950K(131072K), 0.0015540 secs] [GC (System.gc()) 7944K->7363K(131072K), 0.0008640 secs] [Full GC (System.gc()) 7363K->7116K(131072K), 0.0085270 secs] [GC (Allocation Failure) 512K->390K(130560K), 0.0008690 secs] [GC (Allocation Failure) 902K->592K(130560K), 0.0008500 secs] [GC (Allocation Failure) 1104K->718K(130560K), 0.0007220 secs] [GC (Allocation Failure) 1230K->924K(131072K), 0.0012260 secs] [GC (System.gc()) 7919K->7309K(131072K), 0.0018500 secs] [Full GC (System.gc()) 7309K->975K(131072K), 0.0059300 secs] [GC (Allocation Failure) 512K->374K(130560K), 0.0007940 secs] [GC (Allocation Failure) 886K->598K(130560K), 0.0007240 secs] [GC (Allocation Failure) 1110K->718K(130560K), 0.0007680 secs] [GC (Allocation Failure) 1230K->916K(131072K), 0.0009900 secs] [GC (System.gc()) 7887K->7340K(131072K), 0.0008910 secs] [Full GC (System.gc()) 7340K->7116K(131072K), 0.0091600 secs] [GC (Allocation Failure) 512K->416K(130560K), 0.0007990 secs] [GC (Allocation Failure) 928K->584K(130560K), 0.0008580 secs] [GC (Allocation Failure) 1096K->728K(130560K), 0.0007360 secs] [GC (Allocation Failure) 1240K->910K(131072K), 0.0010150 secs] [GC (System.gc()) 7883K->7339K(131072K), 0.0011770 secs] [Full GC (System.gc()) 7339K->971K(131072K), 0.0069840 secs] [GC (Allocation Failure) 512K->406K(130560K), 0.0005700 secs] [GC (Allocation Failure) 918K->622K(130560K), 0.0011430 secs] [GC (Allocation Failure) 1134K->710K(130560K), 0.0015010 secs] [GC (Allocation Failure) 1222K->948K(131072K), 0.0020340 secs] [GC (System.gc()) 7921K->7304K(131072K), 0.0013160 secs] [Full GC (System.gc()) 7304K->7110K(131072K), 0.0091750 secs] [GC (System.gc()) 7121K->7142K(131072K), 0.0002990 secs] [Full GC (System.gc()) 7142K->966K(131072K), 0.0050000 secs]
- gc日志中(加粗部分为System.gc触发的)可以得到如下结论:
- 申请了一个6M大小的空间,赋值给b引用,然后调用gc函数,因为此时这个6M的空间还被b引用着,所以不能顺利gc;
- 申请了一个6M大小的空间,赋值给b引用,然后将b重新赋值为null,此时这个6M的空间不再被b引用,所以可以顺利gc;
- 申请了一个6M大小的空间,赋值给b引用,过了b的作用返回之后调用gc函数,但是因为此时b并没有被销毁,还存在于栈帧中,这个空间也还被b引用,所以不能顺利gc;
- 首先调用localVarGc1() ,很显然不能顺利gc,函数调用结束之后再调用gc函数,此时因为localVarGc1这个函数的栈帧已经随着函数调用的结束而被销毁,b也就被销毁了,所以6M大小的空间不被任何对象引用,于是能够顺利gc。
- 总结一下
- 局部变量表中的变量是很重要的垃圾回收根节点,被局部变量表中变量直接或者间接引用的对象都不会被回收。
- 局部变量为啥不会被回收
- 局部变量表位于方法栈帧中,方法结束时栈帧被回收,其中的所有内容包括局部变量表不再有效。
- 既然是局部变量产生对象,离开这个方法后,这个变量就不存在了,那么他所产生的对象也应该被回收。
05.用什么方式回收
5.1 设置对象null
- 如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?
- 不会,在下一个垃圾回收周期中,这个对象将是可被回收的。
- 也就是说当一个对象的引用变为null时,并不会被垃圾收集器立刻回收,而是在下一次垃圾回收时才会释放其占用的内存。
5.2 包一个软引用
06.垃圾回收有关函数
- System.gc()方法
- 命令行参数监视垃圾收集器的运行:
- 使用System.gc() 可以不管JVM使用的是哪一种垃圾回收的算法,都可以请求Java的垃圾回收。在命令行中有一个参数-verbosegc可以查看Java使用的堆内存的情况,它的格式如下:java -verbosegc classfile
- 需要注意的是,调用System.gc()也仅仅是一个请求(建议) 。JVM接受这个消息后,并不是立即做垃圾回收,而只是对几个垃圾回收算法做了加权,使垃圾回收操作容易发生,或提早发生,或回收较多而已。
finalize()方法
概述:在JVM垃圾回收器收集一个对象之前,一般要求程序调用适当的方法释放资源。但在没有明确释放资源的情况下,Java提供了缺省机制来终止该对象以释放资源,这个方法就是finalize()。它的原型为:protected void finalize() throws Throwable在finalize()方法返回之后,对象消失,垃圾收集开始执行。原型中的throws Throwable表示它可以抛出任何类型的异常。- 意义:之所以要使用finalize() ,是存在着垃圾回收器不能处理的特殊情况。假定你的对象(并非使用new方法)获得了一块“特殊”的内存区域,由于垃圾回收器只知道那些显示地经由new分配的内存空间,所以它不知道该如何释放这块“特殊”的内存区域,那么这个时候Java允许在类中定义一个finalize() 方法。
- 特殊的区域例如:1)由于在分配内存的时候可能采用了类似 C语言的做法,而非JAVA的通常new做法。这种情况主要发生在native method中,比如native method调用了C/C++方法malloc()函数系列来分配存储空间,但是除非调用free() 函数,否则这些内存空间将不会得到释放,那么这个时候就可能造成内存泄漏。但是由于free() 方法是在C/C++中的函数,所以finalize() 中可以用本地方法来调用它。以释放这些“特殊”的内存空间。2)又或者打开的文件资源,这些资源不属于垃圾回收器的回收范围。
- 换言之,finalize() 的主要用途是释放一些其他做法开辟的内存空间,以及做一些清理工作。因为在Java中并没有提够像“析构”函数或者类似概念的函数,要做一些类似清理工作的时候,必须自己动手创建一个执行清理工作的普通方法,也就是override Object这个类中的finalize()方法。比如:销毁通知。
- 一旦垃圾回收器准备好释放对象占用的存储空间,首先会去调用finalize() 方法进行一些必要的清理工作。只有到下一次再进行垃圾回收动作的时候,才会真正释放这个对象所占用的内存空间。
- JAVA里的对象并非总会被垃圾回收器回收。1 对象可能不被垃圾回收,2 垃圾回收并不等于“析构”,3 垃圾回收只与内存有关。也就是说,并不是如果一个对象不再被使用,是不是要在finalize() 中释放这个对象中含有的其它对象呢?不是的。因为无论对象是如何创建的,垃圾回收器都会负责释放那些对象占有的内存。
- 当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。
07.调用finalize()
为何需要有finalize()
假定你的对象(并非使用new)获得了一块“特殊”的内存区域。由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为finalize() 的方法。- 它的工作原理“假定”是这样的
- 一旦垃圾回收器准备释放好对象占用的存储空间,将先调用其finalize() 方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以你要是打算使用finalize() ,就能在垃圾回收时刻做一些重要的清理工作。
- 这里存在一个编程陷阱
- finalize()并不是C++中的析构函数。在C++中,对象一定会被销毁(如果程序没有缺陷)。但是Java中的对象却并非总是被垃圾回收。
垃圾回收只与内存有关。
使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。本地方法可以让java调用非java代码。本地方法现在只支持C和C++。但是C和C++又可以调用其它语言写的代码。所以实际上可以调用任何代码。- 在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非使用free() 函数,否则存储空间得不到释放,从而造成内存泄漏。所以要在finalize()中用本地方法调用它。
Java虚拟机并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。如果你想看一下垃圾回收时的情形,那可以使用System.gc() ,用于强制进行终结动作。
08.GC回收原理分析
8.1 关于GC概念介绍
- 有几个函数可以访问GC,例如运行GC的函数System.gc() ,但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC
通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。 通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间
GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。
8.2 如何监听GC过程
- 系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下 D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <Pause_time>
- 第一部分GC_Reason,这个是触发这次GC操作的
原因,一般情况下一共有以下几种触发GC操作的原因: - GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。 - GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。 - GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。 - GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。
- 第二部分Amount_freed,表示系统通过这次GC操作释放了多少内存
- 第三部分Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)
第四部分Pause_time表示这次GC操作导致应用程序暂停的时间。关于这个暂停的时间,Android在2.3的版本当中进行过一次优化,在2.3之前GC操作是不能并发进行的,也就是系统正在进行GC,那么应用程序就只能阻塞住等待GC结束。虽说这个阻塞的过程并不会很长,也就是几百毫秒,但是用户在使用我们的程序时还是有可能会感觉到略微的卡顿。而自2.3之后,GC操作改成了并发的方式进行,就是说GC的过程中不会影响到应用程序的正常运行,但是在GC操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了
8.3 GC过程与对象的引用类型关系
- Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference
Image.png
- 软引用和弱引用
- 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术
- 软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
- 利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。
07.触发主GC的条件
- 触发主GC的条件有哪些?
- 1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
- 3)在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。
- 由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。
08.减少GC开销措施
- 根据上述GC的机制,程序的运行会直接影响系统环境的变化,从而影响GC的触发。若不针对GC的特点进行设计和编码,就会出现内存驻留等一系列负面影响。为了避免这些影响,基本的原则就是尽可能地减少垃圾和减少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的机会。