目录介绍

  • 01.先思考三个问题
  • 02.什么是垃圾回收
  • 03.内存垃圾回收机制
  • 03.关于GC概念介绍
  • 04.如何监听GC过程
  • 05.GC过程与对象的引用类型关系
  • 06.与垃圾回收有关函数
  • 07.触发主GC的条件
  • 08.减少GC开销措施

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计N篇[近100万字,陆续搬到网上],转载请注明出处,谢谢!
  • 链接地址:https://github.com/yangchong211/YCBlogs
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

01.先思考三个问题

  • 在了解回收机制之前,必须要了解内存
    • 先思考三个问题
      • JVM是怎么分配内存的
      • 识别哪些内存是垃圾需要回收
      • 最后才是用什么方式回收
    • 栈的内存管理是顺序分配的,而且定长,不存在内存回收问题;而堆 则是为java对象的实例随机分配内存,不定长度,所以存在内存分配和回收的问题

02.什么是垃圾回收

  • 什么是垃圾回收?
    • 垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
    • 注意:垃圾回收回收的是无任何引用的对象占据的内存空间而不是对象本身。换言之,垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,可以理解为一种文字游戏。
  • 分析一下
    • 引用:如果Reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。(引用都有哪些?对垃圾回收又有什么影响?)
    • 垃圾:无任何对象引用的对象(怎么通过算法找到这些对象呢?)。
    • 回收:清理“垃圾”占用的内存空间而非对象本身(怎么通过算法实现回收呢?)。
    • 发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中(堆内存为了配合垃圾回收有什么不同区域划分,各区域有什么不同?)。
    • 发生时间:程序空闲时间不定时回收(回收的执行机制是什么?是否可以通过显示调用函数的方式来确定的进行回收过程?)

02.内存垃圾回收机制

  • 是从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,当遍历一遍后得到上述这些无法回收的对象和他们所引用的对象链,组成无法回收的对象集合,而其他孤立对象(集)就作为垃圾回收
  • GC为了能够正确释放对象,必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC都需要进行监控。监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

03.关于GC概念介绍

  • 有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义,该函数不保证JVM的垃圾收集器一定会执行。因为不同的JVM实现者可能使用不同的算法管理GC
  • 通常GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。
    通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间

04.如何监听GC过程

  • 系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下
    D/dalvikvm: , ,
  • 第一部分GC_Reason,这个是触发这次GC操作的
    1
    2
    3
    4
    5
    原因,一般情况下一共有以下几种触发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操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了

05.GC过程与对象的引用类型关系

  • Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference
  • 软引用和弱引用
    • 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术
    • 软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
    • 利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。
    • 内存泄漏的原因:堆内存中的长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄露的根本原因

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.触发主GC的条件

  • 触发主GC的条件有哪些?
    • 1)当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
    • 2)Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
    • 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的机会。

其他介绍

01.关于博客汇总链接

02.关于我的博客