• JVM-自动内存管理机制


    关于GC:

      垃圾收集通常被称为"GC",经过半个世纪的发展,内存动态分配与内存回收技术已经相当成熟。那我们为何还要了解GC和内存分配呢?

      当我们需要排除各种内存溢出、内存泄露问题时,当垃圾收集成为系统到达更高并发量的瓶颈时,我们需要对这项技术实施必要的监控和调节。

      垃圾收集器在对堆进行回收前,首要就是判断对象之中哪些存活哪些已经死去(不可能再被任何途径使用的对象)。如何判断对象状态呢,jvm提供了两种算法,分别是引用计数算法和可达性分析算法。下面分别对两种算法进行介绍:


     引用计数算法:

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

      该算法的缺陷在于:很难解决对象之间相互循环引用的问题。所以在主流Java虚拟机中并未采用该算法管理内存。

    可达性分析算法:

      从GC Roots开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots不可达时(即没有任何引用链相连),证明此对象是不可用的。如图所示:

      

      Java中,可作为GC Root的对象包括以下几种:

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

      2、方法区中类静态属性引用的对象。

      3、方法区中常量引用的对象。

      4、本地方法栈中JNI引用的对象。


     两种算法中判断对象是否存活都与“引用”有关,在JDK1.2后Java对于引用的概念进行了扩充,以便更合理的回收对象。其分为强引用(Strong Reference)、软引用(Soft Referenece)、弱引用(Weak Referenece)、虚引用(Phantom Referenece)。这4种引用强度依次逐渐减弱。

      1、强引用:普遍存在于程序中,类似Object obj = new Object()这类的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

      2、软引用:描述一些还有用但并非必需的对象。对于软引用关联的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收后,依旧没有足够的内存,才会抛出内存溢出异常。

    ·   3、弱引用:描述非必需对象。弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够都会被回收掉。

      4、虚引用:也称为幽灵/幻影引用,对一个对象设置虚引用关联的唯一目的是能在这个对象被收集器回收时收到一个系统通知。一个对象是否有虚引用的存在,完全不对其生存时间构成影响,也无法通过虚引用取得一个对象的实例。


     对象的自救:

      在可达性分析算法中不可达的对象,也不是绝对的死亡。它们更像是处于缓刑的阶段。一个对象真正的确认死亡至少要经历两次标记过程。

      当对象进行可达性分析后发现没有与GC Roots相连接的引用链,那它会被第一次标记并进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。判断不执行的依据是:当对象没有覆盖finalize()方法或finalize()方法已经被虚拟机执行过,那虚拟机将这两种情况视为没必要执行。如果一个对象被判断为有必要执行finalize()方法,那么其会被放置在一个叫F-Queue的队列中,并被一个虚拟机自动建立的低优先级Finzlizer线程去触发,为何不是执行,因为虚拟机不会等待它运行结束。为了避免一个对象在该方法中执行缓慢或死循环,导致队列中其他对象永久等待,造成整个内存回收系统崩溃。

      finalize()方法是对象逃脱死亡的最后机会,如果对象在该方法中重新与引用链上的任何一个对象建立关联就成功自救(将自己赋值给某个类变量或对象的成员变量)。注意:任何一个对象的finalize()方法最多只会被系统自动调用一次,如果对象面临下一次回收,该方法不会被再次执行。


     方法区的回收:

      方法区(或HotSpot虚拟机中的永久代),在Java虚拟机规范中可以不要求在该区域实现垃圾收集。在堆中,尤其是新生代,常规应用一次垃圾收集可以回收75%-95%的空间,而永久代的垃圾收集效率远低于此。永久代的垃圾收集主要回收:废弃常量和无用的类。

      废弃常量:没有任何String对象引用常量池中的某变量,也没有其他地方引用了这个字面量,如果此时发送垃圾回收,这个常量就会被系统清理出常量池。

      无用的类,同时满足一下三个条件的类:

        该类所有的实例都已经被回收,在Java堆中不存在该类的任何实例。

        加载该类的ClassLoader已经被回收

        该类对应的java.lang.Class对象没有在任何地方被引用,无法再任何地方通过反射访问该类的方法。


    垃圾收集算法:


     标记-清除算法:

      分为“标记“和”清除”两个阶段。首先标记出所有需要回收的对象,标记完成后统一回收所有被标记的对象。

      缺陷:

        1、效率,标记和清除两个过程效率都不高。

        2、空间,标记清除后会产生大量不连续的碎片,空间碎片太多会导致以后程序运行中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。


     复制算法:

      将内存按容量分为大小相等的两块,每次只使用其一。当一块内存用完,将还存活的对象复制到另外一块上。然后再把已使用过的内存空间一次清理掉。这样一次对整个半区进行回收,内存分配时就无需考虑内存碎片问题。移动堆顶指针,按顺序分配内存,运行更高效。

      新生代的内存回收:

        新生代采用复制算法,其中98%的对象都是“朝生夕死”,所以并不需要安装1:1的比例来分内存空间。新生代内存被分为一块较大的Eden(伊甸)区和两块较小的Survivor(幸存者)区。每次使用Eden和其中一块Survivor。当回收时,将Eden和刚才使用的Survivor中还活着的对象一次性的复制到另一块Survivor空间上,最后清理Eden和Survivor空间。我们没有办法保证每次回收存活下来的对象能够分配完全在Survivor空间上,当Survivor空间不够时,还需要依赖其他内存(老年代)进行分配担保。

      分配担保:如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。


     标记-整理算法:

      复制算法在对象存活较高时要进行较多复制操作,效率会降低。对于老年代可能存在所有对象100%存活的极端情况,所以老年代一般不直接用复制算法,而是标记整理算法。对象进行标记后,让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。


     分代收集算法:

      根据对象存活周期的不同将内存划分为几块,根据不同年代的特点采用不同的收集算法。新生代采用复制算法,老年代使用标记-清理或标记-整理来回收


     垃圾收集器:

      并发:用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集器程序运行在另一个CPU上

      并行:多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态


     Serial(串行)收集器:

      历史最悠久的,最稳定及效率最高(与其他收集器的单线程比)的单线程收集器,进行垃圾收集时必须暂停其他所有的工作线程(服务暂停)。新生代、老年代使用串行回收;新生代复制算法、老年代标记-压缩。

      参数控制:-XX:+UseSerialGC 串行收集器

    ParNew收集器:

      Serial收集器的多线程版本,包括Serial收集器可用的所有控制参数、收集算法、服务暂停、对象分配规则、回收策略都与Serial收集器完全一样。新生代并行、老年代串行;新生代复制算法、老年代标记-压缩。

      参数控制:

      -XX:+UseParNewGC ParNew收集器、-XX:ParallelGCThreads 限制线程数量

    Parallel Scavenge收集器:

      新生代收集器,使用复制算法也是并行的多线程收集器。CMS等垃圾收集器的关注点是尽可能缩短垃圾收集时用户线程的暂停时间,而该收集器,目的是达到一个可控制的吞吐量。新生代复制算法、老年代标记-压缩。

      参数控制:

        -XX:MaxGCPauseMillis 设置最大垃圾收集器停顿时间、-XX GCTimeRatio 设置吞吐量大小

      自适应调节策略:

        虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例

    Serial Old收集器:

      Serila收集器的老年版本,单线程收集器。新生代采用复制算法、老年代采用标记-整理算法。

    Parallel Old收集器:

      Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法。

      参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行


     CMS收集器:

      以获取最短回收停顿时间为目的的收集器,基于标记-清除算法实现。运作过程包括:

      1、初始化标记。2、并发标记。3、重新标记。4、并发清除

      其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew)

      优点: 并发收集、低停顿 
      缺点: 产生大量空间碎片、并发阶段会降低吞吐量

      参数控制:

      -XX:+UseConcMarkSweepGC 使用CMS收集器

      -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

      -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理

      -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)


     G1收集器:

      面向服务端应用的垃圾收集器。其特点:

        1、并发与并行:G1能充分利用多CPU、多核环境优势。使用多个CPU来缩短服务暂停时间。通过并发方式让Java程序继续执行

        2、分代收集:采用不同的方式处理新创建的对象和已经存活一段时间、熬过多次GC的旧对象以获取更好的收集效果

        3、空间整合:G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC

        4、可预测停顿:降低停顿时间时G1和CMS共同关注点,但G1能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了

      上面的垃圾收集器,收集范围是整个新生代或老年代。而G1使用时将整个Java堆划分成多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。

      G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。和CMS类似,G1收集器收集老年代对象会有短暂停顿。

      G1收集器的运作大致划分为以下几步:

        1、初始标记:标记一下GC Roots能直接关联到的对象,并修改TAMS的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,需要停顿线程,但耗时很短。

        2、并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。可与用户程序并发执行。

        3、最终标记:修正在并发标记期间因用户程序继续运作导致产生变动的部分标记记录。该阶段需要停顿线程,但是可并发执行。

        4、筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间来制定回收计划。


     常用收集器组合:

      


     GC触发的条件:

      1、程序调用System.gc时可以触发;

      2、系统自身来决定GC触发的时机。系统判断GC触发的依据:根据Eden区和From Space区的内存大小来决定。当内存大小不足时,则会启动GC线程并停止应用线程。

    GC操作的对象分为:

      通过可达性分析法无法搜索到的对象和可以搜索到的对象。对于搜索不到的方法进行标记。    


    内存分配与回收策略:

    对象优先在Eden分配:

      大多数情况下,对象在新生代的伊甸区分配,当该区没有足够空间分配时,虚拟机将发起一次Minor GC

      Minor GC:

      新生代(由 Eden and Survivor 组成)的垃圾收集叫做Minor GC。当发生时需要注意到:

        1、当 JVM 无法为一个新的对象分配空间时会触发 Minor GC,比如当 Eden 区满了。所以分配率越高,越频繁执行 Minor GC。

        2、内存池被填满的时候,其中的内容全部会被复制,指针会从0开始跟踪空闲内存。Eden 和 Survivor 区进行了标记和复制操作,取代了经典的标记、扫描、压缩、清理操作。所以 Eden 和 Survivor 区不存在内存碎片。写指针总是停留在所使用内存池的顶部。

        3、执行 Minor GC 操作时,不会影响到永久代。从永久代到年轻代的引用被当成 GC roots,从年轻代到永久代的引用在标记阶段被直接忽略掉。

        4、质疑常规的认知,所有的 Minor GC 都会触发“全世界的暂停(stop-the-world)”,停止应用程序的线程。对于大部分应用程序,停顿导致的延迟都是可以忽略不计的。其中的真相就 是,大部分 Eden 区中的对象都能被认为是垃圾,永远也不会被复制到 Survivor 区或者老年代空间。如果正好相反,Eden 区大部分新生对象不符合 GC 条件,Minor GC 执行时暂停的时间将会长很多。所以 Minor GC 的情况就相当清楚了——每次 Minor GC 会清理年轻代的内存。

      Major GC:是清理老年代、Full GC:是清理整个堆空间—包括年轻代和老年代。

      Minor GC与Full GC的区别:

        1、新生代GC(Minor GC):是发生在新生代垃圾收集的动作,Minor GC非常频繁,一般回收速度也快

        2、老年代GC(Major GC/Full GC):是发生在老年代的GC,出现了Major GC,经常会伴随至少一次Minor GC。Major GC速度比Minor GC 慢10倍以上。

    大对象直接进入老年代:

      需要大量连续内存空间的Java对象,如很长的字符串或数组。虚拟机提供参数:-XX:PretenureSizeThreshold。大于这个值的对象直接在老年代分配。

    长期存活的对象将进入老年代:

      虚拟机给每个对象定义一个对象年龄计数器。如果对象在Eden出生并经过一次Minor GC后仍存活,并能被Survior容纳,将被移至Survior。并设置对象年龄为1.对象在Survior每熬过一次Minoc GC,年龄就增1.当年龄到一定程度(默认15),就会被晋升到老年代。通过-XX:MaxTenuringThreshold设置对象晋升老年代的阈值。

    动态对象年龄判断:

      虚拟机并不是永远地要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果在Survivor中相同年龄所有对象大小的总和大于Survivor的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

    空间分配担保:

      1、在发生Minor GC之前,虚拟机会检测老年代最大可用的连续空间是否大于新生代所有对象空间,如果这个条件成立,那么Minor GC可以确保是安全的。

      2、如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。

      3、如果允许,那么继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于则尝试进行一次Minor GC,如果小于或设置不允许,则进行一次Full GC


    参考资料:

      《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》

      《纯洁的微信-JVM系列文章》:http://www.ityouknow.com/jvm.html

      由衷的感谢提供学习资料的前辈们!!!

  • 相关阅读:
    JQuery使用总结
    JS应用总结
    Base64数据转成Excel,并处理Excel的格式
    HTTP压缩
    谷歌开发工具解析
    .Net LIst排重
    MySql日志系统
    .Net生成PDF流
    Mysql MVCC
    JAVA期末综合课程设计
  • 原文地址:https://www.cnblogs.com/zhangbLearn/p/10053812.html
Copyright © 2020-2023  润新知