• JVM 内存分配和垃圾回收(GC)机制


    一  判断对象是否存活

    垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“活着”,哪些已经"死去”,即不能再被任何途径使用的对象。

    1.1 引用计数法 (Reference Counting

    给对象加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效的时候,计数器值减1;任何时刻计数器为0的对象就是不可能再被使用的。

    引用计数法的实现简单,判断效率也很高,但是主流的java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间的循环引用的问题。

    举个例子:

    对象objA和对象objB都有字段instance,赋值令objA.instance = objB 以及objB.instance = objA,除此之外,这两个对象再无任何引用,但是他们互相引用着对方,导致它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收他们。

     1.2 可达性分析算法(根搜索算法 GC Roots Tracing

    在主流的商用程序语言中的主流实现中,都是通过可达性分析来判定对象是否存活的。

    这个算法的基本思想是通过一系列称为“GC Roots”的对象作为起始点,如果从GC roots到这个对象不可达,即一个对象到GC Roots 没有任何引用链相连,则证明此对象是不可用的。

    在java语言中,可作为GC Roots 的对象包括以下几种:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
    • 本地方法栈中JNI(Java Native Interface 一般说的native方法)引用的对象
    • 方法区中常量引用的对象
    • 方法区中类静态属性引用的对象

     1.3 引用

    java中将引用分为强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。

    强引用:

      强引用是指程序代码之中普遍存在的,类似"Object obj = new Object()"这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

    软引用:

      软引用是用来描述一些还有用但非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。

    弱引用:

      被弱引用关联的对象只能生存到下一次垃圾收集发生之前。

    虚引用:

      为一个对象设置虚引用的目的是能在这个对象被垃圾收集机制回收时收到一个系统通知。

    二 垃圾收集算法

    2.1 标记 - 清除算法(Mark-Sweep

      先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
      缺点:1 回收了被标记的对象后,由于未经过整理,所以导致很多内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集机制。
                      2 效率问题,标记和清除两个过程效率都不高 。

          图解:绿色是被标记为可回收的,当回收后,未使用的内存空间非常零碎,产生内存碎片

    2.2 复制算法(Copying

      将可用的内存按容量划分为大小相等的两块(from,to),每次只是用其中一块(总有一块是空的【to区域】)。当这块的内存用完了,就将还存活着的对象复制到另外一块上面,然后把已使用过的内存空间一次清理完。  
      一般不需要按照1:1的比例来划分内存空间,而是将一块内存分为一块较大的Eden空间和两块较小的Survivor空间(From Survivor 和 To Survivor),每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。
      HotSpot虚拟机默认Eden和Survivor大小的比例是8:1,也就是每次新生代中可用的内存空间为整个新生代容量的90%,只有10%的内存时被浪费的。
      缺点:浪费内存空间,如果对象存活率较高时要执行较多的复制操作,效率降低。
           优点:不产生内存碎片
     
        图解:有一块内存区域是空的,一般是to区域。保留区域每次回收后都因为复制的时候让他们变为连续的地址空间,所有不产生内存碎片。
     

    2.3 标记-整理算法(Mark-Compact

      复制收集算法在对象存活率较高的时候就要进行较多的复制操作,效率将会变低。
      标记-整理算法的“标记”过程和标记-清除算法一致,只是后面并不是直接对可回收对象进行整理,而是让所有存活的对象都向一段移动,然后直接清理掉端边界意外的内存。
     
        图解:由于标记后继续整理,可以很明显的看出未使用的地址空间都是连续的,不会产生内存碎片。
     

    2.4 分代收集算法(Generational Collection

      当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分为新生代和老年代。
      在新生代,每次垃圾收集时都会发现有大批对象死去,只有少量存活,那就使用 复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
      而老年代中因为对象存活率较高,没有额外空间进行分配担保,就必须使用 标记 - 清除  或者 标记 - 整理 算法来进行回收。
     

    补充:分代划分内存介绍

        整个JVM内存总共划分为三代:年轻代(Young Generation)、年老代(Old Generation)、(JDK 1.7 没了)持久代(Permanent Generation)

        1、年轻代:所有新生成的对象首先都放在年轻代内存中。年轻代的目标就是尽可能快速的手机掉那些生命周期短的对象。年轻代内存分为一块较大的Eden空间和两块较小的Survior空间,每次使用Eden和其中的一块Survior.当回收时,将Eden和Survior中还存活的对象一次性拷贝到另外一块Survior空间上,最后清理Eden和刚才用过的Survior空间。

        2、年老代:在年轻代经历了N次GC后,仍然存活的对象,就会被放在老年代中。因此可以认为老年代存放的都是一些生命周期较长的对象。

        3、持久代:基本固定不变,用于存放静态文件,例如Java类和方法。持久代对GC没有显著的影响。持久代可以通过-XX:MaxPermSize=<N>进行设置。

    三  内存分配和回收策略

    3.1 Jvm怎么判断对象可以回收了?

      对象没有引用

      作用域发生未捕获异常

      程序在作用域正常执行完毕

       程序执行了System.exit()

      程序发生意外终止(被杀线程等)

    在Java程序中不能显式的分配和注销缓存,因为这些事情JVM都帮我们做了,那就是GC。

    有些时候我们可以将相关的对象设置成null 来试图显示的清除缓存,但是并不是设置为null 就会一定被标记为可回收,有可能会发生逃逸。

    将对象设置成null 至少没有什么坏处,但是使用System.gc() 便不可取了,使用System.gc() 时候并不是马上执行GC操作,而是会等待一段时间,甚至不执行,而且System.gc() 如果被执行,会触发Full GC ,这非常影响性能。

    3.2 JVM GC什么时候执行?

    eden区空间不够存放新对象的时候,执行Minor GC。升到老年代的对象大于老年代剩余空间的时候执行Full GC,或者小于的时候被HandlePromotionFailure 参数强制Full GC 。调优主要是减少 Full GC 的触发次数,可以通过 NewRatio 控制新生代转老年代的比例,通过MaxTenuringThreshold 设置对象进入老年代的年龄阀值(后面会介绍到)。

    3.3 JVM分别对新生代和老年代采用不同的垃圾回收机制

    在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young )又被划分为

    三个区域:Eden、From Survivor、To Survivor。

       这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。

       堆的内存模型大致为:

         

        从图中可以看出: 堆大小 =新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。

     

    新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里,由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。对象从这个区域“消失”的过程我们称之为:Minor GC 。

    老年代(Old generation):对象没有变得不可达,并且从新生代周期中存活了下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的空间,发生在老年代的GC次数要比新生代少得多。对象从老年代中消失的过程,称之为:Major GC 或者 Full GC。

    (JDK7后 没了)持久代(Permanent generation)也称之为 方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件非常严苛,必须符合以下三种条件才会被回收:

    1、所有实例被回收

    2、加载该类的ClassLoader 被回收

    3、Class 对象无法通过任何途径访问(包括反射)

    可能我们会有疑问:

    如果老年代的对象需要引用新生代的对象,会发生什么呢?

    为了解决这个问题,老年代中存在一个 card table ,它是一个512byte大小的块。所有老年代的对象指向新生代对象的引用都会被记录在这个表中。当针对新生代执行GC的时候,只需要查询 card table 来决定是否可以被回收,而不用查询整个老年代。这个 card table 由一个write barrier 来管理。write barrier给GC带来了很大的性能提升,虽然由此可能带来一些开销,但完全是值得的。

    新生代空间的构成与逻辑

    为了更好的理解GC,我们来学习新生代的构成,它用来保存那些第一次被创建的对象,它被分成三个空间:

    · 一个伊甸园空间(Eden)

    · 两个幸存者空间(Fron Survivor、To Survivor)

    默认新生代空间的分配:Eden : Fron : To = 8 : 1 : 1

    老年代空间的构成与逻辑

    老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这里的对象几乎都是从Survivor 空间中熬过来的,它们绝不会轻易的狗带。因此,Full GC(Major GC)发生的次数不会有Minor GC 那么频繁,并且做一次Major GC 的时间比Minor GC 要更长(约10倍)。

    Java 中的堆也是 GC收集垃圾的主要区域。GC 分为两种:Minor GC、FullGC ( 或称为 Major GC )。

    Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。当一个对象被判定为 "死亡" 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域。当对象在 Eden ( 包括一个 Survivor 区域,这里假设是 from 区域 ) 出生后,在经过一次 Minor GC后,如果对象还存活,并且能够被另外一块 Survivor 区域所容纳(上面已经假设为 from 区域,这里应为 to 区域,即 to 区域有足够的内存空间来存储 Eden 和 from 区域中存活的对象 ),则使用复制算法将这些仍然还存活的对象复制到另外一块 Survivor 区域 ( 即 to 区域 ) 中,然后清理所使用过的 Eden以及 Survivor 区域 ( 即from 区域 ),并且将这些对象的年龄设置为1,以后对象在 Survivor 区每熬过一次 Minor GC,就将对象的年龄 + 1,当对象的年龄达到某个值时 ( 默认是 15 岁,可以通过参数 -XX:MaxTenuringThreshold 来设定),这些对象就会成为老年代。但这也不是一定的,对于一些较大的对象 (即需要分配一块较大的连续内存空间 ) 则是直接进入到老年代

     

    Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。现实的生活中,老年代的人通常会比新生代的人"早死"。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 "死掉" 了的。因此,Full GC发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。

     

    另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 (即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。

    3.4 JVM参数选项

        下面只列举其中的几个常用和容易掌握的配置选项

    -Xms

    初始堆大小。如:-Xms256m

    -Xmx

    最大堆大小。如:-Xmx512m

    -Xmn

    新生代大小。通常为 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 个 Survivor 空间。实际可用空间为 = Eden + 1 个 Survivor,即 90% 

    -Xss

    JDK1.5+ 每个线程堆栈大小为 1M,一般来说如果栈不是很深的话, 1M 是绝对够用了的。

    -XX:NewRatio

    新生代与老年代的比例,如 –XX:NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3

    -XX:SurvivorRatio

    新生代中 Eden 与 Survivor 的比值。默认值为 8。即 Eden 占新生代空间的 8/10,另外两个 Survivor 各占 1/10 

    -XX:PermSize

    永久代(方法区)的初始大小

    -XX:MaxPermSize

    永久代(方法区)的最大值

    -XX:+PrintGCDetails

    打印 GC 信息

    3.5 jvm调优问题--full gc太过频繁该如何处理?

    1 full gc频繁说明old区很快满了。

    2 如果是一次full gc后,剩余对象不多。那么说明你eden区设置太小,导致短生命周期的对象进入了old区。

    3 如果一次full gc后,old区回收率不大,那么说明old区太小。

    参考

    《深入理解java虚拟机》

    http://hllvm.group.iteye.com/group/topic/38223#post-248757

    http://www.iteye.com/topic/1119491

    http://www.importnew.com/1993.html

  • 相关阅读:
    bzoj3302
    bzoj1264
    听风
    bzoj5073
    bzoj2144
    bzoj1263
    bzoj3653
    Docker 入门 2 镜像基本操作
    Docker 入门 1 准备 Docker 环境
    Docker Hub 镜像加速
  • 原文地址:https://www.cnblogs.com/chengdabelief/p/7466876.html
Copyright © 2020-2023  润新知