如何深入理解Java内存回收机制? - Go语言中文社区

如何深入理解Java内存回收机制?


  Java作为一门优秀的编程语言,有着很多优点。其中,就有内存自动管理这一项。Java提供了对内存的自动管理,程序员无需在程序中进行分配、释放内存,不会再被那些可怕的内存分配错误打扰。
  但是,Java对于内存的自动管理并不是万能的,Java依然会存在内存泄漏的情况。让我们来举个栗子,这个栗子在平时十分的常见。

//我们要采用基于数组的方式实现一个Stack
public class Stack
{
    //存放栈内元素的数组
    private Object[] elements;
    //记录栈内元素的个数
    private int size=0;
    public Stack(int initialCapacity)
    {
        elements=new Object[initialCapacity];
    }
    //向“栈”顶压入一个元素
    public void Push(Object obj)
    {
        ensureCapacity();
        elements[size++]=obj;
    }
    //从栈中弹出一个元素
    public Object pop()
    {
        if(size==0)
        {
            throw new RuntimeException("空栈异常");
        }
        return elements[--size];
    }

    //保证底层数组能容纳栈内所有元素
    public void ensureCapacity()
    {
        //增加堆栈容量
        if(elements.length==size)
        {
            Object[] oldElements=elements;
            int newLength=0;
            newLength=(int)(elements.length*1.5);
            elements=new Object[newLength];
            //将原数组复制进新数组
            System.arraycopy(oldElements,0,elements,0,size);
        }
    }

    public static void main(String[] args)
    {
        Stack stack=new Stack(10);
        //向栈顶压入10个元素
        for(int i=0;i<10;i++)
        {
            stack.Push(i);
        }
        //依次弹出10个元素
        for(i=0;i<10;i++)
        {
            System.out.println(stack.pop());
        }
    }
}

  上面的程序看似正常,其实底层却出现着严重的内存泄漏。问题就出现在pop函数。当pop函数运行一次,Stack会记录该栈的尺寸减1,但并未清除elements数组最后一个元素的引用,因此,垃圾回收机制不会回收这些数组对象的内存,而这些对象并不会被再用到(栈已经默认没有改对象所在的索引了),这就造成了内存泄漏。

  由于Java向我们许下了垃圾回收承诺,导致了Java的内存泄漏更隐蔽。一个内存泄漏点导致的内存泄漏可能并不多,但并发用户一多,运行时间一长,内存泄漏就会变得相当可怕……掌握Java内存回收机制就显得十分之重要了。
  
  下面我们以三个问题来讲解Java内存回收机制
  1、回收什么?
  2、何时回收?
  3、怎么回收?

1、回收什么?

  在了解回收机制回收什么之前,我们要了解一下Java对象在内存中的三种状态。分别为:可达状态,可恢复状态,不可达状态。下图显示三种对象之间的转换关系:
  这里写图片描述
  
  垃圾回收机制回收没有被引用变量引用的对象。
  其实,当Java对象被创建出来后,垃圾回收机制会实时监控每一个对象的运行状态,如:对象申请,引用,被引用,赋值等。当垃圾回收机制监控到某个对象不再被引用变量引用时,垃圾回收机制就会回收它所占用的空间。
  所以!在我们不再需要使用一个对象时,要记得取消对该对象的所有引用,这样,垃圾回收机制才能自动回收它,否则就会造成内存泄漏。
  
  

2、何时回收?

  anytime:
  回收时间大致有如下两种情况:
  
  1)当应用程序空闲时,即没有应用线程在运行时,垃圾回收机制会被调用。因为垃圾回收机制在优先级最低的线程中进行,所以当应用忙时,垃圾回收线程就不会被调用,但以下条件除外。

  2)Java堆内存不足时,垃圾回收机制会被调用。当应用线程在运行,并在运行过程中创建新对象,若这时内存空间不足,JVM就会强制地调用垃圾回收机制以便回收内存用于新的分配。若GC(Garbage Collector)一次之后仍不能满足内存分配的要求,JVM会再进行两次GC作进一步的尝试,若仍无法满足要求,则 JVM将报“out of memory”的错误,Java应用将停止。

  要注意垃圾收集发生的不可预知性:由于实现了不同的垃圾回收算法和采用了不同的收集机制,所以它有可能是定时发生,有可能是当出现系统空闲CPU资源时发生,也有可能是和原始的垃圾收集一样,等到内存消耗出现极限时发生,这与垃圾收集器的选择和具体的设置都有关系。不要试图去假定垃圾收集发生的时间,这一切都是未知的。比如,方法中的一个临时对象在方法调用完毕后就变成了无用对象,这个时候它的内存就可以被释放。
  
  由于是否进行主GC由JVM根据系统环境决定,而系统环境在不断的变化当中,所以主GC的运行具有不确定性,无法预计它何时必然出现,但可以确定的是对一个长期运行的应用来说,其主GC是反复进行的。

