Java内存分配机制

JVM内存管理

  • Java采用GC进行内存管理

  • Android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap(堆)进行遍历。到最后,部分没有直接或者间接引用到GC Roots的就是需要回收的垃圾,会被GC回收掉。而内存泄漏出现的原因就是存在了无效的引用,导致本来需要被GC的对象没有被回收掉。

  • 理解内存分配

    • Java虚拟机是先一次性分配一块较大的空间,然后每次new时都在该空间上进行分配和释放,减少了系统调用的次数,节省了一定的开销,这有点类似于内存池的概念;二是有了这块空间过后,如何进行分配和回收就跟GC机制有关了。
    • java一般内存申请有两种:静态内存和动态内存 。很容易理解,编译时就能够确定的内存就是静态内存,即内存是固定的,系统一次性分配,比如int类型变量;动态内存分配就是在程序执行时才知道要分配的存储空间大小,比如java对象的内存空间。
  • 深入的JVM内存管理知识,推荐《深入理解Java虚拟机》。

JVM内存分配的几种策略。

静态的

  • 静态的存储区,内存在程序编译的时候就已经分配好了,这块内存在程序整个运行期间都一直存在
    它主要存放静态数据、全局的static数据和一些常量。

栈式的

  • 在执行方法时,方法一些内部变量的存储都可以放在栈上面创建,方法执行结束的时候这些存储单元就会自动被注释掉。栈 内存包括分配的运算速度很快,因为内在在处理器里面。当然容量有限,并且栈式一块连续的内存区域,大小是由操作系统决定的,他先进后出,进出完成不会产生碎片,运行效率高且稳定

堆式的

  • 也叫动态内存 。我们通常使用new 来申请分配一个内存。这里也是我们讨论内存泄漏优化的关键存储区。GC会根据内存的使用情况,对堆内存里的垃圾内存进行回收。堆内存是一块不连续的内存区域,如果频繁地new/remove会造成大量的内存碎片,GC频繁的回收,导致内存抖动,这也会消耗我们应用的性能

堆和栈的区别

  • 在函数中(说明是局部变量)定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。
  • 当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
  • 堆内存用于存放所有由new创建的对象(内容包括该对象其中的所有成员变量)和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。
  • 在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。

得出结论

  • 1.局部变量的基本数据类型和引用,存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。
  • 2.成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被new出来使用的。
  • 3.我们所说的内存泄露,只针对堆内存,他们存放的就是引用指向的对象实体。

举个例子

1
2
3
4
5
6
7
8
9
10
11
public class Sample() {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上的。
mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

调用 System.gc();进行内存回收

  • 我们知道可以调用 System.gc();进行内存回收,但是GC不一定会执行。面对GC的机制,我们是否无能为力?其实我们可以通过声明一些引用标记来让GC更好对内存进行回收。

  • 小技巧
  • 成员变量全部存储在堆中(包括基本数据类型,引用及引用的对象实体),因为他们属于类,类对象最终还是要被new出来的
  • 局部变量的基本数据类型和引用存在栈中,应用的对象实体存储在堆中。因为它们属于方法当中的变量,生命周期会随着方法一起结束

内存泄漏简单介绍

内存泄漏的定义
当一个对象已经不需要使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用,从而导致了对象不能被GC回收。这种导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏

内存泄漏与内存溢出的区别

  • 内存泄漏(Memory Leak)
    • 进程中某些对象已经没有使用的价值了,但是他们却还可以直接或间接地被引用到GC Root导致无法回收。当内存泄漏过多的时候,再加上应用本身占用的内存,日积月累最终就会导致内存溢出OOM
  • 内存溢出(OOM)
    • 当应用的heap资源超过了Dalvik虚拟机分配的内存就会内存溢出

内存泄漏带来的影响

  • 应用卡顿
    • 泄漏的内存影响了GC的内存分配,过多的内存泄漏会影响应用的执行效率
  • 应用异常(OOM)
    • 过多的内存泄漏,最终会导致 Dalvik分配的内存,出现OOM

典型内存泄漏案例

  • 案例代码

    1
    2
    3
    4
    5
    6
    Vector v = new Vector(10);
    for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;
    }
  • 分析

    • 在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

GC回收机制

什么是垃圾回收

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

内存垃圾回收机制

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

关于GC概念介绍

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

