java 虚拟机(jvm)垃圾回收(GC)算法

  • GC中的垃圾特指于内存中不会再使用的对象,垃圾回收有很多算法:

    • 引用计数法
    • 标记压缩法
    • 复制算法
    • 分代,分区的思想
  • 引用计数法

    • 古老而经典,但是有一些严重的问题
    • 核心是当对象被引用的时计数器加1,引用失效时减1
    • 问题:
      • 无法处理循环引用的情况
      • 每次进行加减操作比较浪费系统性能
  • 标记清除法

    • 分为标记和清除俩个阶段进行处理内存中的对象
    • 弊端:
      • 导致空间碎片问题,垃圾回收后的空间不是连续的,不连续的内存空间的工作小徐要低于连续的空间
  • 复制算法*

    • 核心思想是将内存空间分为俩块,每次只使用其中一块,垃圾回收时,将正在使用的内存中的存留对象复制到未被使用的内存块中去,之后去清除之前正在使用的内存块中的所有对象,反复去交换俩个内存的角色,完成垃圾收集。
    • java新生代中的from和to空间就是使用这个算法
  • 标记压缩法*

    • 标记压缩在标记清除法之上做了优化,把存活的对象压缩到内存一端(避免空间碎片问题),而后进行垃圾清理
    • java老年代使用的是标记压缩算法
  • 为啥新生代和老年代使用不同的算法?

    • 年轻代对象因为生命周期短,每次有约90%以上对象的占用空间被回收,采用“复制-清除”算法清理,具体过程:
      将新生代分为一个Eden空间和两个Survivor空间,默认Eden空间和Survivor空间的比例为8:1,对象分配到Eden和其中一个Survivor空间,回收时将存活的对象复制到另一个Survivor空间,然后将Eden空间和先前使用的Survivor空间清理。
    • 老年代因对象生命周期较长,每次回收只有少部分对象没清理,如果使用“复制-清理”算法的话需要额外预留更多的空闲空间用于复制生存对象,( 例如,100M的老年代占用空间 每次能回收50% ,那么他需要预留50M的空间 内存使用上不经济 )所以回收时使用“标记-整理”算法。
  • 分代算法

    • 呵呵,看上一个问题:为啥新生代和老年代使用不同的算法?
  • 分区算法

    • 被oracle收购后,提出的新算法,应用还不广泛,还在摸索,G1使用的就是这个算法
    • 将整个内存分为N多个小的独立空间,每个小空间可以独立使用,这样细粒度的控制一次回收多少个小空间和那些小空间,而不是对整个空间进行GC,进而提升性能,减少GC的停顿时间
  • GC的停顿时间

    • 为了高效的让垃圾回收器可以高效的执行,大部分情况下,会要求系统进入一个停顿的状态,停顿的目的是终止所有应用线程,只有这样系统才不会有新的垃圾产生,同时停顿保证了系统状态咋某一瞬间的一致性,也有益于更好的标记垃圾对象,因此在垃圾回收时,都会产生应用程序的停顿
  • 对象如何进入老年代

    • 对象首次创建时会被放在新生代的eden区,如果没有gc介入,则对象不会离开eden区,一般来讲,只要对象的年龄达到一定的大小,就会自动离开年轻代,进入老年代,对象的年龄是由对象经历gc的次数决定的,在新生代如果对象没有被回收则年龄加一,虚拟机提供了一个参数来控制新生代对象的最大年龄,当超过这个年龄范围就会晋升到老年代,这个参数就是 -XX:MaxTenuringThreshold 默认情况下为15
    • 另外,大对象,即新生代eden区无法装入时,也会直接进入老年代,jvm有个参数可以设置对象的大小超过指定的大小后,直接晋升老年代-XX:PretenureSizeThreshold,但是要注意TLAB区域有限分配空间,虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此会失去在老年代分配的机会
  • 关于TLAB区

    import java.util.HashMap;
    import java.util.Map;
    
    public class Test{
    
        public static void main(String[] args) {
          
            //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000
            //这种现象原因为:虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会
            //参数:-Xmx30M -Xms30M -XX:+UseSerialGC -XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000 -XX:-UseTLAB
            Map<Integer, byte[]> m = new HashMap<Integer, byte[]>();
            for(int i=0; i< 5*1024; i++){
                byte[] b = new byte[1024];
                m.put(i, b);
            }
        }
    }
    

    以上程序启动(注意加上jvm参数)之后你会看到老年代的使用率几乎为0,这种现象原因为:虚拟机对于体积不大的对象 会优先把数据分配到TLAB区域中,因此就失去了在老年代分配的机会,java的每个线程都是默认使用TLAB区的,如果要禁用,则加上jvm参数-XX:-UseTLAB,会看到老年代使用的内存大致为5M

    • TLAB区的全称是Thread Local Allocation Buffer,线程本地分配缓存,线程专用的内存分配区域,为了加速对象分配而生的,每个线程都有一个TLAB,来避免多线程冲突的问题,提高对象分配的效率,TLAB空间一般不会太大,当大对象无法再TLAB分配时,则会直接分配到堆上
    • -XX:+UseTLAB 使用TLAB
    • -XX:+TLABSize设置TLAB大小
    • -XX:TLABRefillWasteFraction 设置维护进入TLAB空间的单个对象大小,他是一个比例值,默认是64,即如果对象大于整个空间的1/64,则在堆创建对象
    • -XX:+PrintTLAB查看TLAB信息,要与-XX:DoEscape Analysis 配合使用,禁用逃逸分析参数
    • -XX:ResizeTLAB自动调整TLABRefillWasteFraction阈值
    • jdk1.7之后,TLAB区是自动调整的,不建议修改
  • 对象的创建流程

    • 尝试栈上分配,能分配则在栈上分配
    • 不能分配,尝试TLAB分配
    • 对象大小未超过TLAB阈值,分配到TLAB
    • 超过TLAB阈值,尝试在堆上分配,
    • 超过PretenureSizeThreshold(jvm参数,可以设置对象的大小超过指定的大小后,直接晋升老年代),分配到老年代
    • 否则在eden区分配
  • 垃圾收集器

    • 串行垃圾回收器
      使用单线程镜像垃圾回收的回收器,垃圾收集时只有一个工作线程,设置方式:-XX:+UseSerialGC,此时新生代和老年代都是使用串行垃圾回收器,对于并行能力较弱的计算机来说,串行的垃圾回收器会有更好的性能表现
    • 并行垃圾回收器
      • 使用多线程同时进行垃圾回收,对于并行能力较强的计算机而言,可以缩短垃圾回收所需的实际时间
      • ParNew回收器是一个工作在新生代的垃圾收集器,他只是简单的将串行回收器多线程化,他的回收策略和算法与串行回收器一样
        • 使用 -XX:+UseParNewGC,新生代使用ParNew回收器,老年代还是使用串行回收器
        • -XX:ParallelGCThreads可以指定工作线程数,一般和计算机的cpu个数相当,避免过多的线程影响性能
      • *新生代ParallelGC回收器,使用了复制算法的收集器,也是多线程独占形式的收集器,但ParallelGC回收器有个非常重要的特点--非常关注系统的吞吐量,提供了俩个非常关键的参数控制系统的吞吐量
        • -XX:MaxGCPauseMillis设置最大垃圾收集停顿时间,虚拟机会把GC的停顿时间控制在这个值以内,但是该值设置过小,会导致GC频繁,从而增加了GC的总时间.
        • -XX:GCTimeRatio设置吞吐量大小,他是个0到100之间的数,默认99,那么系统将花费不超过1/(1+n)的时间用于垃圾回收,也就是1/(1+99),即1%的时间
        • 另外还可指定-XX:+UseAdaptiveSizePolicy,打开自适应模式,这种模式下,新生代的大小,eden,from/to区的比例,以及晋升老年代的对象年龄参数会被自动调整,以达到在堆大小,吞吐量和停顿时间之间的平衡点
      • *老年代ParallelOldGC,多线程回收器,也是一种关注吞吐量的回收器,使用了标记压缩法进行实现
        • -XX:+UseParallelOldGC进行设置使用
        • -XX:+ParallelGCThreads设置垃圾收集时线程数量
    • CMS回收器(主流)
      • 全称为Concurent Mark Sweep ,并发标记清除,使用标记清除法,主要关注系统停顿时间
      • 使用-XX:+UseConcMarkSweepGC进行设置使用
      • 使用-XX:ConcGCThreads设置并发线程数量
      • CMS并不是独占的回收器,在CMS回收的过程中,应用程序仍然在不停的工作,同时会有新的垃圾产生,所以在使用CMS的时候应确保应用程序的内存足够可用,CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阈值的时候开始回收,可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认是68,即当老年代的空间使用率达到68%的时候,会执行CMS回收,如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器进行垃圾回收,这会导致应用程序中断,知道垃圾回收完成后才会正常工作,这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingOccupancyFraction参数的设置要根据实际的情况
      • 标记清除法有个缺点是产生空间碎片,CMS有个参数:-XX:+UseCMSCompactAtFullCollection可以使CMS回收完成之后进行一次碎片整理,-XX:CMSFullGCsBeforeCompaction可以设置进行多少次CMS回收后,对内存进行一次压缩
    • G1回收器(发展中)
      • Garbage-First是jdk1.7中提出的垃圾回收器,从长期目标来说是为了取代CMS回收器,有独特的垃圾回收策略,G1属于分代垃圾回收器,区分新生代和老年代,依然有eden和from/to区,它并不要求整个eden区或者新生代,老年代的空间都连续,它使用了分区算法
      • 并行性:G1回收期间可以多线程同时工作
      • 并发性:G1拥有与应用程序交替执行能力,部分工作可以与应用程序同时执行,在整个gc期间不会完全阻塞应用程序
      • 兼顾新生代和老年代一起工作,之前的垃圾收集器他们或者在新生代或者老年代工作,这是一个很大的不同,
      • 空间整理:G1在垃圾回收过程中,不会像CMS那样若干次GC之后需要进行碎片整理,G1采用了有效复制对象的方式,减少空间碎片
      • 可预见性:由于分区的原因,G1可以只选取部分区域进行回收,缩小了回收的范围,提升了性能
      • 使用-XX:+UseG1GC 配置使用
      • 使用-XX:MaxGCPauseMillis指定最大停顿时间
      • 使用-XX:ParallelGCThreads设置并行回收的线程数量
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容