No.4 Java的内存回收(内存回收)

1. Java引用的种类

 内存管理分为:内存分配和内存回收。都是由JVM自动处理的

  • 对象在内存中的状态:可达、可恢复(回收前调用finalize方法)、不可达
    • JVM回收标准:是否还有引用变量引用该对象
    • 有向图理解。线程对象作为根节点,变量、对象作为节点,引用关系作为有向边。在有向图中,从线程节点<当然线程对象也要存在,没有被销毁>可达的对象都是可达状态。
  • 强引用
    • 一般的引用/大部分 都是强引用,被强引用的对象不会被回收,是内存泄露的主要原因之一。
  • 软引用
    • 通过SoftReference类实现(该类的用法)
    • 只有软引用的对象,当系统内存充足时不会被回收,当内存紧张时会被回收
    • 可以用来解决系统内存紧张的难题
  • 弱引用
    •  String str = "Java程序引用"; 方式定义的字符串直接量会被系统缓存(会使用强引用来引用),系统不会回收被缓存的字符串常量。 String str = new String("Java程序引用"); 方式则不会缓存

    • 当垃圾回收机制运行(具有不确定性)时,不管系统内存是否紧张,都会被回收(不确定性)。
    • WeakReference类,WeakHashMap更常用
  • 虚引用
    • 主要用于跟踪对象被垃圾回收的状态:程序可以通过检查与虚引用关联的引用队列中是否已经包含指定的虚引用,从而了解虚引用所引用的对象是否即将被回收。
    • 不能单独使用,需要和引用队列联合使用
      • 软引用和弱引用与引用队列联合使用,系统回收被引用对象之后,将会把被回收对象的对应的引用添加到关联的引用队列中去。
      • 而虚引用与引用队列联合使用的时候,在对象被释放之前,将把引用它的虚引用添加到引用队列中去,这使得可以在对象被回收之前采取行动。
    • 无法通过虚引用获取它所引用的对象

2. Java的内存泄露

  • 不在使用的内存却没有被回收,就是内存泄露
  • Java中,可达状态但是不再使用的内存,会引起内存泄露(垃圾回收机制不会回收可达状态的内存)
  • eg:ArrayList中的remove方法elementData[--size] = null;