如何监听GC过程

  • 系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下
    D/dalvikvm: , ,

  • 第一部分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操作的开始和结束的时候会短暂阻塞一段时间,不过优化到这种程度,用户已经是完全无法察觉到了

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

Java对引用的分类Strong reference, SoftReference, WeakReference, PhatomReference

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

与垃圾回收有关函数

  • 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 会释放该线程上的所有同步锁。

触发主GC的条件

  • 触发主GC的条件有哪些?
    • 当应用程序空闲时,即没有应用线程在运行时,GC会被调用。因为GC在优先级最低的线程中进行,所以当应用忙时,GC线程就不会被调用,但以下条件除外。
    • Java堆内存不足时,GC会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用GC线程,以便回收内存用于新的分配。若GC一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。
    • 在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。
  • 由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。

减少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的机会。

四种引用

引用说明

  • java.lang.ref包中提供了几个类:SoftReference类、WeakReference类和PhantomReference类,它们分别代表软引用、弱引用和虚引用。ReferenceQueue类表示引用队列,它可以和这三种引用类联合使用,以便跟踪Java虚拟机回收所引用的对象的活动。

Java下ref包和Android下ref包

  • 在Android下的ref包结构
  • 在java下的ref包

强引用

  • 关于强引用引用的场景
    • 直接new出来的对象
    • String str = new String(“yc”);

强引用介绍

  • 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
  • 通过引用,可以对堆中的对象进行操作。在某个函数中,当创建了一个对象,该对象被分配在堆中,通过这个对象的引用才能对这个对象进行操作。

强引用的特点

  • 强引用可以直接访问目标对象。

  • 强引用所指向的对象在任何时候都不会被系统回收。JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。

  • 强引用可能导致内存泄露。

  • 注意相互引用情况

软引用

关于SoftReference软引用

  • SoftReference:软引用–>当虚拟机内存不足时,将会回收它指向的对象;需要获取对象时,可以调用get方法。
  • 可以通过java.lang.ref.SoftReference使用软引用。一个持有软引用的对象,不会被JVM很快回收,JVM会根据当前堆的使用情况来判断何时回收。当堆的使用率临近阈值时,才会回收软引用的对象。

软引用应用场景

  • 例如从网络上获取图片,然后将获取的图片显示的同时,通过软引用缓存起来。当下次再去网络上获取图片时,首先会检查要获取的图片缓存中是否存在,若存在,直接取出来,不需要再去网络上获取。

软引用的简单使用

  • 用法如下

    1
    2
    3
    MyObject aRef = new  MyObject();
    SoftReference aSoftRef = new SoftReference(aRef);
    MyObject anotherRef = (MyObject)aSoftRef.get();

软引用的特点

  • 特点:如果一个对象只具有软引用

    • 那么如果内存空间足够,垃圾回收器就不会回收它;
    • 如果内存空间不足了,就会回收这些对象的内存。
    • 只要垃圾回收器没有回收它,该对象就可以被程序使用。
    • 软引用可用来实现内存敏感的高速缓存。
    • 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 代码如下

    1
    2
    ReferenceQueue queue = new  ReferenceQueue();
    SoftReference ref = new SoftReference(aMyObject, queue);
  • 如何回收:

    • 那么当这个SoftReference所软引用的aMyOhject被垃圾收集器回收的同时,ref所强引用的SoftReference对象被列入ReferenceQueue。
    • 也就是说,ReferenceQueue中保存的对象是Reference对象,而且是已经失去了它所软引用的对象的Reference对象。
    • 另外从ReferenceQueue这个名字也可以看出,它是一个队列,当我们调用它的poll()方法的时候,如果这个队列中不是空队列,那么将返回队列前面的那个Reference对象。
  • 在任何时候,我们都可以调用ReferenceQueue的poll()方法来检查是否有它所关心的非强可及对象被回收。

    • 如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
    • 利用这个方法,我们可以检查哪个SoftReference所软引用的对象已经被回收。
    • 于是我们可以把这些失去所软引用的对象的SoftReference对象清除掉。
  • 常用的方式为

    1
    2
    3
    4
    SoftReference ref = null;
    while ((ref = (EmployeeRef) q.poll()) != null) {
    // 清除ref
    }

