垃圾回收

Java的垃圾回收机制是Java虚拟机(JVM)提供的能力,用于在空闲时间以不定时的方式动态回收 无任何引用 的对象占据的 内存空间
需要注意的是: 垃圾回收回收的是无任何引用的对象占据的 内存空间 而不是对象本身。 垃圾回收的是堆中的内存,而不是栈中的内存!!
System.gc()
Runtime.getRuntime().gc()
上面的方法调用时用于显式通知JVM可以进行一次垃圾回收,但真正垃圾回收机制具体在什么时间点开始发生动作这同样是不可预料的。
编程时注意:
当某个对象不再需要的时候可以把它设为null,这样的话在下一次垃圾回收的时候该对象便可被回收,设为null不代表马上回收。还必须注意该引用的对象是否被监听,如果有,则要去掉监听器,然后再赋空值。 但没有必要每个对象用完都马上设为null。
需要设为null的情况:
1、对象是全局对象,会伴随整个程序的运行,当我们不需要该对象时应手动设为null
2、对象是局部对象(一个类可以是另一个类的局部对象),会在函数结束时自动释放,但如果对象占用的内存较大,占用时间较长,考虑到内存紧缺,可以在对象用完后(函数结束前)马上将其设为null
不需要设为null的情况:
1、对象是局部变量,且占用空间不大,时间不长,可以等到函数结束自动回收
2、对象是另一个对象的局部对象时,当包含它的对象被释放后(如设为null),该对象也自动释放

有两个条件会触发GC(Garbage Collector):
1、程序空闲时,因为GC在优先级最低的线程中进行,所以当应用程序忙时,GC线程不会被调用,条件2除外
2、java堆内存不足时
减少GC开销的几个措施:
1、不要显式调用System.gc()
此函数虽只是建议回收而并非强制回收,但很多情况下还是会增加GC的次数,因为GC可以一次回收多个对象,太频繁使用该函数会使得GC回收次数增加,每次回收的对象个数自然就少了
2、尽量减少临时对象的使用
3、对象不用时最好显式设为null
显式设为null与调用System.gc()不同,显式设为null有利于GC收集器判定垃圾,因为设为null的对象都是垃圾,都可以回收,而System.gc()只是告诉收集器有垃圾
4、尽量使用StringBuffer来累加字符串,少用String
5、能用基本类型如int、long就用基本类型,少用对象类型如Integer、Long对象
因为基本类型变量占用的内存资源比对应的对象类型少很多

JVM怎样确定哪些对象是“垃圾”:

1.引用计数器算法:

引用计数器算法是给每个对象设置一个计数器,当有地方引用这个对象的时候,计数器+1,当引用失效的时候,计数器-1,当计数器为0的时候,JVM就认为对象不再被使用,是“垃圾”了。

引用计数器实现简单,效率高;但是不能解决循环引用问问题(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用),同时每次计数器的增加和减少都带来了很多额外的开销,所以在JDK1.1之后,这个算法已经不再使用了。

2.根搜索方法:

根搜索方法是通过一些“GC Roots”对象作为起点,从这些节点开始往下搜索,搜索通过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots的引用链连接的时候,说明这个对象是不可用的。

GC Roots对象包括:

a) 虚拟机栈(栈帧中的本地变量表)中的引用的对象。

b) 方法区域中的类静态属性引用的对象。

c) 方法区域中常量引用的对象。

d) 本地方法栈中JNI(Native方法)的引用的对象。

生存还是死亡?

在根搜索算法中不可达的对象,也并非是非死不可的,要真正宣告一个对象死亡,至少要经过两次标记过程,如果对象在进行根搜索后发现没有与GC Roots相连的引用链,那它将被第一次标记并进行一次筛选,筛选条件是此对象是否有必要执行finalize方法,当对象没有覆盖finalize方法或该方法已经被虚拟机调用过一次了(finalize方法只能被JVM调用一次,用户可以手动调用多次,用户调用不会影响JVM的调用),则虚拟机认为没必要再执行finalize方法(直接回收)。如果虚拟机认为有必要执行finalize方法,则该对象会被放置在一个名为F-Queue的队列中,稍后会有一条虚拟机自动建立的低优先级的Finalizer线程来触发对象的finalize方法。finalize方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,第二次标记时,如果对象这时候还没有逃脱,那该对象就离死不远了。在finalize方法中,对象只要重新与引用链上的任何一个对象建立关联,譬如把自己(this)赋值给某个类变量或对象的成员变量,那么第二次标记时它将被移出“即将回收”的集合,但并不意味着该对象就永生了,只要根搜索不可达,则gc会再次(与第一次一样)对该对象筛选,发现finalize方法已经执行过了,故将其直接回收,所以,一个对象只能逃脱一次。如下面例子:

publicclass Test{

   publicstatic Test test;

   publicvoid isAlive(){

      System.out.println("I am alive!");

   }

   @Override

   protectedvoid finalize() throws Throwable{

      super.finalize();

      Test.test = this;

   }

   publicstaticvoid main(String[]args) throws InterruptedException{

      Test.test = new Test();

      test = null;

      System.gc();

      Thread.sleep(1000);

      if(test!=null)

         test.isAlive();

      else

         System.out.println("对象已被回收");

      test = null;

      System.gc();

      Thread.sleep(1000);

      if(test!=null)

         test.isAlive();

      else

         System.out.println("对象已被回收");

   }

}

上面例子中有两段完全一样的代码,执行结果却是一次成功逃脱,一次失败,原因是finalize方法只会被系统自动调用一次。需要提醒的是,finalize方法运行代价比较高,不确定性大,无法保证各个对象的调用顺序,所以,finalize所能做的所有工作,使用try-finally可以做的更好,我们可以完全忘掉java中还有这个方法的存在。

了解了JVM是怎么确定对象是“垃圾”之后,让我们来看看垃圾回收的算法:

1.标记—清除算法(Mark-Sweep)

标记—清除算法包括两个阶段:“标记”和“清除”。在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。

标记—清除算法是基础的收集算法,标记和清除阶段的效率不高,而且清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。

垃圾回收前:


垃圾回收后:


绿色:存活对象 红色:可回收对象 白色:未使用空间

2.复制算法(Copying)

复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。

复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以两块内存的比例不是1:1(大概是8:1)。

垃圾回收前:


垃圾回收后:


绿色:存活对象 红色:可回收对象 白色:未使用空间

3.标记—整理算法(Mark-Compact)

标记—整理算法和标记—清除算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。

标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。

垃圾回收前:


垃圾回收后:


绿色:存活对象 红色:可回收对象 白色:未使用空间

4.分代收集(Generational Collection)

分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用标记—复制算法,老年代采用标记—整理算法。

垃圾算法的实现涉及大量的程序细节,而且不同的虚拟机平台实现的方法也各不相同。上面介绍的只不过是基本思想。

版权声明:本文为博主原创文章,未经博主允许不得转载。

你可能感兴趣的:(垃圾回收)