3、怎么回收?

  垃圾回收机制主要完成下面的两件事情:

  1、跟踪并监控每个对象,当对象处于不可达状态时,回收该对象所占用的内存
  2、清理内存分配、回收过程中产生的内存碎片

  垃圾回收机制需要完成这两方面的工作,这两方面的工作量都不小,因此垃圾回收算法就成为了Java程序运行效率的重要因素。高效的垃圾回收机制既能保证垃圾回收的快速运行,避免内存的分配和回收成为应用程序的性能瓶颈,又不能导致应用程序产生停顿。因此,我们就需要弄清楚垃圾回收算法。

 1) 垃圾回收算法

  A.Copying(复制)算法(适用于回收新生代)

  将堆内分成两个相同空间,从根(从下往上溯源到类或实例的最大父类对象)开始访问每一个关联的可达对象,将空间A的可达对象全部复制到空间B,然后一次性回收整个空间A。

  这里写图片描述
  
  对于复制算法而言,只需访问所有可达对象,将所有的可达对象复制走后就可以回收整个空间,完全不用理会那些不可达的对象,所以遍历空间的成本较小,但却需要巨大的复制成本和内存。
  
  B、Mark-Sweep(标记-清除)算法

  是不压缩的回收方式,垃圾回收器先从根开始访问所有可达对象,将它们标记为可达状态,然后再一次遍历整个内存区域,将没有标记的对象进行回收处理。

  标记-清除算法
  
  无需进行大规模的复制操作,内存利用率高。但是这种算法需要遍历两次堆内存空间,遍历成本较大,因此造成程序暂停的时间随堆空间大小线性增大。而且垃圾回收回来的内存往往是不连续的,因此整理后对内存的碎片很多。

  C、Mark-Compact(标记-整理)算法(适用于回收老年代)

  是一种压缩的回收方式,这种方式充分利用上述两种方法的优点。即:先从根开始访问所有可达对象,将它们标记为可达状态。接下来垃圾回收器将这些对象搬迁在一起,这个过程为内存压缩,然后垃圾回收机制再次回收那些不可达对象所占用的内存空间,这样就避免了回收产生的内存碎片。


  不论采用哪种机制实现垃圾回收,不论采用哪种内存回收方式,具体实现起来总是利弊参半的。因此,实际实现垃圾回收机制时总会综合使用多种设计方式,也就是对不同情况采取不同的垃圾回收实现。

  现行的垃圾回收器用分代的方式来采用不同的回收设计。分代的基本思路是根据对象的生存时间,将堆内存分成3代:

  Young(新生代);
  Old(老年代);
  Permanent(永久代);

  垃圾回收器会根据不同代的特点采用不同的回收算法,从而充分利用各种回收算法的优点。
  
  D、分代回收算法
 
   新生代

  • 特点:大批对象死去,只有少量存活。
  • 算法:复制算法。

    老年代

  • 特点:对象存活率高,没有额外空间进行分配担保。
  • 算法:标记-清除算法/标记-整理算法。

2)垃圾回收器

  由于该内容比较少应用到,所以将简略带过。
A、串行回收器(只使用一个CPU)
  垃圾回收执行会使得应用程序产生暂停。策略为:Young代使用串行复制算法,Old代使用串行标记压缩算法。

B、并行回收器(充分利用多个CPU)
  
C、并行压缩回收器
  与B的不同只在于对Old代的回收上。
D、并发标识-清理回收器
  对Old代采用并发回收操作。
  
  下面,介绍一下内存管理的小技巧,掌握了这些技巧,我们的程序运行速度将会得到质的提升。

内存管理的小技巧

  为了提高Java程序的运行性能,根据前面介绍的内存回收机制,下面给出Java内存管理的几个小技巧

1、尽量使用直接量

   直接量就相当于在缓存池中存储了一个该对象,可重复使用

2、使用StringBuilder和StringBuffer进行字符串连接

   由于String是固定长的字符串对象,累加String对象时,并非在一个String对象中扩增,而是重新创建新的String对象,如Str5=Str1+Str2+Str3+Str4,这条语句执行过程中会产生多个垃圾对象,因为对次作“+”操作时都必须创建新的String对象,但这些过渡对象对系统来说是没有实际意义的,只会增加更多的垃圾。避免这种情况可以改用StringBuffer来累加字符串,因StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。

3、尽早释放无用对象的引用(置为null)

4、尽量少用静态对象变量

   静态变量属于全局变量,不会被GC回收,它们会一直占用内存。

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

6、缓存经常使用的对象

   可以考虑把常用的对象用缓存池保存起来。如数据库连接池。常见的缓存有两种方式:
   1、使用HashMap进行缓存(以牺牲空间换取时间的技术)
   2、直接使用某些开源的缓存项目

7、考虑使用SoftReference

   当程序需要创建长度很大的数组时,可以使用SoftReference来包装数组元素,而不是直接让数组元素来引用对象
   SoftReference:当内存足够时,功能等同于普通引用,当内存不够时,牺牲自己,释放软引用所引用的对象。
   注意点:要注意软引用的不确定性,在取出SofeReference所引用的Java对象之后需要显示判读该对象是否为null,如果是,则应重建该对象。

8、能用基本类型如Int,Long,就不用Integer,Long对象

   基本类型变量占用的内存资源比相应对象占用的少得多,如果没有必要,最好使用基本变量。

9、分散对象创建或删除的时间

   集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在面临这种情况时,只能进行主GC,以回收内存或整合内存碎片,从而增加主GC的频率。集中删除对象,道理也是一样的。它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主GC的机会。

10、不要显式调用System.gc()、finalize方法

   此函数建议JVM进行主GC,虽然只是建议而非一定,但很多情况下它会触发主GC,从而增加主GC的频率,也即增加了间歇性停顿的次数。

总结

  Java垃圾回收机制作为Java的核心高级功能,我们不仅要知道其强大与方便,更应该了解其内部的实现原理,这样,我们才能更好的开发高效快速的程序……
 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/lincolnGE/article/details/73985713
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-03-07 18:46:37
  • 阅读 ( 1301 )
  • 分类:Go深入理解

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