实际应用案例

  • 正常是用来处理图片这种占用内存大的情况

  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    View view = findViewById(R.id.button);
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
    Drawable drawable = new BitmapDrawable(bitmap);
    SoftReference<Drawable> drawableSoftReference = new SoftReference<Drawable>(drawable);
    if(drawableSoftReference != null) {
    view.setBackground(drawableSoftReference.get());
    }
  • 这样使用软引用好处

    • 通过软引用的get()方法,取得drawable对象实例的强引用,发现对象被未回收。在GC在内存充足的情况下,不会回收软引用对象。此时view的背景显示
    • 实际情况中,我们会获取很多图片.然后可能给很多个view展示, 这种情况下很容易内存吃紧导致oom,内存吃紧,系统开始会GC。这次GC后,drawables.get()不再返回Drawable对象,而是返回null,这时屏幕上背景图不显示,说明在系统内存紧张的情况下,软引用被回收。
    • 使用软引用以后,在OutOfMemory异常发生之前,这些缓存的图片资源的内存空间可以被释放掉的,从而避免内存达到上限,避免Crash发生。

注意避免软引用获取对象为null

  • 在垃圾回收器对这个Java对象回收前,SoftReference类所提供的get方法会返回Java对象的强引用,一旦垃圾线程回收该Java对象之后,get方法将返回null。所以在获取软引用对象的代码中,一定要判断是否为null,以免出现NullPointerException异常导致应用崩溃。

弱引用

WeakReference弱引用

  • *WeakReference**
  • 弱引用–>随时可能会被垃圾回收器回收,不一定要等到虚拟机内存不足时才强制回收。要获取对象时,同样可以调用get方法。
  • 特点
    • 如果一个对象只具有弱引用,那么在垃圾回收器线程扫描的过程中,一旦发现了只具有弱引用的对象
    • 不管当前内存空间足够与否,都会回收它的内存。
    • 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
  • 弱引用也可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

WeakReference:防止内存泄漏,要保证内存被虚拟机回收

  • 先看一个handler小案例【千万不要忽视淡黄色警告】

  • 为什么这样会造成内存泄漏

    • 这种情况就是由于android的特殊机制造成的:当一个android主线程被创建的时候,同时会有一个Looper对象被创建,而这个Looper对象会实现一个MessageQueue(消息队列),当我们创建一个handler对象时,而handler的作用就是放入和取出消息从这个消息队列中,每当我们通过handler将一个msg放入消息队列时,这个msg就会持有一个handler对象的引用。因此当Activity被结束后,这个msg在被取出来之前,这msg会继续存活,但是这个msg持有handler的引用,而handler在Activity中创建,会持有Activity的引用,因而当Activity结束后,Activity对象并不能够被gc回收,因而出现内存泄漏。
  • 根本原因

    • Activity在被结束之后,MessageQueue并不会随之被结束,如果这个消息队列中存在msg,则导致持有handler的引用,但是又由于Activity被结束了,msg无法被处理,从而导致永久持有handler对象,handler永久持有Activity对象,于是发生内存泄漏。但是为什么为static类型就会解决这个问题呢?因为在java中所有非静态的对象都会持有当前类的强引用,而静态对象则只会持有当前类的弱引用。声明为静态后,handler将会持有一个Activity的弱引用,而弱引用会很容易被gc回收,这样就能解决Activity结束后,gc却无法回收的情况。

弱引用解决办法

  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private MyHandler handler = new MyHandler(this);
    private static class MyHandler extends Handler{
    WeakReference<FirstActivity> weakReference;
    MyHandler(FirstActivity activity) {
    weakReference = new WeakReference<>(activity);
    }

    @Override
    public void handleMessage(Message msg) {
    super.handleMessage(msg);
    switch (msg.what){
    }
    }
    }

弱引用实际应用案例

  • 代码:

虚引用

关于PhantomReference类虚引用

  • 虚引用是所有引用类型中最弱的一个。一个持有虚引用的对象,和没有引用几乎是一样的,随时都可能被垃圾回收器回收。当试图通过虚引用的get()方法取得强引用时,总是会失败。并且,虚引用必须和引用队列一起使用,它的作用在于跟踪垃圾回收过程。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在垃圾回收后,销毁这个对象,奖这个虚引用加入引用队列。

Android实际开发中没有用到过

  • 貌似开发中没有接触过虚引用

其他介绍

弱引用和软引用区别

  • 弱引用与软引用的根本区别在于:只具有弱引用的对象拥有更短暂的生命周期,可能随时被回收。
  • 而只具有软引用的对象只有当内存不够的时候才被回收,在内存足够的时候,通常不被回收。

