天天看点

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结

目录

1、自动内存管理

2、垃圾回收

2.1、回收什么?

2.2、何时回收?

2.2.1引用计数法

2.2.2 GC Roots:可达性分析算法

2.2.3 垃圾回收条件

2.2.4 回收时机

2.3、如何回收?

2.3.1、标记-复制

2.3.2、标记-清理

2.3.3、标记-整理

3、内存的分代思想

3.1、内存分代的问题思考

3.2、垃圾收集器

3.2.1、新生代垃圾收集器

3.2.2、老年代垃圾收集器

4、JVM常用配置参数

5、小结

前言     c++和java之间有一堵由内存动态分配和垃圾收集技术所围成的墙。墙外面的人想进去墙里面的人想出来。Java/JVM通过gc营造出了一种“无限内存”的假象,正式由于gc的存在,程序员才这么肆无忌惮。

1、自动内存管理

    JVM营造了一种“无限内存”的假象,JVM实现只要“尽力而为”就行,撑不住的时候就抛出OOM,至于使用什么GC算法,什么时候触发GC,这些都是实现细节,规范并不管。JVM实现唯一要保证的是“当前正在使用的对象不能被干掉了”。     JDK引入的 Epsilon GC(A No-Op Garbage Collector是一种极端的表现,根本不gc,应用尽管用,new到-Xmx用满了就OOM。一旦java的堆被耗尽,jvm就直接关闭。设计的目的是提供一个完全消极的GC实现,分配有限的内存分配,最大限度降低服务垃圾收集延迟时间来提高吞吐量。     进过技术不断的发展,内存的动态分配与内存回收技术已经相当成熟,一切看起来都以进入“自动化”的时代,那我们为什么还要去了解gc和内存分配呢? 因为自动的好处是我们暂时可以不用管,省心,前提是一切运行正常。但是如果程序运行中发生了内存的泄漏和溢出,或者当垃圾回收成为系统达到高并发的瓶颈的时候,我们就需要对这些“自动化”的技术实施必要的监控和调节。 自动内存管理的核心 主要解决两个问题: 内存的自动分配和回收 (1)给对象分配内存;

  • 对象主要分配在堆中的Eden区上,有时候也分配在老年代(大对象),之后会通过gc回收;
  • 如果经过逃逸分析,是小对象且不会被外部引用,无逃逸,则直接在栈上分配,随着程序占空间的销毁而被回收,减轻了垃圾回收器的压力;
  • 标量替换(聚合量):如果一个对象没有被外部访问,则不会创建对象,直接创建一些标量,不能分解的变量,这样就会随着程序方法的调用结束而结束生命。

(2)回收分配给对象的内存;

  • 根据一定的算法回收无用的java对象。

2、垃圾回收

理解垃圾回收,首先了解三个问题。

  1. 回收什么?
  2. 何时回收?
  3. 如何回收?

2.1、回收什么?

    主要回收无用的对象,或者“已死”的对象。 java内存运行时区域的各个部分,其中程序计数器/虚拟机栈/本地方法栈三个都是随着线程而生,随线程而灭;栈中的每一个栈帧分配多少内存基本上是在类结构确定下来的时候就大体已经确定了【除了JIT编译器进行的一些优化】,所以随着方法的结束和线程的结束,内存自然就跟着回收了。     但是堆和方法区不一样,java所创建的对象基本上都存储在堆区,虽然“永久代”方法区的垃圾回收效率比较低,但是“永久代”对于废弃的常量和无用的类,也会进行相应的回收,尤其大量使用反射和动态代理CGlib等场景都需要虚拟机具备类卸载功能,保证无用的资源可以被回收,不会发生内存泄漏。     要回收就要确定什么样的对象是垃圾,确定该对象是否“存活”或者“死去”;“死去”的对像就是不可能再被任何途径使用的对象,这些对象就可以被回收。例如堆中一个对象没有任何指针对其引用(循环引用除外),它就是垃圾对象,方法区中废弃的常量和类等。

2.2、何时回收?

2.2.1引用计数法

    当该对象没有任何其他对象和程序的引用,即计数为0就可以回收。

  • 优点:实现简单,效率也很高;
  • 缺点:无法解决循环引用的问题;

2.2.2 GC Roots:可达性分析算法

    Tracing gc的基本思路是:进过一系列名为“gc-root"的对像作为起始点,从这些节点向下走的路径称之为引用链,当一个对象到gc-root没有任何引用链相连的时候,则证明此对象是不可达的,不可达的对象即可回收。   GC Roots,或者说”tracing GC”的根引用集合,就是 一组必须存活的对对象“live set”的引用集合。   思路:给定一个集合的引用作为根出发,通过引用关系遍历对象图,能被遍历到(可到达的)对象被判定为存活,其余对象(也就是没有遍历到的)就自然被判定为死亡。 因为GC的管理只针对java堆和方法区,栈区/本地方法区/程序计数器属于线程私有,不被GC管理,因而选择这些区域的对象作为gc root, 就可以找到存活的对象,至少保证正在使用的对象不被干掉,从而避免被gc回收。     在java语言里,可以作为gc-roots的对象包括:正在引用的对象--可以通过mat工具查看。

  • 虚拟机栈(栈帧中的本地变量表);所有java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话:当前所有正在被调用的方法的引用类型的参数/局部变量/临时值;因为局部变量属于栈帧,栈帧属于栈,一个虚拟机栈对应一个线程,说明这个变量正在被线程调用,通过它就可以找到那些不是垃圾;
  • 方法区中的静态属性引用的对象; 静态数据结构里指向gc堆里的对象引用;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI,Native方法中引用的对象;
  • Classloader;
  • java进程/线程;

注意:traceing gc 的本质是通过找出所有活对象来把其余空间认定为“无用”,而不是找出所有死掉的对象并回收它们的占用的内存。 gc-root的工作流程:  

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结

2.2.3 垃圾回收条件

思考: 不可达的对象一定会被回收吗 ? 在搜索算法中的不可达对象,也并非是非死不可的,可以进行自我拯救。真正宣告一个对象的死亡需要经以下两个条件:

  • 1. 没有与GC-root相连的引用链;
  • 2. 该对象是否有必要执行finalize方法;【判断对象是否覆盖过finalize方法,是否已经调用过一次】

   finalize()方法是对象逃过死亡的最后一次机会,意思就是对象在这个阶段还可以被复活。但是finalize方法只会被调用一次,如果有必要执行finalize方法,且该对象不想被回收,则只需要在F-Queue队列中重新引用上任何一个对象即可。如果此时对象任然没有任何的引用,那么就会被稍后GC正式的回收。  

2.2.4 回收时机

堆区垃圾回收:实例对象不可达及finalize方法调用之后仍没有引用;

  • 引用计数:循环引用问题;循环引用的对象是否挂在根上,挂在根上可达;否则不可达回收;
  • GC-Root算法 + finalize();
  • 强/软/弱/虚引用的判断;

方法区垃圾回收 废弃的常量和 无用的类

  • (1)该类所有的实例都已经被回收,即Java堆中不存在该类的任何实例;
  • (2)加载该类的ClassLoader已经被回收,因为类加载器是可以作为gc-root的;
  • (3)该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

方法区的垃圾回收注意点:

  • 1. 永久代的内存回收效率比较低下,回收的性能消耗高于回收的内存的收益,性价比低,所以一般可以不考虑使用。
  • 2. 可以使用参数设置,在反射和动态代理-CGLIB - 字节码框架asm使用频繁的场景建议打开永久代的内存回收策略;

2.3、如何回收?

    已经能够确定一个对象是垃圾之后,接下来就是考虑如何回收?如何回收就需要良好的垃圾回收算法,这个也是能让程序员解脱内存管理,专心编码的关键所在。大体主要分为3类垃圾回收算法。

2.3.1、标记-复制

主要思想是:将可用内存容量划分为大小相等的两块,每次只使用其中的一块,有50%的空间浪费,类似Eden区的from和to两块区域。当其中一块用完之后,就将还存活的对象复制到另一块后备空间里,然后在把使用过的内存空间一次清理释放掉。

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结
优点 实现简单,效率高; 不会存在内存碎片;
缺点 就是需要2倍的内存来管理; 复制需要时间开销,适用于少量对象的场景;
使用场景 新生代垃圾收集器基本都采用标记复制;

   我们可以从jvm的新生代设置的参数看到:-XX:SurvivorRatio=8,表示新生代中Eden区域和Survivor区域的容量比值,代表Eden:Survivor=8:1。一个Survior占新生代的1/10,表达的意思是 Survivor中的from区间和to区间与Edon的比例是1:1:8,from和to的空间也就是采用了标记复制的算法,所以空间大小一样,且程序运行期间浪费年轻代的1/10。

2.3.2、标记-清理

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

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结
优点 清除效率高;
缺点 存在内存碎片;
使用场景 老年代垃圾收集器CMS采用“标记-清理”但是可以通过设置灵活改变;

2.3.3、标记-整理

    与“标记-清理”不同的是标记完存活对象,清理完对象后,将所有存活的对象都向一端移动,并更新引用其对象的指针,进行了内存的复制整理。因为要移动对象,所以它的效率要比 “标记-清理”效率低,但是不会产生内存碎片。

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结
优点 没有碎片;
缺点 复制移动资源需要时间的开销; 指针的改变增加了程序的消耗;
使用场景 老年代的串行收集器和并行收集器都采用“标记-整理”的算法;

3、内存的分代思想

   由于java大部分对象都具有 “朝生夕死”的特性,所以对于存活时间长的对象,减少被gc的次数可以避免不必要的开销。这样我们就把内存分成新生代和老年代,新生代存放刚创建的和存活时间比较短的对象,老年代存放存活时间比较长的对象。这样每次仅仅清理年轻代,也就是Minor GC,非常频繁,但是速度较快;老年代的Major GC。仅在必要时时再做清理可以极大的提高GC效率,节省GC时间。

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结

分代与回收算法的使用

  • Young区: 复制算法,对象在被分配之后,“朝生夕死”,可能生命周期比较短,Young区复制效率比较高;
  • Old区: 标记清除或标记整理,Old区对象存活时间比较长,且对象多,复制来复制去没必要,不如做个标记清理,效率高;

3.1、内存分代的问题思考

问题一: 为什么会有老年代和青年代?

  • (1)提高垃圾回收效率,减少扫描的空间大小,分治思想;
  • (2)减少停顿时间,分代后扫描的区域小;
  • (3)停顿时间小提高吞吐量;
  • (4)扫描更加油针对性的young区,让对象早点被回收,减少old-gc的频率,接受一定程度的young-gc;

前提:对象大部分朝生夕死,对象都是先进入年轻代后进入老年代;此外java大部分用来做web开发: 例如:web应用,order订单;支付完线程就结束了,所以只有少部分对象存在,大部分都是垃圾对象; 问题二: 为什么会有survivor from和to区域?例如NewRation=8:1:1

  • (1)减小或减缓垃圾对象去老年代的机率;让朝生夕死的对象尽快回收,不让其进入老年代,如果进入老年代,old区很快被填满,导致触发full-gc频率增加;
  • (2)提高效率,避免扫描整个堆空间;增大old区的大小会导致一次full-gc的时间增长;
  • (3)from-to复制整理算法虽然有一半空间的浪费,但是提升了整个垃圾回收和空间的利用效率;

问题三: 何时进入老年代?

  • 虚拟机采用了分代收集的思想来管理内存,虚拟机给每个对象定义了一个对象年龄,没熬过一次MinorGC,年龄增加一岁,默认为15,就会被晋升到老年代,也可以通过参数:-XX:MaxThenuringThreshold来设置它的值。
  • 当suvivor区相同年龄的对象到达其内存空间的一半的时候,就直接进入老年代。变相的成为了大对象;
  • 申请的大对象,年轻代的空间不够,直接进入老年代;

问题四: 老年代的担保兜底策略:

  • 主要用历史晋升的对象的平均值大小来判断老年代剩余的空间是否满足接下来可能面临的新生代对象的晋升;
  • form和to的空间不够;
  • 担保机制,直接在老年代分配;

tips-大对象:可以通过-XX:PretenureSizeThreshold参数:令大于这个值的对象直接在老年代分配,这样做的目的是避免年轻代发生大批量的拷贝。  

3.2、垃圾收集器

(1). 新生代的收集器包括:

  • Serial  / PraNew / Parallel Scavenge;

(2). 老年代的收集器包括:

  • Serial Old  / Parallel Old / CMS;

(3). 回收整个Java堆(新生代和老年代)

  • G1收集器;

几种收集器及它们之间的组合关系如下:

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结

3.2.1、新生代垃圾收集器

(1). Serial收集器 - 复制算法 Serial收集器是最基本、发展历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择,它是一种单线程收集器,不仅仅意味着它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的是其在进行垃圾收集的时候需要暂停其他线。

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结
优点 对与单cpu,实现简单高效;
缺点 单线程工作不能充分利用多核cpu性能;之间会产生STW,给用户带来不良体验;
使用场景 新生代垃圾收集器,虚拟机运行在Client模式下的默认新生代收集器
使用参数 串行收集器,分为年轻代 Serial 和老年代 Serial Old 收集器。 1. -XX:+UseSerialGC 这个参数就是可以指定使用新生代串行收集器和老年代串行收集器; 2. -XX:+UseParNewGC 新生代使用 ParNew 回收器,老年代使用串行收集器; 3. -XX:+UseParallelGC 新生代私用 ParallelGC 回收器,老年代使用串行收集器; 而 Serial 收集器出现的日志为 DefNew . 注:【“+” 号的意思是ture,开启,反之,如果是 “-”号,则是关闭】

(2). ParNew 收集器 - 复制算法 ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 单CPU,ParNew不会比Serial收集效果更好,但是随着CPU的数量增加,ParNew则在GC时对系统资源有效利用更好;另外,除了serial收集器外,只有ParNew收集器可以和CMS收集器愉快的合作;

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结
优点 在多核cpu的情况下,gc效率更高;
缺点 单核的效率 <= Serial; 产生STW;
使用场景 新生代垃圾收集器,在Server模式下,ParNew收集器是一个非常重要的收集器,且能与CMS垃圾收集配合使用。
参数使用 行收集器是 Serial 的多线程版本,在 CPU 并行能力强大的计算机上有很大优势。 其中: 1. -XX:+UseParNewGC 强制设置新生代使用 ParNew 收集器,老年代使用串行收集器。 2. -XX:+UseConcMarkSweepGC:  会默认使用ParNew作为新生代收集器,老年代使用 CMS。 3. -XX:ParallelGCThreads={value} 这个参数是指定并行 GC 线程的数量,一般最好和 CPU 核心数量相当。默认情况下,当 CPU 数量小于8, ParallelGCThreads 的值等于 CPU 数量,当 CPU 数量大于 8 时,则使用公式:3+((5*CPU)/ 8);同时这个参数只要是并行 GC 都可以使用,不只是 ParNew。 而ParNew的GC日志则表吸纳出ParNew。

(3). Parallel Scavenge并行回收收集器 - 复制算法     Parallel Scavenge收集器也是一个新生代并行多线程收集器。Parallel Scavenge收集器的目标则是: 追求可控制高吞吐量+高效利用CPU 。吞吐量=程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。

垃圾回收算法1、自动内存管理2、垃圾回收3、内存的分代思想4、JVM常用配置参数5、小结
优点 吞吐量可控,让用户代码获得更长的运行时间;并行收集高效利用多核cpu;
缺点 (1) 标记和清理产生2次STW; (2) 不能和cms一起愉快的合作,框架不同;
使用场景 该 收集器是 Java 8 的默认新生代收集器,因为它能够根据系统当前状态给出吞吐量最高的GC 配置。所以,在一些手工调优复杂的场合或者对实时性要求不高的场合,可以使用该处理器。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
参数使用 1. -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间,他的值是一个大于0的整数。ParallelGC 工作时,会调整 Java 堆大小或者其他的一些参数,尽可能的把停顿时间控制在 MaxGCPauseMillis 以内。 注意:此值不是越小越好,原因是GC停顿时间缩短是以牺牲吞吐量和调整新生代空间来换取的,这将会到值频繁 GC ,虽然系统停顿时间小了,但总吞吐量下降了。 2. -XX: GCTimeRatio 设置吞吐量大小,他的值是一个0 到100之间的整数,假设 GCTimeRatio 的值是 n ,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集,默认 n 是99,即不超过1% 的时间用于垃圾收集。 3. -XX:UseAdaptiveSizePolicy: 打开自适应策略,与ParNew的区别。在这种模式下,新生代的大小,eden 和 Survivor 的比例,晋升老年代的对象年龄等参数会被自动调整。以达到堆大小,吞吐量,停顿时间的平衡点。 注: 参数1-2其实是矛盾的,吞吐量和停顿时间是反比的,需要找到一个平衡点。如果是调参场景比较复杂的情况下,可采用自适应策略。

3.2.2、老年代垃圾收集器

(1). Serial Old收集器 - 标记整理算法     Serial Old是Serial收集器的老年代垃圾回收,它同样是一个单线程(串行)收集器,不同于年轻代的Serial算法的是使用标记整理算法,工作流程和年轻代一样。

优点 单线程工作效率高,实现简单;
缺点 产生STW;多核CPU不能充分利用其性能
使用场景 在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用; 作为 CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用;
参数使用 参考Serial收集器的使用

(2). Parallel Old收集器 - 标记整理算法 Parallel Old是Parallel Scavenge收集器的老年代版本,也是一种关注系统吞吐量的老年代垃圾回收机制,使用多线程和 “标记-整理”算法。之前一直是 Parallel Scavenge + old serial的组合,由于无法真正的利用多核cpu的性能,所以其吞吐量反而不一定有PreNew+CMS组合给力,现在终于才有了新生代和老年代都关注的吞吐量的新组合: Parallel Scavenge + Parallel Old。

优点 高效利用cpu;获得较大的吞吐量;可以和Parallel Scavenge一起愉快的合作;
使用场景 老年代垃圾回收, 注重吞吐量以及CPU资源敏感的场景 , “吞吐量优先”  收集器的名副其实 组合: Parallel Scavenge +  Parallel Old 组合。
参数使用 -XX:+ UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器。该参数可以启用 ParallelOldGC。 -XX: ParallelGCGThreads :同时可以指定该参数设置并行线程数量。

4、JVM常用配置参数

配置参数 功能
-Xms 初始堆大小。如:-Xms4g
-Xmx 最大堆大小。如:-Xmx4g
-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 永久代(方法区)的最大值;
开启各种GC算法的参数 1)串行
  • -XX:+UseSerialGC
  • -XX:+UseSerialOldGC
