java垃圾回收机制整理

Posted lb-alex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java垃圾回收机制整理相关的知识,希望对你有一定的参考价值。

一、垃圾回收器和finalize() 

 java垃圾回收器只负责回收无用对象占据的内存资源。但是如果你的对象不是通过 new 创建的(所有的new 对象都往堆中开辟资源,在一个地方,方便清理/管理资源),它会不知道该如果释放该对象的这块特殊内存。为了应对这个情况,Object自带一个finalize()方法。

  finalize()这方法的原理是:一旦垃圾回收器准备释放该对象占用的存储空间,将会先调用其继承/重写的fialize(),并且调用方法后不是立即执行回收,而是在下一次(JVM觉得需要更大内存的时候)回收动作发生时,才会真正回收对象占用的内存。所以一般自己重写fialize()方法,是在回收的最后时刻做一些重要的清理工作。

java垃圾回收几个特点:

1、对象可能不被垃圾回收

   你创建的对象做了某个功能,比如显示在电脑的屏幕上。那么除非你特别处理从屏幕上擦除,它永远不可能得到清理。所以如果在finalize()方法中做擦除屏幕的处理,当垃圾回收时,finalize()被调用,屏幕图像清除。请注意:垃圾回收器只有在JVM觉得需要更大内存的时候才会运行(虽然开销小,但是一直运行还是有开销的),所以大部分回收动作是发生在濒临存储空间用完的那一刻,逼得JVM去运行垃圾回收器。如果程序执行结束 (或者中断运行),那些资源也会全部还给操作系统。

2、垃圾回收并不等于析构

  这个是C的概念,因为java和C的牵扯太深,所以经常拿来对比。简单说C有一个东西叫析构函数,在销毁对象前必须执行这个析构函数。这里的垃圾回收并不代表析构。finalize()就是类似功能但是不等于。

3、垃圾回收只与内存有关

  这里就要讲到finalize()的真正用途。该方法内部执行的操作也应该和内存及其回收有关,所以fialize()方法不是通用的方法。你可能会想到,当对象包含成员对象属性的时候,finalize()是否应该明确要清除那些对象呢?不正确。应该这样理解:无论对象如果创建,垃圾回收器都会负责释放对象占用的所有内存。所以finalize()一般是来处理通过创建对象以外的方式为对象分配存储空间。说起来有些绕口,但是举个例子就知道了。

  java跟踪源码的时候经常遇到关键字native修饰的方法,这些方法也叫“本地方法“。在使用这些本地方法的时候,内部调用的是非java代码的方式(不是C就是C++)。.这些非代码中,也许会用到C的malloc()函数系列来分配存储空间。这样除非调用C的free()方法,否则存储空间将不会释放。所以可以在finalize()使用native方式调用free。

  以上,就是建议尽量少重写finalize()的道理。

二、垃圾回收条件

  既然fialize()使用场景这么生僻,那就不要指望频繁使用fialize()。你必须创建其他的清理方法,来自己根据业务清理。但是fialize()有个特点是:程序调用它,是该对象“终结条件“的验证。也就是被标记了,该对象已死,可以回收了。例如:某个对象代表打开的一个文件,在对象被回收前程序员应该关闭文件。只要对象存在没有被适当清理的部分,程序就存在隐晦的缺陷。fialize()可以用来最终发现这种情况。

 


// Using finalize() to detect an object that
// hasn‘t been properly cleaned up.

class Book 
  boolean checkedOut = false;
  Book(boolean checkOut) 
    checkedOut = checkOut;
  
  void checkIn() 
    checkedOut = false;
  
  protected void finalize() 
    if(checkedOut)
      System.out.println("Error: checked out");
    // Normally, you‘ll also do this:
//     super.finalize(); // Call the base-class version
  


public class TerminationCondition 
  public static void main(String[] args) 
    Book novel = new Book(true);
    // Proper cleanup:
    novel.checkIn();
    // Drop the reference, forget to clean up:
    new Book(true);
    // Force garbage collection & finalization:
    System.gc();
  
/* Output:
Error: checked out
*///:~

 

这个例子的终结条件是:所有Book对象在被当做垃圾回收前都应该checkIn。但是在main里面,第二本书没有checkIn,这个时候通过finalize(),就能明确知道,有的对象没有在销毁前处理干净了。另外,代码中还使用了System.gc();这是强制唤起垃圾回收机器,来触发BOOK的finalize();当然,如果不这样强制唤起也行,当程序运行到被分配了大量内存的时候(可以大量反复创建BOOK),逼得垃圾回收器会自动触发。如果BOOK有继承某个父类,要触发该父类的finalize(),可以使用super.finalize();调用。

三、垃圾回收器如何工作

  一般印象里面,在堆内分配新资源会比较慢,毕竟比不了堆栈快。但是其实JVM在这方面是做了大量的优化,其中垃圾回收器对于提高对象的创建速度,具有明显效果。即使用垃圾回收器释放存储空间有利于未使用存储空间的分配。通俗点就是说,垃圾回收器回收的内存越多,创建对象理论上会更快(还是有临界的,一般认为媲美其他语言在堆栈中创建对象,比如C)。C的堆就好像一个院子,里面的每个对象各管各的存储空间。一段时间以后某个对象被销毁了。它的空间必须被重新使用。在某些JVM中,堆就像一个传送带,分配一个新的对象,它就往前移动一格。这个意味着空间分配会非常快(寻址快)。java的寻址指针只需要简单移动到尚未分配的区域就行,这样效率比得上C在堆栈上分配的速度。其中,记录对象空间地址“下标“方面,还是有部分开销的,但是比C需要查找堆的开销,小得多。其实,java中的堆未必完全是像传送带,因为这会造成频繁的内存页面调度(内存是分页的,翻页时是要移出硬盘,放在虚拟内存上)。页面调度会显著影响性能,最终,在创建足够的对象后,内存(大量虚拟内存充斥)资源耗尽。

  这里就轮到垃圾回收器登场了,当垃圾回收器工作的时候,一边回收空间,一边使堆中的对象排列紧凑。这样“堆指针”就可以很容易移动到更靠近传送带的开始处(java堆分配空间是先进),也就尽量避免了页面错误。垃圾回收期会对对象重新排列,实现高速的、有无限空间(?)可以分配的堆模型。