使用软引用或者弱引用防止内存泄漏

  • 在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。
  • 软引用,弱引用都非常适合来保存那些可有可无的缓存数据。如果这样做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间。

到底什么时候使用软引用,什么时候使用弱引用呢?

  • 个人认为,如果只是想避免OutOfMemory异常的发生,则可以使用软引用。如果对于应用的性能更在意,想尽快回收一些占用内存比较大的对象,则可以使用弱引用。
  • 还有就是可以根据对象是否经常使用来判断。如果该对象可能会经常使用的,就尽量用软引用。如果该对象不被使用的可能性更大些,就可以用弱引用。
  • 另外,和弱引用功能类似的是WeakHashMap。WeakHashMap对于一个给定的键,其映射的存在并不阻止垃圾回收器对该键的回收,回收以后,其条目从映射中有效地移除。WeakHashMap使用ReferenceQueue实现的这种机制。

四种引用用一张表总结[摘自网络]

源码分析

首先看看如何通过弱引用加载图片

Reference的源代码

  • 源码说明:

  • 看到Reference除了带有对象引用referent的构造函数,还有一个带有ReferenceQueue参数的构造函数。那么这个ReferenceQueue用来做什么呢?

  • 需要我们从enqueue这个函数来开始分析。当系统要回收Reference持有的对象引用referent的时候,Reference的enqueue函数会被调用,而在这个函数中调用了ReferenceQueue的enqueue函数。

  • 那么我们来看看ReferenceQueue的enqueue函数做了什么?

  • 看看这段源代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    public abstract class Reference<T> {

    private static boolean disableIntrinsic = false;
    private static boolean slowPathEnabled = false;
    volatile T referent; /* Treated specially by GC */
    final ReferenceQueue<? super T> queue;
    Reference queueNext;
    Reference<?> pendingNext;

    //返回此引用对象的引用。如果这个引用对象有由程序或垃圾收集器清除,然后此方法返回
    public T get() {
    return getReferent();
    }

    private final native T getReferent();

    //清除此引用对象。调用此方法不会将对象加入队列
    public void clear() {
    this.referent = null;
    }

    //是否引用对象已进入队列,由程序或垃圾收集器。
    //如果该引用对象在创建队列时没有注册,则该方法将始终返回
    public boolean isEnqueued() {
    return queue != null && queue.isEnqueued(this);
    }

    //添加引用对象到其注册的队列,如果他的方法是通过java代码调用
    public boolean enqueue() {
    return queue != null && queue.enqueue(this);
    }

    Reference(T referent) {
    this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
    this.referent = referent;
    this.queue = queue;
    }
    }

ReferenceQueue的enqueue函数

  • 源码说明
  • 可以看到首先获取同步锁,然后调用了enqueueLocked(Reference)函数

ReferenceQueue的enqueueLocked(Reference)函数

  • 源码说明
  • 通过 enqueueLocked函数可以看到ReferenceQueue维护了一个队列(链表结构),而enqueue这一系列函数就是将reference添加到这个队列(链表)中

ReferenceQueue.isEnqueued()代码

  • 让我们回到Reference源码中

  • 可以看到除了enqueue这个函数还有一个isEnqueued函数,同样这个函数调用了ReferenceQueue的同名函数,源码如下:

    1
    2
    3
    4
    5
    boolean isEnqueued(Reference<? extends T> reference) {
    synchronized (lock) {
    return reference.queueNext != null && reference.queueNext != sQueueNextUnenqueued;
    }
    }
  • 源码分析说明

  • 可以看到先获取同步锁,然后判断该reference是否在队列(链表)中。由于enqueue和isEnqueue函数都要申请同步锁,所以这是线程安全的。

  • 这里要注意“reference.queueNext != sQueueNextUnenqueued”用于判断该Reference是否是一个Cleaner类,在上面ReferenceQueue的enqueueLocked函数中我们可以看到如果一个Reference是一个Cleaner,则调用它的clean方法,同时并不加入链表,并且将其queueNext设置为sQueueNextUnequeued,这是一个空的虚引用