(2)并行(吞吐量优先):
  • -XX:+UseParallelGC
  • -XX:+UseParallelOldGC
(3)并发收集器(响应时间优先)
  • -XX:+UseConcMarkSweepGC
  • -XX:+UseG1GC
gc日志打印参数 1. -XX:+PrintGCDateStamps 打印 GC 日志时间戳。 2. -XX:+PrintGCDetails 打印 GC 详情。 3. -XX:+PrintGCTimeStamps: 打印此次垃圾回收距离jvm开始运行的所耗时间。 4. -Xloggc: 将垃圾回收信息输出到指定文件 5. -verbose:gc 打印 GC 日志 6. -XX:+PrintGCApplicationStopedTime 查看 gc 造成的应用暂停时间 7. XX:+PrintTenuringDistribution, 对象晋升的日志 8. -XX:+HeapDumpOnOutOfMemoryError 内存溢出时输出 dump 文件。

5、小结

  • 1、年轻代垃圾回收器-复制算法
    • serial:串性收集器 - 复制算法
    • PraNew:多线程收集器- serial的多线程版本 - 复制算法
    • Parallel Scavenge: 并行收集器 - 复制算法--开始关注吞吐量
      • Parallel Scavenge追求可控制的吞吐量 + 高效利用cpu
        • -XX:MaxGCPauseMillis 设置最大垃圾收集停顿时间
        • XX:GCTimeRatio 设置吞吐量大小,默认 n 是99,即不超过1% 的时间用于垃圾收集。
  • 2、老年代垃圾回收器-标记清理
  • serial-old:串性收集器 - 标记整理算法
    • cms的后备垃圾收集器,当出现concurrent mode failure时使用;
  • Parallel-old: - 标记整理算法,和 Parallel Scavenge组合,
    • 之前都是Parallel Scavenge + old Serial组合;
    • PraNew+ CMS组合(后备:serial-old) ;
    • 现在有了:Parallel-Scavenge + Parallel-Old,真正关于吞吐量的一个垃圾收集组合;
    • -XX:ParallelGCGThreads 同时可以指定该参数设置并行线程数量。
  • CMS(Concurrent Mark Sweep):标记清除算法 —开始关注较少的停顿时间-后面详解
  • G1:关注停顿时间可控;

    水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。   参考资料: 《深入了解jvm虚拟机》 参考资料: https://docs.oracle.com/en/java/javase/11/gctuning/available-collectors.html#GUID-F215A508-9E58-40B4-90A5-74E29BF3BD3C