Java问题系列:内存回收

        Java内存的分配(创建Java对象)和回收(回收Java对象)构成了Java内存管理。

        我们通过关键字new创建Java对象,JVM会在堆(heap)内存中为每个对象分配空间;当一个对象失去引用时,JVM的垃圾回收器会自动清除它们,并回收它们所占用的内存空间。

    

        Q:JVM在何时决定回收一个Java对象所占用的内存?

        A:对于一个Java对象,如果不再有引用指向这个对象,那么JVM的垃圾回收器就可以回收它(不一定会回收)。对于软引用和弱引用,JVM的垃圾回收器会根据条件对它们进行回收。对于只有软引用指向的对象,当系统内存空间不足时,JVM将会对它进行回收。对于只有弱引用指向的对象,当JVM的垃圾回收器运行时,总会对它进行回收。


        Q:JVM会不会漏掉回收某些Java对象,使之造成内存泄漏?

        A:会。如果程序中有一些Java对象,它们处于可达状态,但程序以后永远不会再访问它们,那它们占用的内存空间也不会被回收,这就会产生内存泄漏。

1 对象在内存中的状态

        基本上,可以把JVM内存中对象引用理解成一种有向图;把引用变量、对象当成有向图的顶点,将引用关系当成图的有向边,有向边总是从引用端指向被引用的Java对象。因为Java所有对象都是由线程创建出来的,因此可以把线程对象当成有向图的起始顶点。对于单线程程序而言,整个程序只有一条main线程,那么该图就是以main线程为起始顶点的有向图。

        JVM的垃圾回收器使用有向图方式来管理内存中的对象,因此可以方便地处理循环引用的问题。例如,有3个对象相互引用,A对象引用B对象,B对象引用C对象,C对象又引用A对象,只要从有向图的起始顶点不可到达它们,垃圾回收器就会回收它们。

        采用有向图来管理内存中的对象具有高的精度,但缺点是效率较低。

        以下列程序为例:

class Node
{
	Node next;
	String name;
	public Node(String name)
	{
		this.name = name;
	}
}
public class NodeTest
{
	public static void main(String[] args) 
	{
		Node n1 = new Node("第一个节点");
		Node n2 = new Node("第二个节点");
		Node n3 = new Node("第三个节点");
		n1.next = n2;
		n3 = n2;
		n2 = null;
	}
}

         以上程序对应的有向图如下:

Java问题系列:内存回收_第1张图片


        当一个对象在堆内存中运行时,根据它在对应有向图中的状态,可以把它所处的状态分成3种。

  • 可达状态

    当一个对象被创建后,有一个以上的引用指向它,在有向图中可以从起始顶点导航到该对象,那么它就处于可达状态。

  • 可恢复状态

    对于一个处于可达状态的对象,如果不再有任何引用指向它,在有向图中从起始顶点不能导航到该对象,那么它进入可恢复状态。在这个状态下,JVM的垃圾回收器准备回收该对象占用的内存。但在回收该对象之前,JVM会调用该对象的finalize()进行资源清理。如果在调用finalize()期间重新让一个以上引用指向该对象,则该对象会再次进入可达状态;否则,该对象将进入不可达状态。

  • 不可达状态

    当一个对象进入不可达状态后,JVM的垃圾回收器才会真正回收该对象所占用的内存。

    Java问题系列:内存回收_第2张图片

2 Java内存泄漏

        程序运行过程中会不断地分配内存空间,那些不再使用的内存空间应该及时回收它们,如果存在无用的内存没有被回收回来,那就是内存泄漏。

        对于Java程序而言,所有不可达的对象都由垃圾回收器负责回收,因此我们不需要kao这部分的内存泄漏。但如果程序中有一些Java对象,它们处于可达状态,但程序以后永远不会再访问它们,那它们占用的内存空间也不会被回收,这就会产生内存泄漏。

        Java问题系列:内存回收_第3张图片

        

        参kaoArrayList中的remove()的源代码,看它是如何避免内存泄漏的:

public E remove(int index) {
	RangeCheck(index); // 检查index索引是否越界

	modCount++; // 使修改次数加1
	E oldValue = (E) elementData[index]; // 获取被删除的元素
    
    // 整体搬家
	int numMoved = size - index - 1;
	if (numMoved > 0)
		System.arraycopy(elementData, index + 1, elementData, index, numMoved);
		elementData[--size] = null; // 将最后一个数组元素赋为null,让垃圾回收器来回收它

	return oldValue;
}

3 Java引用的种类

        java.lang.ref提供了与JVM的垃圾回收器密切相关的引用类。这些引用类对象可以保持对使用对象的引用,同时JVM依然可以在内存不够用的时候对使用对象进行回收。同时,该包也提供了在对象的“可达”状态发生改变时,进行提醒的机制。

        java.lang.ref包通常用于实现与缓存相关的应用。


        java.lang.ref 包中类的继承关系,见下图:

Java问题系列:内存回收_第4张图片


        Java语言对对象的引用有4种:强引用(FinalReference)、软引用(SoftReference)、弱引用(WeakReference)和虚引用(PhantomReference)。

引用类型 取得目标对象的方式 垃圾回收条件 是否可能内存泄漏
强引用(FinalReference) 直接调用 不回收 可能
软引用(SoftReference) 通过get()调用 视内存情况回收 不可能
弱引用(WeakReference) 通过get()调用 永远回收 不可能
虚引用(PhantomReference) 无法取得 不回收 可能


        强引用(FinalReference)

        我们通常都是使用强引用来对对象进行引用。如:

String str = new String(“强引用字符串”);

        此处的str引用就称之为强引用。

        在JVM的实现中,实际上采用了FinalReference类对其进行引用,而Finalizer作为实现类来管理强引用对象。


    强引用具有如下特点:

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

  • 强引用所指向的对象在任何时候都不会被JVM回收。即时系统内存非常紧张,即使有些对象以后永远都不会被用到,JVM也不会对它进行回收。

  • 由于JVM肯定不会回收强引用指向的对象,所以强引用是造成内存泄漏的主要原因之一。


        软引用(SoftReference)

        软引用需要通过SoftReferece类来实现。如:

SoftReference<String> sr = new SoftReference<String>(new String("软引用字符串")); 
System.out.println(sr.get()); // “软引用字符串”


        软引用具有如下特点:

  • 软引用通过get()访问目标对象。

  • 软引用指向的对象有可能被JVM回收。对于只有软引用指向的对象,当系统内存空间充足时,JVM不会对它进行回收;当系统内存空间不足时,JVM将会对它进行回收。

  • 由于JVM根据系统内存空间情况决定是否回收软引用指向的对象,所以软引用不会造成内存泄漏。


        弱引用(WeakReference)

        弱引用通过WeakReference类实现。如:

WeakReference<String> wr = new WeakReference <String>(new String("弱引用字符串")); 
System.out.println(wr.get()); // “弱引用字符串”


        弱引用具有如下特点:

  • 弱引用通过get()访问目标对象。

  • 对于只有弱引用指向的对象,当JVM的垃圾回收器运行时,总会对它进行回收。当然,并不是说它马上就会被回收,必须等到垃圾回收器运行的时候。

  • 由于JVM总会回收弱引用指向的对象,所以弱引用不会造成内存泄漏。

        另外,与WeakReference功能类似的还有java.util.WeakHashMap。


        虚引用(PhantomReference)

        虚引用通过PhantomReference类来实现,同时它必须和引用队列ReferenceQueue联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态;程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用指向的对象的回收情况。

ReferenceQueue<String> rq = new ReferenceQueue<String>();
PhantomReference<String> pr = new PhantomReference<String>(new String("虚引用字符串"), rq);
// 不能访问虚引用指向的对象,输出null
System.out.println(pr.get()); 
// 强制垃圾回收
System.gc();
System.runFinalization();
// 取出引用队列中最先进入队列中的引用与pr进行比较,输出true
System.out.println(rq.poll() == pr);


        虚引用具有如下特点:

  • 不能访问弱引用指向的目标对象。

  • 对于虚引用引用的对象,JVM可能会回收它,之后JVM会将把虚引用添加到关联的引用队列中。注意:根据JVM实现的不同,可能会有所差异。

  • 由于JVM不一定会回收虚引用指向的对象,所以虚引用有可能造成内存泄漏。


        不同 Java 虚拟机上的表现与分析

        让我们来回顾一下四种引用类型的表现以及在垃圾回收器回收清理内存时的表现。注意,不同的虚拟机实现,可能会使得结果有所差异。

  • 强引用 (FinalReference), 这是最常用的引用类型 . JVM 系统采用 Finalizer 来管理每个强引用对象 , 并将其被标记要清理时加入 ReferenceQueue, 并逐一调用该对象的 finalize() 方法 .

  • 软引用 (SoftReference), 引用类型表现为当内存接近满负荷 , 或对象由 SoftReference.get() 方法的调用没有发生一段时间后 , 垃圾回收器将会清理该对象 . 在运行对象的 finalize 方法前 , 会将软引用对象加入 ReferenceQueue 中去 .

  • 弱引用 (WeakReference), 引用类型表现为当系统垃圾回收器开始回收时 , 则立即会回收该对象的引用 . 与软引用一样 , 弱引用也会在运行对象的 finalize 方法之前将弱引用对象加入 ReferenceQueue.

  • 虚引用 (PhantomReference), 这是一个最虚幻的引用类型 . 无论是从哪里都无法再次返回被虚引用所引用的对象 . 虚引用在系统垃圾回收器开始回收对象时 , 将直接调用 finalize() 方法 , 但不会立即将其加入回收队列 . 只有在真正对象被 GC 清除时 , 才会将其加入 Reference 队列中去 .

4 内存管理的小技巧

  • 尽量使用直接量来创建对象

        例如:

String str = “hello”;
  • 使用StringBuilder和StringBuffer进行字符串连接

        使用String进行字符串连接运算,在运行时将生成大量临时字符串,占用内存空间,导致性能下降。

  • 尽早释放无用对象的引用

        因为局部变量会随着方法结束而变成垃圾,所以大部分时候无需将局部变量显式设置为null;但是有时又是必要的,例如:

public void info(){
	Object obj = new Object();
	ojb = null;
	// 执行耗时、耗内存操作或者调用耗时、耗内存的方法
	…
}
  • 尽量少用静态变量

        静态变量的生命周期与所属的类同步。

  • 避免在经常调用的方法、循环中创建Java对象

        系统频繁地为对象分配和回收内存空间,将对性能造成巨大的影响。

  • 缓存经常使用的对象

        使用HashMap进行缓存;或者使用OSCache、Ehcache等开源项目。

  • 尽量不要使用finalize()

        finalize()是不可靠的,而且JVM的垃圾回收器工作量已经够大了,不要再增加它的负担。

  • kao虑使用SoftReference

        当内存充足时,它的功能等同于普通引用;当内存不足时,它会牺牲自己,释放所引用的对象。注意:使用软引用时不要忘记软引用的不确定性。当通过软引用获取到所引用的对象后,应该显式判断该对象是否为null;当该对象为null时,应重建该对象。





你可能感兴趣的:(垃圾回收,内存回收,JavaSE,内存管理,java引用)