enqueueLocked(Reference)函数中的Cleaner是做什么的

  • 在stackoverflow网站中找到这个解释
    • sun.misc.Cleaner是JDK内部提供的用来释放非堆内存资源的API。JVM只会帮我们自动释放堆内存资源,但是它提供了回调机制,通过这个类能方便的释放系统的其他资源。
    • 可以看到Cleaner是用于释放非堆内存的,所以做特殊处理。
    • 通过enqueue和isEnqueue两个函数的分析,ReferenceQueue队列维护了那些被回收对象referent的Reference的引用,这样通过isEnqueue就可以判断对象referent是否已经被回收,用于一些情况的处理。

软引用SoftReference源码

  • 关于这段源码分析

  • 可以看到SoftReference有一个类变量clock和一个变量timestamp,这两个参数对于SoftReference至关重要。

    • clock:记录了上一次GC的时间。这个变量由GC(garbage collector)来改变。
    • timestamp:记录对象被访问(get函数)时最近一次GC的时间。
  • 那么这两个参数有什么用?

    • 我们知道软引用是当内存不足时可以回收的。但是这只是大致情况,实际上软应用的回收有一个条件:
    • clock - timestamp <= free_heap * ms_per_mb
    • free_heap是JVM Heap的空闲大小,单位是MB
    • ms_per_mb单位是毫秒,是每MB空闲允许保留软引用的时间。Sun JVM可以通过参数-XX:SoftRefLRUPolicyMSPerMB进行设置
  • 举个栗子:

    • 目前有3MB的空闲,ms_per_mb为1000,这时如果clock和timestamp分别为5000和2000,那么
    • 5000 - 2000 <= 3 * 1000
    • 条件成立,则该次GC不对该软引用进行回收。
    • 所以每次GC时,通过上面的条件去判断软应用是否可以回收并进行回收,即我们通常说的内存不足时被回收。
  • 源码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class SoftReference<T> extends Reference<T> {
    static private long clock;
    private long timestamp;
    public SoftReference(T referent) {
    super(referent);
    this.timestamp = clock;
    }
    public SoftReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
    this.timestamp = clock;
    }
    public T get() {
    T o = super.get();
    if (o != null && this.timestamp != clock)
    this.timestamp = clock;
    return o;
    }
    }

弱引用WeakReference源码

  • 源码分析说明

  • 没有其他代码,GC时被回收掉。

  • 源码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    public class WeakReference<T> extends Reference<T> {
    public WeakReference(T referent) {
    super(referent);
    }
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
    }
    }