3. 垃圾回收机制

  • 两件事情:回收不可达对象;清理内存分配、回收过程中产生的内存碎片(不连续的内存空间)
  • 垃圾回收的基本算法
    • 串行回收和并行回收
    • 并发执行和应用程序停止
    • 压缩、不压缩、复制
      • 复制、标记清楚、标记压缩
  • 堆内存的分代回收(堆内存被分为三个代来存放对象)
    • 依据:对象生存时间的长短,然后根据不同代采用不同的垃圾回收策略,充分发挥各自的优势。
      • 基于如下两点事实:①绝大多数对象不会被长时间引用,Young代就会被回收;②很老的对象和很新的对象之间很少存在相互引用的情况
    • Young代:复制算法;可达状态的对象较少,复制成本不大
      • 1个Eden区和2个Survivor区:对象先被分配到Eden区(一些大的对象可能直接分配到Old代),Survivor区中的对象至少经历一次垃圾回收。一次复制算法会将Eden区和1个Survivor区中的可达对象复制到另一个空的Survivor区中,然后进入下一个循环,即两个Survivor区同时间有一个是用来保存对象,另一个是空的
    • Old代
      • Young代多次回收仍然存在的对象会进入到Old代中,其中对象的特点:不容易死;随时间的流逝,其中的对象会越来越多,因此Old代的空间成本要比Young代大
      • 回收特点:垃圾回收的执行频率无需太高,因为死去的对象较少;每次执行需要花费更多的时间完成
      • 标记压缩算法,不会大量地产生内存碎片
    • Permanent代
      • 主要用于装载Class、方法等信息,垃圾回收机制通常不会回收该代中的对象
    • 次要回收:当Young代的内存将要用完的时候,垃圾回收机制会对该代进行垃圾回收,回收频率较高,系统开销小。
    • 主要回收:当Old代的内存将要用完的时候,垃圾回收机制会进行全回收,即对Young代和Old代都要进行回收,此时回收成本较大,因此成为主要回收
    • 通常来说,Young代先被回收,Old代的回收频率要低的多;内存压缩时,每个代都独立的进行压缩
  • 常见垃圾回收器
    • 串行回收器
      • Young代:串行复制算法
      • Old代:串行标记压缩算法,三个阶段:mark、sweep、compact(压缩阶段,执行sliding compaction,将活动对象往Old代的前端移动,尾部保留连续的空间)
    • 并行回收器
      • Young:与串行回收器基本相似,增加多CPU并行的能力,即启动了多个线程来并行回收(并不是与主线程并发,而是多个回收线程并行执行
      • Old:与串行完全相同
    • 并行压缩回收器(将取代并行回收器)
      • Young:与并行完全相同
      • Old:将Old代分成几个固定大小的区域
        • mark:多个回收线程并行标记可达对象(会更新可达对象所在区域的大小及其位置信息)
        • summary:操作Old代区域(而不是单个对象)。从最左边检验区域密度,当某区域的密度达到某个数值时,判定该区域及其右边区域应该回收(进行压缩及回收空间),其左边区域标识为密集区域(不会将新对象移到这里,也不会对该区域进行压缩)。该阶段:串行实现
        • compact:利用上阶段生成的数据识别出需要装填的区域,多个回收线程并行地将数据复制到该区域中。该阶段后,Old代一端密集存储大量的活动对象,另一端则为大块的空闲块。
    • 并发标识-清理 回收器(CMS),适用于实时性要求较高的程序,对Old代回收:并发执行,程序仅仅需要两次很短的暂停
      • Young:与并行回收器完全相同,依然会导致应用程序暂停,
      • Old:并发操作
        • mark:
          • 垃圾回收开始时,短暂的暂停,标识直接引用的可达对象(initial mark);
          • 并发标识阶段(concurrent marking phase),依据初始标识中发现的可达对象来寻找其他可达对象;
          • 再标记阶段(remark),因为并发标识过程中 应用程序可能会重新产生可达对象,为避免漏掉这些,需要再次很短的暂停下,多线程并行地再标识
        • concurrent sweep:并发清理
      • 默认在Old代68%满时就开始回收,因为是并行操作的,如果等到Old代满了,应用程序就没有可用内存了;而其他回收器回收时,不是并行操作的,在回收时,程序会暂停,应用程序不产生新的对象。
      • CMS不会对Old代进行内存压缩,即它的可用空间是不连续的,保存了一份可用空间列表,分配内存效率下降。
      • CMS需要更大的堆内存,因为:并发标识时,此时应用程序也在分配对象,Old代会同时增长;在并发清理阶段新成为垃圾的对象并不能立即被回收,只有等到下次垃圾回收阶段时才被回收。<浮动垃圾>(mine:串行,mark之后执行sweep,之间程序暂停,不会再产生新的对象;并发,mark(含有并发mark)之后并发sweep,程序并发执行,仍然可能产生新的对象,待清理的对象和新产生的对象在一起自然要大一点的内存。)(总之,因为并行,可能在清理的时候又产生新的对象,所以需要更大点的内存;而串行时,清理的时候不会产生新的对象,清理完毕后才会产生新的对象,需要的内存没有这么大)
      • 可执行附加选项强制回收permanent代的内存
      • 串行回收和并发回收对比(mine)
        • 串行:应用程序暂停——>回收器执行回收阶段(mark——>....——>sweep...)——>程序继续执行
        • 并发:应用程序短暂暂停1——>CMS执行initial mark(标记直接引用的可达对象)——>程序执行,CMS并发标记concurrent mark(标记通过初始标识标识的可达对象能寻找到的其他可达对象)——>程序短暂暂停2——>remark(标记并发标记时程序新产生的可达对象)——>程序继续执行,并发清理<此时才真正开始清理操作,之前都是标识操作>

4. 内存管理的小技巧

  • 尽量使用直接量
    • 使用字符串以及Byte、Short、Interger...等包装类时,直接使用直接量创建,而不用new 关键字new新的对象
  • 进行字符串拼接时使用StringBuilder/StringBuffer,而不是String(String不可变,用其拼接时会产生大量的临时字符串)
  • 尽早释放无用对象的引用
    • 一般情况下局部变量的作用时间比较段,对其引用值无需 = null;但是若是其所在的方法中执行了耗内存、费时的操作时,应该将对无用对象的引用变量的值赋值null,尽早释放无用对象
  • 尽量少用静态变量,尤其是静态变量引用对象。因为静态变量属于Class类对象,类对象在permanent代中(它的类变量自然也在该代中),其存在时间很长
  • 避免在经常调用的方法、循环中创建对象。
    • 导致不断的为对象分配(创建)、回收(销毁)内存,这些操作都是影响性能的
  • 缓存经常使用的对象(eg:数据库连接池)
    • 避免不断的分配、回收操作
    • 方法:
      • 使用HashMap:控制容器中的key-value对不能太多,否则HashMap占用过大的内存将导致性能下降
      • 使用开源缓存项目
    • 缓存设计:
      • 牺牲空间来换取时间,都是使用容器保存已用过的对象。如何控制容器占用的内存,又保留大部分已用过的对象,是程序设计的关键。(一些缓存算法)
  • 尽量不使用finalize方法
    • 垃圾回收器回收资源前会调用该方法。
    • 回收机制本身已经教严重制约应用程序的性能
    • 本身回收机制的负担就比较重,而且回收Young代内存会导致程序暂停,影响性能;再在finalize方法中执行资源清理会加重回收机制的负担,导致运行效率更差
  • 考虑使用SoftReference
    • 创建长度很大的数组时,考虑使用软引用进行包装数组元素(内存够时引用,不足时释放)
    • 软引用具有不确定性,当获取引用对象时,需要显示判断对象是否为空(避免出现异常),若为空,应该重新创建。
  • 吼吼吼
PS:不足之处,欢迎指正、交流
原文地址:https://www.cnblogs.com/fang--/p/6187726.html