下面是垃圾回收(不止java)常用的三种设计方式:

1、引用计数

  每个对象含有一个应用计数。当有引用连接到对象的时候+1,引用离开作用域或者赋值null的时候-1。好了,那么当发现某个对象的引用是0的时候,就释放它占用的空间(这里会出现一变为0就释放空间)。这里就存在缺陷,如果对象循环引用,即A引用B,B引用A,就出现“对象可以被回收,但是引用计数不是0”的情况。对于垃圾回收器来说,定位这种互相引用的对象组开销极大。另外,管理引用计数的开销不大,但是这个会在整个程序生命周期内持续发生。引用计数的方式一般用来表述垃圾收集,但没有应用于任何一种JVM中。

2、stop-and-copy

  这种方式是先暂停程序(不是后台运行,而是停止程序,执行垃圾回收),然后将所以存活的对象从当前的堆中复制到另外一个堆,剩下的都是垃圾。当对象被复制到新的家(堆)时,会把这些对象一个挨着一个,所以新堆保持紧凑排列。这个就是前面说的JVM虚拟机的垃圾回收期为什么能做到使对象紧凑排列了。复制过程会产生新的开销,以及所有指向就旧对象的引用都要指向新的地址。这里可能出现来自非堆的引用(不是new出来的对象),这些会在遍历旧堆的引用的时候被找出来,重新指向新堆。

  这种回收方式,效率低。首先,是因为要有两个堆,然后对象要在两个堆中复制转移,所以实际上维护的空间比理论上大一倍。例如,某些JVM的做法是,在堆里面分配几个大的内存块,复制的操作在这里个内存块中进行。其次,当程序运行趋于稳定以后,产生的垃圾比较少,甚至可能没有垃圾。这个时候来回复制就很浪费了。为了避免浪费,JVM会进行检查,要是没有新垃圾产生,就自动转换成下一种模式。

3、mark-and-sweep

  Sun公司早期版本的虚拟机使用的就是这个。这种方式的思路是从堆和静态存储区出发,遍历所有的引用,然后就能找到所有存活对象。每当找到一个存活对象,就给该对象一个标记,记上一笔。所有标记都做完以后,清理开始。在清理的时候,没有标记的对象将被释放,不会发生复制动作。这个时候堆中就有点像C的样子,所以如果要让剩下的对象内存连续,就需要重新整理剩下的对象。

  

小结:

  Sun的文献把垃圾回收看做是低优先级的后台进程,指的是因为stop-and-copy:毕竟要暂停程序。但是在早期版本中,JVM使用的是mark-and-sweep。现在这两种回收方式通过JVM进行监视,如果有所对象都很稳定,垃圾回收器效率降低的话,就切换到mark-and-sweep。同样的,如果mark-and-sweep的效果不好,堆中出现了很多垃圾碎片(无引用对象),就会切换到stop-and-copy。在stop-and-copy使用的时候,因为内存分配以较大的“块”为单位,如果对象较大,它会占用整个块。在stop-and-copy运行的到停止程序运行的操作前,会把所有存活的对象复制到新的块(堆)中,这个时候旧的块就会被废弃,垃圾回收器就可以向废弃的块复制新对象进去,灵活利用资源。每个块都有相应的代数(count)来记录它是否还存活。如果块在某处被引用,count会增加。垃圾回收器将对上次回收动作之后新分配的块进行整理。这个対短命的对象很有帮助。垃圾回收期会定期进行完整的清理动作----大型对象仍然不会被复制,内含小型对象的那些块还是会被复制并且整理。

  JVM有许多的附加技术用以提升速度。比如JIT(Just-In-Time)编译器的技术。这个会把程序全部或部分翻译成本地机器码,程序运行速度因此快上很多。当需要装载某个类的时候(创建该类的第一个对象,后续创建不会再次装载),编译器先找到对应的class文件,然后把字节码内容装入内存中。这个时候,有两种方式可以选。其一是让JIT编译所有代码转换成机器码。但是因为装载的发生不可控制,是零散在整个程序的生命周期内的,累加起来就需要花费很多时间,并且会增加可执行代码的长度(字节码要比JIT编译后展开的机器码小很多),这个可能导致内存页面调度,从而降低程序的速度。其二是惰性评估(lazy evaluatio),意思是JIT只有在必要的时候才编译代码,这样从不会被执行的代码(import 进来,但是没有使用)就压根不会被JIT编译。JDK中的Java HotSpot技术就是采用了类似方法,代码每次执行都会做一些优化,所以执行次数越多,它的速度就越快。

技术图片

 

以上是关于java垃圾回收机制整理的主要内容,如果未能解决你的问题,请参考以下文章

Java学习之垃圾回收机制

Java学习笔记3.11.1 垃圾回收 - 垃圾回收的作用

垃圾回收机制重新整理篇

Java垃圾回收机制学习心得

Java底层 - 垃圾回收

深入理解 Java 垃圾回收机制