虚引用PhantomReference源码

  • 源码分析说明

  • 可以看到get函数返回null,正如前面说得虚引用无法获取对象引用。(注意网上有些文章说虚引用不持有对象的引用,这是有误的,通过构造函数可以看到虚引用是持有对象引用的,但是无法获取该引用

  • 同时可以看到虚引用只有一个构造函数,所以必须传入ReferenceQueue对象。

  • 前面提到虚引用的作用是判断对象是否被回收,这个功能正是通过ReferenceQueue实现的。

  • 这里注意:不仅仅是虚引用可以判断回收,弱引用和软引用同样实现了带有ReferenceQueue的构造函数,如果创建时传入了一个ReferenceQueue对象,同样也可以判断。

  • 源码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    public class PhantomReference<T> extends Reference<T> {
    public T get() {
    return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
    }
    }

检测垃圾的算法

  • 垃圾收集器一般必须完成两件事:检测出垃圾;回收垃圾。怎么检测出垃圾?一般有以下几种方法:引用计数法,可达性分析算法。

引用计数法

  • a.引用计数法:
    • 给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。好了,问题来了,如果我有两个对象A和B,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收。
    • 引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。
    • 引用计数的优点:
      • 垃圾收集器可以很快地执行,当一个对象的引用数为0时就可以回收这个对象,垃圾收集交织在程序的正常执行过程中,不用长时间中断程序的正常执行。
    • 引用计数的缺点:
      • 1.每次引用计数的增加和减少会带来额外的开销
      • 2**.无法检测出循环引用**
      • 总结一下: 难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。

可达性分析算法

  • 可达性分析算法

    • 以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常量池中引用的对象,本地方法中引用的对象等
    • 有时候通常将可达性分析算法称为根搜索算法。
  • 什么是根集

    • 所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
    • 这种算法的基本思路:
      • (1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
      • (2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
      • (3)重复(2)。
      • (4)搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。

标记可达对象

  • JVM中用到的所有现代GC算法在回收前都会先找出所有仍存活的对象。根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图。下图中所展示的JVM中的内存布局可以用来很好地阐释这一概念:
  • 首先,垃圾回收器将某些特殊的对象定义为GC根对象。
    • 所谓的GC根对象包括:
    • (1)虚拟机栈中引用的对象(栈帧中的本地变量表);
    • (2)方法区中的常量引用的对象;
    • (3)方法区中的类静态属性引用的对象;
    • (4)本地方法栈中JNI(Native方法)的引用对象。
    • (5)活跃线程。
  • 接下来,垃圾回收器会对内存中的整个对象图进行遍历,它先从GC根对象开始,然后是根对象引用的其它对象,比如实例变量。回收器将访问到的所有对象都标记为存活。
  • 存活对象在上图中被标记为蓝色。当标记阶段完成了之后,所有的存活对象都已经被标记完了。其它的那些(上图中灰色的那些)也就是GC根对象不可达的对象,也就是说你的应用不会再用到它们了。这些就是垃圾对象,回收器将会在接下来的阶段中清除它们。关于标记阶段有几个关键点是值得注意的:
    • 开始进行标记前,需要先暂停应用线程,否则如果对象图一直在变化的话是无法真正去遍历它的。暂停应用线程以便JVM可以尽情地收拾家务的这种情况又被称之为安全点(Safe Point),这会触发一次Stop The World(STW)暂停。触发安全点的原因有许多,但最常见的应该就是垃圾回收了。
    • 暂停时间的长短并不取决于堆内对象的多少也不是堆的大小,而是存活对象的多少。因此,调高堆的大小并不会影响到标记阶段的时间长短。
    • 在根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程:
      • 1.如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
      • 2.如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
    • (4)实际上GC判断对象是否可达看的是强引用。
  • 当标记阶段完成后,GC开始进入下一阶段,删除不可达对象。

处理垃圾的算法

标记-清除(Mark-sweep)

  • 什么是标记-清除算法
    • 标记—清除算法是最基础的收集算法,为了解决引用计数法的问题而提出。它使用了根集的概念,它分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。
    • 这是最基础的算法,后续的收集算法都是基于这个算法扩展的。
  • 优点和缺点
    • 优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。
    • 缺点:
      • 标记和清除过程的效率都不高。(这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量。如图4.1所示。)。
      • 标记清除后会产生大量不连续的内存碎片。虽然空闲区域的大小是足够的,但却可能没有一个单一区域能够满足这次分配所需的大小,因此本次分配还是会失败(在Java中就是一次OutOfMemoryError)不得不触发另一次垃圾收集动作。
  • 算法示意图:

复制(Copying)

  • 什么是复制算法
    • 此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎片”问题。
    • 当然,此算法的缺点也是很明显的,就是需要两倍内存空间。
    • 该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。
    • 复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。一种典型的基于Coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象区和空闲区,在对象区与空闲区的切换过程中,程序暂停执行。
  • 它的优缺点
    • 优点:(1)标记阶段和复制阶段可以同时进行。(2)每次只对一块内存进行回收,运行高效。(3)只需移动栈顶指针,按顺序分配内存即可,实现简单。(4)内存回收时不用考虑内存碎片的出现(得活动对象所占的内存空间之间没有空闲间隔)。
    • 缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半。
  • 算法示意图

标记-整理(Mark-Compact)

  • 此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。

  • 什么是标记-整理算法

    • 该算法标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。
    • 此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
  • 它的优缺点

    • 优点:(1)经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。(2)使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。
    • 缺点:GC暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。
  • 算法示意图

分代收集算法(当今最常用的方法)

  • 这是当前商业虚拟机常用的垃圾收集算法。分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
  • 为什么要运用分代垃圾回收策略?在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,有的对象生命周期较长,比如Http请求中的Session对象,线程,Socket连接等;有的对象生命周期较短,比如String对象,由于其不变类的特性,有的在使用一次后即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会很长,而且对于存活时间较长的对象进行的扫描工作等都是徒劳。因此就需要引入分治的思想,所谓分治的思想就是因地制宜,将对象进行代的划分,把不同生命周期的对象放在不同的代上使用不同的垃圾回收方式。

如何对对象划分

如何对对象划分

  • 将对象按其生命周期划分
    • 年轻代(Young Generation)
    • 年老代(Old Generation)
    • 持久代(Permanent Generation)
    • 其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。

年轻代

  • 年轻代
    • 是所有新对象产生的地方。
    • 年轻代被分为3个部分——Enden区和两个Survivor(幸存者)区(From和to)
    • 当Eden区被对象填满时,就会执行Minor(新生) GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。

年老代

  • 年老代
    • 在年轻代中经历了N次回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。这时候MajsorGC会清理些老年代垃圾,通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。
    • 持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响。

GC中对象的六种可触及状态

  • 1.强可触及:对象可以从根结点不通过任何引用对象搜索到
  • 2.软可触及:对象不是强可触及的,但是可以从根结点开始通过一个或多个(未被清除的)软引用对象触及
  • 3.弱可触及:对象既不是强可触及也不是软可触及的,但是从根结点开始
  • 4.可复活的:对象既不是强可触及、软可触及,也不是弱可触及,但是仍然可能通过执行某些终结方法复活到这几种状态之一
  • 5.影子可触及:不上以上任何可触及状态,也不能通过终结方法复活,并且它可以从根结点开始通过一个或多个影子引用对象触及(影子引用不会被垃圾收集器清除,由程序明确地清除)
  • 6不可触及:就是已经准备回收的状态

垃圾回收器的类型

  • Java 提供多种类型的垃圾回收器。 JVM 中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。
    • Serial 收集器:新生代收集器,使用复制算法,使用一个线程进行 GC,串行,其它工作线程暂停。
    • ParNew 收集器:新生代收集器,使用复制算法,Serial 收集器的多线程版,用多个线程进行 GC,并行,其它工作线程暂停。 使用 -XX:+UseParNewGC 开关来控制使用 ParNew+Serial Old 收集器组合收集内存;使用 -XX:ParallelGCThreads 来设置执行内存回收的线程数。
    • Parallel Scavenge 收集器:吞吐量优先的垃圾回收器,作用在新生代,使用复制算法,关注 CPU 吞吐量,即运行用户代码的时间/总时间。 使用 -XX:+UseParallelGC 开关控制使用 Parallel Scavenge+Serial Old 收集器组合回收垃圾。
    • Serial Old 收集器:老年代收集器,单线程收集器,串行,使用标记整理算法,使用单线程进行GC,其它工作线程暂停。
    • Parallel Old 收集器:吞吐量优先的垃圾回收器,作用在老年代,多线程,并行,多线程机制与 Parallel Scavenge 差不错,使用标记整理算法,在 Parallel Old 执行时,仍然需要暂停其它线程。
    • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于获取最短回收停顿时间(即缩短垃圾回收的时间),使用标记清除算法,多线程,优点是并发收集(用户线程可以和GC线程同时工作),停顿小。 使用 -XX:+UseConcMarkSweepGC 进行 ParNew+CMS+Serial Old 进行内存回收,优先使用 ParNew+CMS,当用户线程内存不足时,采用备用方案 Serial Old 收集。

Java对象的访问定位

  • 建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定
  • 目前主流的访问方式有
    • ①使用句柄
    • ②直接指针
  • 这两种对象访问方式各有优势。
    • 使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
    • 使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。
  • 区别
    • 由于对象访问极其频繁,所以Hot Spot也使用第二种方式,直接存实例引用是效率比较高的。但是第一种句柄的方式,好处在于,垃圾回收中,不需要更改栈上所存储的地址,栈上的存储稳定,只需要修改句柄池。

通过句柄访问方式

  • 通过句柄访问方式
    • 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息;
  • 句柄是什么?
    • 一个唯一的整数,作为对象的身份id,区分不同的对象,和同类中的不同实例。程序可以通过句柄访问对象的部分信息。句柄不代表对象的内存地址。
    • 句柄和指针的区别:程序不能通过句柄直接阅读文件中的信息,指针是可以的。从所起的作用这点来说,句柄就是弱化后的指针,更安全,功能减少。

通过直接指针访问方式

  • 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何放置访问类型数据的相关信息,而reference 中存储的直接就是对象的地址。

Java对象销毁

JVM内存分配与回收

  • Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是 内存中对象的分配与回收。
  • JDK1.8之前的堆内存示意图:
    • 从上图可以看出堆内存的分为新生代、老年代和永久代。新生代又被进一步分为:Eden 区+Survior1 区+Survior2 区。值得注意的是,在JDK1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。
  • 分代回收算法
    • 目前主流的垃圾收集器都会采用分代回收算法,因此需要将堆内存分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
    • 大多数情况下,对象在新生代中 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倍以上。

判断对象是否死亡

  • 堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。

引用计数法

  • 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。

    • 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

    • 所谓对象之间的相互引用问题,如下面代码所示:除了对象objA和objB相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知 GC 回收器回收他们。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      public class Test {
      Object instance = null;
      public static void main(String[] args) {
      Test objA = new Test();
      Test objB = new Test();
      objA.instance = objB;
      objB.instance = objA;
      objA = null;
      objB = null;
      }
      }

可达性分析算法

  • 这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。

再谈引用

  • 无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与“引用”有关。
  • JDK1.2以后,Java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用四种(引用强度逐渐减弱)

不可达的对象并非“非死不可”

  • 即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
  • 被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

如何判断一个常量是废弃常量

  • 假如在常量池中存在字符串”abc” ,如果当前没有任何String对象引用该字符串常量的话,就说明常量”abc”就是废弃常量,如果这时发生内存回收的话而且有必要的话,” abc”就会被系统清理出常量池。

  • 注意:JDK1.7及之后版本的JVM已经将运行时常量池从方法区中移了出来,在Java堆(Heap) 开辟了一块区域存放运行时常池

如何判断一个类是无用的类

  • 方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面3个条件才能算是 “无用的类”
    • 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
    • 加载该类的 ClassLoader 已经被回收。
    • 该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
  • 虚拟机可以对满足上述3个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。

Java内存分配案例

  • 以下面代码为例,来分析,Java 的实例对象在内存中的空间分配。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    //JVM 启动时将 Person.class 放入方法区
    public class Person {

    //静态变量,直接放到常量池中
    public static final String number = "13667225184";

    //new Person 创建实例后,name 引用放入堆区,name 对象放入常量池
    private String name;

    //new Person 创建实例后,age = 0 放入堆区
    private int age;

    //Person 方法放入方法区,方法内代码作为 Code 属性放入方法区
    public Person(String name, int age) {
    this.name = name;
    this.age = age;
    }

    //toString 方法放入方法区,方法内代码作为 Code 属性放入方法区
    @Override
    public String toString() {
    return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
    }

    //JVM 启动时将 Test.class 放入方法区
    public class Test {

    //main 方法放入方法区,方法内代码作为 Code 属性放入方法区
    public static void main(String[] args) {

    //person1 是引用放入虚拟机栈区,new 关键字开辟堆内存 Person 自定义对象放入堆区
    Person person1 = new Person("张三", 18);
    Person person2 = new Person("李四", 20);

    //通过 person 引用创建 toString() 方法栈帧
    person1.toString();
    person2.toString();
    }
    }

JVM加载类过程

  • 首先 JVM 会将 Test.class, Person.class 加载到方法区,找到有 main() 方法的类开始执行。
  • 分析步骤
    • 如上图所示,JVM 找到 main() 方法入口,创建 main() 方法的栈帧放入虚拟机栈,开始执行 main() 方法。
    • Person person1 = new Person(“张三”, 18);
    • 执行到这句代码时,JVM 会先创建 Person。实例放入堆区,person2 也同理。

对构造方法赋值

  • 创建完 Person 两个实例,main() 方法中的 person1,person2 会指向堆区中的 0x001,0x002(这里的内存地址仅作为示范)。紧接着会调用 Person 的构造函数进行赋值,如下图:

  • 如上图所示,新创建的的 Person 实例中的 name, age 开始都是默认值。 调用构造函数之后进行赋值,name 是 String 引用类型,会在常量池中创建并将地址赋值给 name,age 是基本数据类型将直接保存数值。

  • 注:Java 中基本类型的包装类的大部分都实现了常量池技术,这些类是 Byte, Short, Integer, Long, Character, Boolean,另外两种浮点数类型的包装类则没有实现。

    基本数据类型 包装类(是否实现了常量池技术)
    byte Byte 是
    boolean Boolean 是
    short Short 是
    char Character 是
    int Integer 是
    long Long 是
    float Float 否
    double Double 否

通过对象调用方法

  • Person 实例初始化完后,执行到 toString() 方法,同 main() 方法一样 JVM 会创建一个 toString() 的栈帧放入虚拟机栈中,执行完之后返回一个值。