文章目录

  • 一、垃圾回收概述
    • 1. 什么是垃圾?
    • 2. 为什么需要GC
    • 3. 早起的垃圾回收
    • 4. Java垃圾回收机制
  • 二、垃圾回收相关算法
    • 1. 垃圾标记阶段: 引用计数算法
    • 2. 标记阶段: 可达分析算法
      • 2.1 GC Roots
    • 3. 对象的finalization机制
      • 3.1 具体过程
    • 4. MAT与JProfiler的GC Roots溯源
      • 4.1 MAT
      • 4.2 JProfiler
    • 5. 清除阶段:标记-清除算法(mark-Sweep)
    • 6. 清除阶段:复制算法
    • 7. 清除阶段:标价-压缩算法
    • 8. 小结
    • 9. 分代收集算法
    • 10. 增量收集算法、分区算法
  • 三、垃圾回收相关概念
    • 1. System.gc()的理解
    • 2. 内存溢出与内存泄漏
      • 2.1 内存溢出(OOM)
      • 2.2 内存泄漏(Memory Leak)
    • 3. Stop The World
    • 4. 垃圾回收的并行与并发
    • 5. 安全点与安全区域
      • 5.1 安全点
      • 5.2 安全区域
    • 6. 引用
      • 6.1 强引用–不回收
      • 6.2 软引用–内存不足即回收
      • 6.3 弱引用–发现即回收
      • 6.4 虚引用–对象回收跟踪
      • 6.5 终结器引用
  • 四、垃圾回收器
    • 1. GC分类和性能指标
      • 1.1 按垃圾回收线程数分
      • 1.2 按工作模式分
      • 1.3 按碎片处理方式分
      • 1.4 按工作的内存区间分
      • 1.5 GC的性能指标
        • 1.5.1 吞吐量
        • 1.5.2 暂停时间
        • 1.5.3 内存占用
    • 2. 不同垃圾回收器概述
    • 3. Serial回收器:串行回收器
    • 4. ParNew回收器:并行回收
    • 5. Parallel回收器:吞吐量优先
      • 5.1 参数设置
    • 6. CMS回收器:低延迟
      • 6.1 CMS的工作原理
      • 6.2 详细理解CMS回收器:低延迟
      • 6.3 CMS的优点
      • 6.4 CMS的弊端
      • 6.5 CMS收集器可以设置的参数
      • 6.6 总结
    • 7. G1回收器:区域化分代式
      • 7.1 G1名字的由来
      • 7.2 G1回收器的特点(优势)
      • 7.3 缺点
      • 7.4 G1回收器的参数设置
      • 7.5 G1回收器的常见操作步骤
      • 7.6 G1回收器的适用场景
      • 7.7 分区Region:化整为零
      • 7.8 G1回收器垃圾回收过程
        • 7.8.1 概述
        • 7.8.2 详解
        • 7.8.3 年轻代GC
        • 7.8.4 并发标记过程
        • 7.8.5 混合回收
        • 7.8.6 可选的回收过程:FUll GC
      • 7.9 G1回收过程:补充
      • 7.10 G1回收器优化建议
    • 8 . 垃圾回收器总结
    • 9. GC日志分析

一、垃圾回收概述

1. 什么是垃圾?

  1. 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。
  2. 什么是垃圾???
    • 垃圾是指在运行程序中没有任何引用指向的对象,这个对象就是需要被回收的垃圾。
  3. 如果不及时对内存中的垃圾进行清理,那么这些垃圾对象所占的内存空间会一直保留到应用程序结束,被保留的空间无法被其他对象使用。甚至可能导致内存溢出

2. 为什么需要GC

  1. 对于高级语言来说,一个基本认识是如果不进行垃圾回收,内存迟早会被消耗完,因为不断的分配内存空间而不进行回收,就好像不停的生产生活垃圾从来不打扫一样。
  2. 除了释放没用的对象,垃圾回收也可以清楚内存中的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
  3. 随着应用程序所应付的业务越来越庞大、复杂、用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断尝试对GC进行优化。

3. 早起的垃圾回收

  1. 通过new 和delete关键字进行内存分配和释放。
  2. 这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。倘若有一处内存区间由于程序员编码问题忘记被回收,那么就会产生内存泄漏,垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所消耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。

4. Java垃圾回收机制

  1. 优点:

    • 自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏和内存溢出的风险
    • 没有垃圾回收器,java也会和cpp一样,各种悬垂指针,野指针,泄漏问题让你头疼不已。
    • 自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
  2. 缺点:
    • 对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将是一场灾难,最严重的就会弱化Java开发人员在出现内存溢出时定位问题和解决问题的能力
    • 此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能在遇见OutOfMemoryError时,快速地根据错误异常日志定位问题和解决问题
    • 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

二、垃圾回收相关算法

1. 垃圾标记阶段: 引用计数算法

  1. 引用计数算法比较简单,对每个对象保存一个整型的引用计数属性。用于记录对象被引用的情况。
  2. 对与一个对象A,只要有任何一个对象引用A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
  3. 优点:
    • 实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性
  4. 缺点:
    • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销
    • 每次赋值都需要更新计数器,伴随着加法和减法操作,着增加了时间开销。
    • 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收中没有使用这类算法。
  5. 小结:
    • 引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
    • 具体哪种最优要看场景的,业界有大规模实践中仅保留引用计数机制,以提供吞吐量的尝试。
    • Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
    • Python如何解决循环引用呢?
    • 手动解除:很好理解,就是在和顺的时机,解除引用关系。
    • 使用弱引用Weakref,weakref是Python提供的标准库,旨在解决循环引用。

2. 标记阶段: 可达分析算法

  1. 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄漏的发生
  2. 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫做追踪性垃圾收集。
  3. 可达性算法的基本思路
    • “GC Roots”:根集合就是一组必须活跃的引用
    • 可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
    • 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或者间接连接着,搜索所走过的路径称为引用链(Reference Chain)
    • 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
    • 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。

2.1 GC Roots

在Java语言中,GC Roots包括以下几类元素:

  1. 虚拟机栈中应用的对象

    • 比如:各个线程被调用的方法中使用的到的参数、局部变量等。
  2. 本地方法栈内JNI(通常说的本地方法)引用的对象。
  3. 方法区中类静态属性引用的对象
    • 比如:Java类的引用类型静态变量。
  4. 方法区中常量引用的对象
    • 比如:字符串常量池(String Table)里的引用。
  5. 所有被同步锁synchronized持有的对象
  6. java虚拟机内部的引用
    • 基本数据类型对应的Class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
  7. 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
  8. 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收(Partial GC)
    • 如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能别其他区域的对象所引用,这是候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
  9. 小技巧:
    • 由于Root 采用栈方式存放变量和指针,所有如果一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
  10. 注意:
    • 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行(相当于冻结在某个时间点上)。这点不满足的话分析结果的准确性就无法保证。
    • 这点也是导致GC进行时必须“Stop The World”的一个重要原因。
    • 即使是号称(几乎)不会发生停顿的CMS收集器,枚举根节点时也是必须要停顿的。

3. 对象的finalization机制

  1. java语言提供了对象终止(finalization)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。
  2. 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法
  3. finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。
  4. 永远不要主动调用finalize()方法,应该交给垃圾回收机制调用。例如包括下面三点:
    • 在finalize()是可能会导致对象复活。
    • 在finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize()方法将没有执行机会。
    • 一个糟糕的finalize()会严重影响GC的性能。
  5. 从功能上来说,finalize()方法与C++中的析构函数比较相似,但是Java采用的是基于垃圾回收器的自动内存管理机制,所以finalize()方法在本质上不同于C++中的析构函数。
  6. 由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
    • 如果从所有的根节点都无法访问到某个对象,说明对象已经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候他们暂时处于“缓刑”阶段。一个无法触及的对象由可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态,如下:

      • 可触及的:从节点开始,可以到达这个对象(不是垃圾)。
      • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
      • 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()之后被调用一次。
    • 以上三种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及是才可以被回收。

3.1 具体过程

判断一个对象objA是否可回收,至少要经历两次标记过程。

  1. 如果对象objA到GC Roots没有引用链,则进行第一次标记。
  2. 进行筛选,判断此对象是否有必要执行finalize()方法。
    • 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
    • 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发器finalize()方法执行。
    • finalize()方法是对象逃脱死亡的最后机会。稍后GC会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时, objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize()方法之后被调用一次。

4. MAT与JProfiler的GC Roots溯源

4.1 MAT

MAT是Memory Analyzer的简称,它是一款功能强大的java堆内存分析器。用于查找内存泄漏以及查看内存消耗情况。
MAT是基于Eclipse开发的,是一款免费的性能分析工具。
可以在http://www.eclipse.org/mat 下载并使用MAT。

  1. 使用方式一: 命令行使用jmap生成dump文件

  2. 方式2:使用JVisualVM导出dump文件

4.2 JProfiler

  1. 使用JProfiler进行对象的溯源,查询没有被回收的对象到底和那个GC Root关联。
  2. 使用JProfiler查询程序oom的原因。

5. 清除阶段:标记-清除算法(mark-Sweep)

背景
标记-清除算法是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并应有与Lisp语言。
执行过程
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,分为两个阶段,第一阶段则是标记,第二阶段是清除。
  • 标记:Collector从引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象
  • 清除:Collector对堆内存从头到尾进行线性(所有的对象都遍历)遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收
  • 缺点:
    • 效率不算高, 遍历两遍。

    • 在进行GC的时候,需要停止整个应用程序,导致用户体验差。

    • 这种方式清理出来的空闲内存不是连续的,产生内存碎片。需要维护一个空闲列表。

      注意:何为清除?
      这里所谓的清除病史真的置空,而是把需要清除的对象地址保存在空闲地址列表里。下次有新对象需要加载时,判断垃圾的位置空间是否足够,如果够,就存放。

6. 清除阶段:复制算法

背景
为了解决标记清除算法在垃圾收集效率方法的缺陷,引入了复制算法。
核心思想
1. 将活着的内存空间分为两块,每次只使用其中一块,
2. 在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,
3. 之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

优点:
1. 没有标记和清除过程,实现简单,运行高效。
2. 复制过去以后保证空间的连续性,不会出现“碎片”问题。
3. 使用指针配置维护引用地址。
缺点:
1. 此算法的缺点也很明显,就是需要两倍的内存空间。
2. 对应G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region直接对象引用关系,不管是内存占用或者实际开销也不小(主要是地址改变了,栈中的引用地址也要跟着改变,也是有消耗的)。
3. 系统的中需要存在大量的垃圾对象,这样复制的时候只需要复制少量的对象,否则基本上都复制了,其实这是复制就没有意义了。因此复制算法应该应用在大量垃圾的地方,比如年轻代中,因为大量的对象都是朝生夕死。

7. 清除阶段:标价-压缩算法

  1. 背景

    • 复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生
    • 但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
    • 记-清除算法可以应用在老年代中,但是该算法不仅执行效率对象,而且在执行完内存回收后会产生内存碎片,所有JVM的设计者需要在此基础上进行改进,标记-压缩算法由此诞生
  2. 执行过程
    • 第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象。
    • 第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。
    • 之后,清理边界外所有的空间。
  3. 使用效果
    • 标记-压缩算法的最终效果等同于标记-清除算法执行完成后,在执行一次内存碎片整理,因此,也可以把它称为标记-清除-压缩算法。
    • 二者的本质差异在于标记-清除算法是一种非移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是一项优缺点并存的风险决策(移动对象的引用地址都得改变)。
    • 可以看到,标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来当我们需要给新对象分配内存是,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销(使用指针碰撞维护)。
  4. 优点
    • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给对象分配内存是,JVM只需要持有一个内存的起始地址即可(指针碰撞的方式,标记-清除使用空闲列表的方式)。
    • 消除了复制算法当中,内存减半的高额代价
  5. 缺点
    • 从效率上来说,标记-整理算法要低于复制算法
    • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
    • 移动过程中,需要全程暂停用户应用程序,即:STW。

8. 小结

  1. 效率上来说,·复制算法是当之无愧的老大·,但是却浪费了太多的内存。
  2. 为了尽量兼顾上面提到的三个指标,标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。

9. 分代收集算法

  1. 没有最好的算法,只有最合适的算法,因地制宜
  2. 前面所有算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
  3. 分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
  4. 在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这里对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象声明周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有序对象甚至只用一次即可回收。
  5. 目前几乎所有的GC都是采用分代收集算法执行垃圾回收的。
  6. 在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点:
    • 年轻代:

      • 年轻代特点:区域相对老年代较小,对象生命周期短,存活率低,回收繁重。
      • 这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适合年轻代的回收,而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。
    • 老年代
      • 老年代特点:区域较大,对象生命周期长,存活率高,回收不及年轻代频繁。
      • 这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除与标记-压缩的混合实现。
      • Mark阶段的开销与存活对象的数量成正比。
      • Sweep阶段的开销与所管理区域的大小成正比。
      • Compact阶段的开销与存活对象的数量成正比。
  7. 以HotSpot中的CMS回收器为例,CMS是基于Mark-Sweep实现的,对应对象的回收效率很高。而对应碎片问题,CMS采用基于Mark-compack算法的serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent Mode Failure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。
  8. 分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。

10. 增量收集算法、分区算法

  1. 背景

    • 上述现有的算法,在垃圾回收过程中,应用软件将处于一种Stop the World的状态。在Stop the world状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过程,应用程序被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集算法的诞生
  2. 基本思想

    • 如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成
    • 总的来说,增量收集算法的基础仍然是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
  3. 缺点
    使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序的代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降

  4. 分区算法

    • 一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。
    • 分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
    • 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

三、垃圾回收相关概念

1. System.gc()的理解

  1. 默认情况下,通过System.gc()或者Runtime.getRuntime().gc()的调用,会显示触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。
  2. 然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用。(相当于提醒垃圾回收器去进行full gc,但是垃圾回收器会不会执行看垃圾回收器心情)
  3. JVM实现者可以通过System.gc()调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()。

2. 内存溢出与内存泄漏

  1. 内存溢出相对于内存泄漏来说,尽管更容易被理解,但是同样的,内存溢出也是引发程序崩溃的罪魁祸首之一。
  2. 由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。
  3. 大多数情况下,GC会进行各种年龄段的垃圾回收,是在不行了就放大招,来一次独占式的Full GC操作,这时候会回收大量的内存,供应用程序继续使用。
  4. javadoc中对OutOfMemoryError的解释是,没有空闲内存,并且垃圾收集器无法提供更多内存

2.1 内存溢出(OOM)

  1. java虚拟机的堆内存设置不够。

    • 比如:可能存在内存泄漏问题。也可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显示指定JVM堆大小或者指定数值偏小。我们可以通过参数-Xms、-Xmx来调整。
  2. 代码创建了大量对象,并且长时间不能被垃圾收集器收集(存在被引用)
    • 对应老版本的Oracle JDK,因为永久代的大小是有限的,并且JVM对永久代垃圾回收(如,常量池回收、卸载不在需要的类型)非常不积极,所以我们不断添加新类型的时候,永久代出现OutOfMemoryError也非常多见,尤其是在运行时存在大量动态类型生成的场合:类似intern字符串缓存占用太多空间,也会导致OOM问题。对应的异常信息会标记处理和永久代相关:“java.lang.OutOfMemoryError:PermGen space”。
  3. 随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的OOM有所改观,出现OOM,异常信息则变成了“java.lang.OutOfMemoryError:Metaspace”。直接内存不再,也会导致OOM。

2.2 内存泄漏(Memory Leak)

  1. 也称作“存储渗漏”。严格来讲,只有对象不会在被程序用到了,但是GC又不能回收它们的情况,才叫内存泄漏。
  2. 但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致OOM,也可以叫做宽泛意义上的“内存泄漏”。
  3. 尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现OutOfMemory异常,导致程序崩溃。
  4. 注意,这里的存储空间并不是指物理空间,而是指虚拟内存大小,这个虚拟内存大小取决于磁盘交换区设定的大小。
  5. 举例
    • 单例模式:单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则导致内存泄漏的产生。
    • 一些提供close的资源未关闭导致内存泄漏(涉及到外部资源的都要手动close)数据库连接(dataSource.getConnection()),网络连接(socket)和io连接必须手动close,否则不能被回收的。

3. Stop The World

  1. Stop The World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
  2. 可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
    • 分析工作必须在一个能确保一致性的快照中进行。
    • 一致性质整个分析期间整个执行系统看起来像被冻结在某个时间点上。
    • 如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。
  3. 被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所有我们需要减少STW的发生。
  4. STW事件和采用哪款GC无关,所有的GC都有这个事件。
  5. 即使G1也不能完全避免STW情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
  6. STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
  7. 开发不要用System.gc();会导致STW的发生。

4. 垃圾回收的并行与并发

并发和并行,在谈论垃圾收集的上下文语境中,它们就可以解释如下:

  1. 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。

    1. 如:ParNew、Parallel Scavenge、Parallel Old
    2. 串行(Serial)
    3. 相较于并行的概念,单线程执行。
    4. 如果内存不够,则程序暂停,启动JVM垃圾回收器进行垃圾回收。回收完,再启动程序的线程
  2. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户的运行。

    1. 用户程序在继续运行,而垃圾收集程序线程运行与另一个CPU上;
    2. 如:CMS、G1

5. 安全点与安全区域

5.1 安全点

  1. 程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点。
  2. Safe Point的选择很重要,如果太少(安全点设置的太少)可能导致GC等待时间太长,如果太频繁(安全点设置的太多)可能导致运行时的性能问题。大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为Safe Point,如果方法调用、循环跳转和异常跳转等。
  3. 如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
    • 抢先式中断:(目前没有虚拟机采用了)
    • 首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。
    • 主动式中断:
    • 设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标记,如果中断标志为真,则将自己进行中断挂起。

5.2 安全区域

  1. Safe Point机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序“不执行”的时候呢?例如线程处于Sleep状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。对于这种情况,就需要安全区域(Safe Region)来解决。
  2. 安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。我们也可以把Safe Region看做是被扩展了的SafePoint。
  3. 实际执行时:
    • 当线程运行到Safe Region的代码时,首先标识已经进入Safe Region。如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程;
    • 当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开SafeRegion的信号为止。

6. 引用

  1. 我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中,如果内存空间在进行垃圾回收后还是很紧张,则可以抛弃这些对象。
  2. 【面试题】强引用、软引用、弱引用、虚引用有什么区别?具体使用场景是什么?
    • 在JDK1.2版本后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。

    • 除强引用外,其他3中能够引用均可以在java.lang.ref包中找到他们的身影。如下图,显示了这3种引用类型对应的类,开发人员可以在应用程序中直接使用它们。

    • Reference子类中只有终结器引用是包内可见的,其他3种引用类型均为public,可以在应用程序中直接使用。

      • 强引用:最传统的“引用”定义,是指在程序代码中普遍存在的引用赋值,即类似“Object object = new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
      • 软引用:在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
      • 弱引用:被弱引用关联的对象只能生存到下一次垃圾回收之前,当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
      • 虚引用:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

6.1 强引用–不回收

  1. 在Java程序中,最常见的引用类型是强引用(普通系统99%以上都是强引用),也就是我们最常见的普通对象引用,也是默认的引用类型。
  2. 当在Java语言中使用new 操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。
  3. 强引用的对象时可触及的,垃圾收集器就是永远不会回收掉被引用的对象
  4. 对应一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示的将相应(强)引用赋值为null,就是可以当做垃圾被收集了,当然具体回收时机还是要看垃圾收集策略。
  5. 相对的,软引用、弱引用和虚引用的对象时软可触及、弱可触及和虚可触及的。在一定条件下,都是可以被回收的。所以,强引用是造成java内存泄漏的主要原因之一。
  6. 强引用可以直接访问目标对象。
  7. 强引用所指向的对象在任何时候都不会被系统回收,虚拟机宁愿抛出OOM异常,也不会回收强引用所指向对象。
  8. 强引用可能导致内存泄漏。

6.2 软引用–内存不足即回收

  1. 软引用是用来描述一些还有用,但非必需的对象。只被软引用关联的对象,在系统将要发生内存溢出异常前,会把这些对象列入回收范围之中进行第二次回收(第一次回收是指回收到不可达的对象,通过可达性算法),如果这次回收还没有足够的内存,才会抛出内存溢出异常。
  2. 软引用通常用来实现内存敏感的缓存。比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足是清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  3. 垃圾回收器在某个时刻决定回收软可达的对象的时候,会清理软引用,并可选的把引用存放到一个引用队列(Reference Queue)。
  4. 类似弱引用,只不过Java虚拟机会尽量让软引用的存活时间长一些,迫不得已才清理。

6.3 弱引用–发现即回收

  1. 弱引用也是用来描述哪些非必需对象,只被弱引用关联的对象只能生存到下一次垃圾收集发生为止。在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。
  2. 但是,由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。
  3. 弱引用和软引用一样,在构造引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。
  4. 软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。
  5. 弱引用对象与软引用对象的最大不同就在于,当GC在进行回收时,需要通过算法检查是否回收软引用对象,而对于弱引用对象,GC总是进行回收。弱引用对象更容易、更快被GC回收

6.4 虚引用–对象回收跟踪

  1. 也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。
  2. 一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎完全一样,随时都可能被垃圾回收器回收。
  3. 它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取的对象时,总是null。
  4. 为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。
    虚引用必须和引用队列一起使用。虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况(我们通过查看引用队列做相应的操作)。
    由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

6.5 终结器引用

四、垃圾回收器

1. GC分类和性能指标

1.1 按垃圾回收线程数分

  1. 可以分为串行垃圾回收器和并行垃圾回收器
  2. 串行回收指的是在同一时间段内只允许一个CPU用于执行垃圾回收操作,此时工作线程被暂停,直至垃圾收集结束。
  3. 在诸如但CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。所以串行回收默认被应用在客户端的client模式下的JVM中。
  4. 在并发能力比较强的CPU上,并行回收器产生的停顿时间要短与串行回收器。
  5. 和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了Stop The World机制。

1.2 按工作模式分

  1. 可以分为并发式垃圾回收器和独占式垃圾回收器
  2. 并发式垃圾回收器与应用程序线程交替或同时工作,以尽可能减少应用程序的停顿时间。
  3. 独占式垃圾回收器(stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

1.3 按碎片处理方式分

  1. 可以分为压缩式垃圾回收器和非压缩式垃圾回收器。
  2. 压缩式垃圾回收器回收完成后,对存活对象进行压缩整理,清除回收后的碎片。
    1. 再分配对象使用指针碰撞。
  3. 非压缩式的垃圾回收器不进行内存空间整理。
    1. 再分配对象使用空闲列表。

1.4 按工作的内存区间分

  1. 可以分为年轻代垃圾回收器和老年代垃圾回收器。

1.5 GC的性能指标

  1. 吞吐量:运行用户代码的时间占总运行时间的比例

    1. 总运行时间:程序的运行时间+内存回收的时间
  2. 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
  3. 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  4. 收集频率:相对于应用程序的执行,收集操作发生的频率。
  5. 内存占用:Java堆所占的内存大小。
  6. 快速:一个对象从诞生到被回收所经历的时间。
  7. 上面3中标注的指标共同构成了一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。
  8. 这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应有程序的影响,即提高了吞吐量。而内存的扩大,对延迟反而大量负面效果(内存空间变大,导致回收时用的时间越长)。
  9. 简单来说,主要抓住两点:
    1. 吞吐量
    2. 暂停时间

1.5.1 吞吐量

  1. 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。
  2. 这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应式不必考虑的。
  3. 吞吐量优先,意味着在单位时间内,STW的时间最短:

1.5.2 暂停时间

  1. 暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态

    1. 例如:GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的
    2. 现在的目标:在最大吞吐量优先的情况下,降低停顿时间。

1.5.3 内存占用

2. 不同垃圾回收器概述

  1. 发展史
  2. 7款经典的垃圾收集器
    • 串行回收器:Serial、Serial Old
    • 并行回收器:ParNew、Parallel Scavenge、Parallel Old
    • 并发回收器:CMS、G1
  3. 7款经典收集器与垃圾分代之间的关系
    • 新生代收集器:Serial、ParNew、Paralle Scavenge
    • 老年代:Serial Old、Paralle Old、CMS
    • 整堆收集器:G1
  4. 垃圾收集器的组合关系
    • 为什么要有很多收集器,一个不够么?因为Java的使用场景很多,移动端,服务器等。所有就需要针对不同的场景,提供不同的垃圾收集器,提供垃圾收集的性能。
    • 虽然我们会对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。没有一种放之四海皆准、任何场景下都适用的完美收集器存在,更加没有万能的收集器。所以我们选择的只是对具体应用最合适的收集器。
  5. 如何查看默认的垃圾收集器
    • -XX:+PrintCommandLineFlags:查看命令行相关参数(包含使用的垃圾收集器)
    • 使用命令行指令:jinfo -flag 相关垃圾回收器参数 进程id
    • 如果结果是 + 表示使用,是 - 表示未使用。

3. Serial回收器:串行回收器

  1. Serial收集器是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。
  2. Serial收集器作为HotSpot中Client模式下的默认新生代辣椒水收集器。
  3. Serial收集器采用复制算法、串行回收和“Stop the world”机制的方式执行内存回收。
  4. 除了年轻代之外,Serial收集器还提供用于执行老年代垃圾收集Serial Old收集器。Serial Old收集器通用也采用了串行回收和“Stop the World”机制,只不过内存回收算法使用的是标记-压缩算法。
  5. Serial Old是运行在Client模式下默认的老年代的垃圾收集器。
  6. Serial Old在Server模式下主要有两个用途:
    • 于是新生代Parallel Scavenge配合使
    • 作为老年代CMS收集器的后备垃圾收集方案
  7. 工作示意图
    • 这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有工作线程,指定它收集结束(Stop The World)
  8. 优势:
    • 简单而高效(与其他收集器在单线程时工作比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
    • 运行在Client模式下的虚拟机是个不错的选择。
  9. 在用户的桌面应用场景中,可用内存一般不大(几十MB至一两百MB),可以在较短时间内完成垃圾收集(几十ms至一百多ms),只要不频繁发生使用串行回收器是可以接受的。
  10. 在HotSpot虚拟机中,使用 -XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器。
    • 等价于 新生代使用Serial GC,且老年代使用Serial Old GC。
  11. 总结
    • 这种垃圾收集器大家了解,现在已经不用串行的了。而且在限定单核CPU才可以用。现在都不是单核的了。
      -对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web应用程序中不会采用串行垃圾收集器的。

4. ParNew回收器:并行回收

  1. 如果说Serial GC是年轻代中的单线程垃圾收集器,那么ParNer收集器则是Serial收集器的多线程版本。

    • Par是Parallel的缩写,New:只能处理的是新生代。
  2. ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法、“Stop The World”机制
  3. ParNew是很多JVM运行在Server模式下新生代的默认垃圾收集器
  4. 对于新生代,回收次数频繁,使用并行方式高效。
  5. 对于老年代,回收次数较少,使用串行方式节省资源。(CPU并行需要切换线程,串行可以省去切换线程的资源)
  6. 在程序中,开发人员可以通过选项“-XX:+UseParNewGC”手动指定使用ParNew收集器执行内存回收任务。它表示年轻代使用并行收集器,不影响老年代。
  7. -XX:ParallelGCThreads限制线程数量,默认开启和CPU数据相同的线程数。

5. Parallel回收器:吞吐量优先

  1. HotSpot的年轻代除了拥有ParNew收集器是基于并行回收的以外,Parallel Scavenge收集器通用也采用了复制算法,并行回收和“Stop The World”机制。
  2. 那么Parallel收集器的出现是否多此一举?
    • 和ParNew收集器不同,Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。
    • 自适应调节策略也是Parallel Scavenge与ParNew的一个重要区别。
  3. 高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运行而不需要太多交互的任务。因此,场景的服务器环境中使用。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序。
  4. Parallel收集器在JDK1.6时提供了用于执行老年代垃圾收集的Parallel Old收集器,用来代替老年代的Serial Old收集器。
  5. Parallel Old收集器采用了标记-压缩算法,但同样也是基于并行回收和“Stop The World”机制。
  6. 在程序吞吐量优先的应用场景中,Parallel收集器和Parallel Old收集器的组合,在Server模式下的内存回收性能很不错。
    在Java8中,默认是此垃圾收集器。

5.1 参数设置

  1. -XX:+UseParallelGC 手动指定年轻代使用Parallel并行收集器执行内存回收任务。
  2. -XX:+UseParallelOldGC手动指定老年代都是使用并行回收收集器。
    1. 分别适用于新生代和老年代。默认jdk8是开启的。
    2. 上面两个参数,默认开启一个,另一个也会被开启。(互相激活)
  3. -XX:ParallelGCThreads设置年轻代并行收集器的线程数。一般地,最好与CPU数量相等,以避免过多的线程数影响垃圾收集性能。
    • 在默认情况下,当CPU数量小于等于8个,ParallelGCThreads的值等于CPU数量。
    • 当CPU数量大于8个,ParallelGCThreads的值等于3+【5CPU_Count】/8,比如CPU核数12个,512/8=7.5 约等于7,7+3 = 10。
  4. -XX:MaxGCPauseMillis设置垃圾收集器最大停顿时间(即STW的时间),单位是毫秒。
    • 为了尽可能地把停顿时间控制在MaxGCPauseMills以内,收集器在工作时会调整Java堆大小或者其他一些参数。
    • 对于用户来讲,停顿时间越短体验越好。但是在服务器端,我们注重高并发,整体的吞吐量。所以服务器端适合Parallel,进行控制。
    • 该参数还有需谨慎。
  5. -XX:GCTimeRatio垃圾收集时间占总时间的比例(= 1 / (N + 1))。用于衡量吞吐量的大小。
    • N取值访问(0,100)。默认值99,也就是垃圾回收时间不超过1%。
    • 与前一个-XX:MaxGCPauseMillis参数有一定矛盾性。暂停时间越长,Ratio参数就容易超过设定的比例。
  6. -XX:+UseAdaptiveSizePolicy设置Parallel Scavenge收集器具有自适应调节策略。
    • 在这种模式下,年轻代的大小、Eden和Survivor的比例、晋升老年代的对象年龄等参数会被自动调整,已达到在堆大小、吞吐量和停顿时间的平衡点。
    • 在手动调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间(MaxGCPauseMills),让虚拟机自己完成调优工作。

6. CMS回收器:低延迟

  1. 在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器:CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
  2. CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合于用户交互的程序,良好的响应速度能提升用户体验。
    1. 目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
    2. CMS的垃圾收集算法采用标记-清除算法,并且也会“Stop-The-World”
  3. 不幸的是,CMS作为老年代的收集器,却无法与JDK1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以
  4. JDK1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。
  5. 在G1出现之前,CMS使用还是非常广泛的。一直到今天,仍然有很多系统使用CMS GC。

6.1 CMS的工作原理


初始标记,重新标记都是Stop The World。
CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段,并发标记阶段、重新标记阶段和并发清除阶段。

  1. 初始标记(initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-The-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
  2. 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  3. 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行。因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短(STW)。
  4. 并发清除(Concurrent-Sweep)阶段(只会清除无用对象,不会压缩整理内存):此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

6.2 详细理解CMS回收器:低延迟

  1. 尽管CMS收集器采用的是并发回收(非独占式),但是在器初始化标记和再次标记这两个阶段中仍然需要执行“Stop-The-World”机制暂停程序中的工作线程,不过暂停时间并不会太长,因此可以说明目前所有的垃圾收集器都做不到完全不需要“Stop The World”,只是尽可能的缩短暂停时间。
  2. 由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收时低停顿的
  3. 另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代集合完全被填满了再进行收集,而是当堆内存使用率达到某一阈值是,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
  4. CMS收集器的垃圾收集算法采用的是标记-清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。
  5. 有人会觉得既然Mark Sweep会造成内存碎片,那么为什么不把算法换成Mark Compact呢?
    答案其实很简单,因为当并发清除的时候,用Compact整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提得它运行的资源不受影响嘛。Mark Compact更适合“Stop The World”这种场景下使用。

6.3 CMS的优点

  1. 并发收集
  2. 低延迟

6.4 CMS的弊端

  1. 产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得提前触发Full GC。
  2. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程`而导致应用程序变慢,总吞吐量会降低。
  3. CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生, 在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被即时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

6.5 CMS收集器可以设置的参数

  1. -XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务。

    • 开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区用)+CMS(old区用)+Serial Old的组合。
  2. -XX:CMSlnitialtingOccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
    • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%。
    • 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。

6.6 总结

  1. HotSpot有这么多的垃圾回收器,那么如果有人问,Serial GC、Parrallel GC、Concurrent Mark Sweep GC这三个GC有什么不同呢?
    请记住以下口令:

    • 如果你想要最小化地使用内存和并行开销,请选Serial GC。
    • 如果你想要最大化应用程序的吞吐量,请选择Parallel GC。
    • 如果你想要最小化GC的中断或停顿时间,请选CMS GC。

7. G1回收器:区域化分代式

  1. 既然我们已经有了前面几个强大的GC,为什么还要发布Garbage First(G1)GC?

    • 原因就在于应用程序所应对的业务越来越庞大、复杂、用户越来越多,没有GC就不能保证应用程序正常进行,而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。G1(Garbage-First)垃圾回收器是在Java7 update 4之后引人的一个新的垃圾回收器,是当今收集器技术发展最前沿成果之一。
    • 与此同时,为了适应限制不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间(Pause Time),同时兼顾良好的吞吐量。
    • 官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,所以才担当起“全功能收集器”的重任与期望

7.1 G1名字的由来

  1. 因为G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续的)。使用不同的Region来表示Eden、幸存者0区,幸存者1区,老年代等。
  2. G1 GC有机会地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾对接的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。
  3. 由于这种方式的侧重点在于回收垃圾最大量的区间(Region),所以我们给G1一个名字:垃圾优先(Garbage First)。
  4. G1(Garbage-First)是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足GC停顿时间的同时,还兼具高吞吐量的性能特征。
  5. 在JDK1.7版本正式启用,移除了Experimental的标识,是JDK9以后的默认垃圾回收器,取代了CMS回收器以及Parallel + Parallel Old组合。被Oracle官方称为“全功能的垃圾收集器”。
  6. 以此同时,CMS已经在JDK9中被标记为废弃(deprecated)。在Jdk8中还不是默认的垃圾回收器,需要使用-XX:UseG1GC来启用

7.2 G1回收器的特点(优势)

  1. 并行与并发

    • 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力。此时用户线程STW。
    • 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况
  2. 分代收集

    • 从分代上看,G1依然属于分代型垃圾回收器,它会区分年轻代和老年代,年轻代依然有Eden区和Survivor区。但从堆的结构上看,它不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
    • 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代
    • 和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代,或者工作在老年代
  3. 空间整合

    • CMS:“标记-清除”算法、内存碎片、若干次GC后进行一次碎片整理。
    • G1将内存划分为一个个的region。内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作是标记-压缩(Mark-Compact)算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
  4. 可预测的停顿时间模型(即:软实时soft real-time)
    这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

    • 由于分区的原因,G1可以只选择部分区域进行内存回收,这样缩小了回收的范围,因此对于全局停顿情况的发生也能得到较好的控制。
    • G1跟踪各个region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
    • 相比于CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但是最差情况要好很多。

7.3 缺点

  1. 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
  2. 从经验上来说,在小内存应用上CMS的表现大概率会优于G1,而G1在大内存应用上则发挥其优势。平衡点在6-8GB之间(6-8GB,G1和CMS表现相当)。

7.4 G1回收器的参数设置

7.5 G1回收器的常见操作步骤

  1. G1的设计原则就是简化JVM性能调优,开发人员只需要简单的三步即可完成调优:

    • 第一步:开启G1垃圾收集器。
    • 第二步:设置堆的最大内存。
    • 第三步:设置最大的停顿时间。
  2. G1中提供了三种垃圾回收模式:YongGC、Mixed GC和Full GC,在不同的条件下被触发。

7.6 G1回收器的适用场景

  1. 面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)
  2. 最主要的应用是需要低GC延迟,并具有大堆的应用程序提供解决方案;
  3. 如果:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒;(G1通过每次只清理一部分而不是全部的Region的增量式清理来保证每次GC停顿时间不会太长)。
  4. 用来替换掉JDK1.5中的CMS收集器:(在下面的情况时,使用G1可能比CMS好:)
    • 超过50%的Java堆被活动数据占用;
    • 对象分配频率或年代提升频率变化很大;
    • GC停顿时间过长(长于0.5至1秒);
  5. HotSpot垃圾收集器里,除了G1以外,其它的垃圾收集器使用内置的JVM线程执行GC的多线程操作,而G1 GC可以采用应用线程承担后台运行的GC工作,即当JVM的GC线程处理速度慢时,系统会调用应用程序线程帮助加速垃圾回收过程。

7.7 分区Region:化整为零

  1. 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB,4MB,8MB,16MB,32MB。可以通过-XX:G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会改变。
  2. 虽然还保留在新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。通过Region的动态分配方式实现逻辑上的连续。
  3. 一个region有可能属于Eden,Survivor或者Old/Tenured内存区域。但是一个region只能属于一个角色。图中E表示该region属于Eden内存区域,S表示属于Survivor内存区域,O表示属于Old内存区域。图中空白的表示未使用的内存空间。
  4. G1垃圾收集器还增加了一种新的内存区域,叫做Humongous内存区域,如图中的H块。主要用于存储大对象,如果超过1.5个region,就放到H。
  5. 设置H的原因:
    • 对于堆中的大对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象,就会对垃圾收集器造成负面影响。为了解决这个问题,G1划分了一个Humongous区,它用来专门存放大对象。如果一个H区装不下一个大对象,那么G1会寻找连续的H区来存储。为了能找到连续的H区,有时候不得不启动Full GC。G1的大多数行为都把H区作为老年代的一部分来看待。

7.8 G1回收器垃圾回收过程

7.8.1 概述

  1. G1 GC的垃圾回收过程主要包括如下三个环节:

    1. 年轻代GC(Young GC)
    2. 老年代并发标记过程(Concurrent Marking)
    3. 混合回收(Mixed GC):包含年轻代和老年代回收。
    4. (如果需要,单线程、独占式、高强度的Full GC还是继续存在的。它针对GC的评估失败提供了一种失败保护机制,即强力回收。)

7.8.2 详解

  1. 应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程:G1的年轻代收集阶段是一个并行的独占式收集器。在年轻代回收期,G1 GC暂停所有应用程序线程,启动多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年代区间,也有可能是两个区间都会涉及。
  2. 当堆内存使用到的一定值(默认45%)时,开始老年代并发标记过程。
  3. 标记完成马上开始混合回收过程。对于一个混合回收期,G1 GC从老年区间移动存活对象到空闲区间,这些空闲区间也就成为了老年代的一部分。和年轻代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的Region就可以了。同时,这个老年代Region是和年轻代一起被回收的。
  4. 举个例子:一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
  5. 一个对象被不同区域引用的问题。
  6. 一个Region不可能是孤立的,一个Region中的对象可能被其他任意Region中对象引用,判断对象存活时,是否需要扫描整个Java堆才能保证准确?
  7. 在其他的分代收集器,也存在这样的问题(而G1更突出)
  8. 回收新生代也不得不同时扫描老年代?
  9. 这样的话会降低Minor GC的效率。
  10. 解决方法:
    • 无论G1还是其他分代收集器,JVM都是使用Remembered Set来避免全局扫描
    • 每个Region都有一个对应的Remembered Set;
    • 每次Reference类型数据写操作时,都会产生一个Write Barrier(写屏障)暂时中断操作;
    • 然后检查将要写入的引用指向的对象是否和该Reference类型数据在不同的Region(其他收集器:检查老年代对象是否引用了新生代对象);
    • 如果不同,通过CardTable把相关引用信息记录到引用指向对象的所在Region对于的Remembered Set中
    • 当进行垃圾收集时,在GC根节点的枚举范围加入Remembered Set;就可以保证不进行全局扫描,也不会有遗漏。

7.8.3 年轻代GC

  1. JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当Eden空间耗尽,G1会启动一次年轻代垃圾回收过程。
  2. 年轻代垃圾回收只会回收Eden区和Survivor区。
  3. YGC时,首先G1停止应用程序的执行(Stop The World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。
  4. 回收过程
    • 第一阶段,扫描根·。根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RSet记录的外部引用作为扫描存活对象的入口。
    • 第二阶段,更新RSet。处理dirty card queue中的card,更新RSet。此阶段完成后,RSet可以准确的反映老年代对所在的内存分段中对象的引用。
    • 第三阶段,处理RSet。识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
    • 第四阶段,复制对象。此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阈值会被复制到Old区中空的内存分段。如果Survivor空间不够,Eden空间的部分数据会直接晋升到老年代空间。
    • 第五阶段,处理引用。处理Soft,Weak,Phantom,Final,JNI Weak等引用。最终Eden空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片。

7.8.4 并发标记过程

  1. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
  2. 根区域扫描(Root Region Scanning):G1 GC扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在young GC之前完成。
  3. 并发标记(Concurrent Marking):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。并发标记阶段,若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
  4. 再次标记(Remark):由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning(SATB)。
  5. 独占清理(cleanup,STW):计算各个区域的存活对象和GC回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫,是STW的。
    1. 这个阶段并不会实际上区做垃圾的收集。
  6. 并发清理阶段:识别并清理完全空闲的区域。

7.8.5 混合回收

  1. 并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCountTarget设置)被回收。
  2. 混合回收的回收集包括八分之一的老年代分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是会收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
  3. 由于老年代的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会划分更多的时间。
  4. 混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占对内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。

7.8.6 可选的回收过程:FUll GC

  1. G1的初衷就是要避免Full GC的出现。但是如果上述方式不能正常工作,G1会停止应用程序的执行(Stop the world),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
  2. 要避免Full GC的发生,一旦发生需要进行调整。什么时候会发生Full GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可以使用,则会回退到full gc,这种情况可以通过增大内存解决。
  3. 导致G1 Full GC的原因可能有两个:
    • Evacuation的时候没有足够的to-space来存放晋升的对象;
    • 并发处理过程完成之前空间耗尽。

7.9 G1回收过程:补充

从Oracle官方透露出来的信息可获知,回收阶段(Evacuation)其实本也有想过设计成与用户程序一起并发执行,但这件事情做起来比较复杂,考虑到G1只是回收一部分Region,停顿时间是用户可控制的,所以并不迫切区实行看,而选择把这个特性放到了G1之后出现的低延迟垃圾收集器(即ZGC)中。另外,还考虑到G1不是仅仅面向低延迟,停顿用户线程能够最大幅度提高垃圾收集效率,为了保证吞吐量所以才选择了完全暂停用户线程的实现方案。

7.10 G1回收器优化建议

  1. 年轻大大小

    • 避免使用-Xmn或-XX:NewRation等相关选项显示设置年轻代大小。
    • 固定年轻代的大小会覆盖暂停时间目标。
  2. 暂停时间目标不要太过严苛
    • G1 GC的吞吐量目标是90%的应用程序时间和10%的垃圾回收时间。\
    • 评估G1 GC的吞吐量时,暂停时间目标不要太严苛。目标太过严苛表示你愿意承受更的垃圾回收开销,而这些会直接影响到吞吐量

8 . 垃圾回收器总结



9. GC日志分析

  1. 概述

    • 通过阅读GC日志,我们可以了解java虚拟机内存分配与回收策略。
    • 内存分配与垃圾回收的参数列表
    • -XX:+PrintGC 输出GC日志。类似:-verbose:gc
    • -XX:+PrintGCDetails 输出GC的详细日志
    • -XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
    • -XX:PrintGCDateStamps 输出GC的时间戳(以日期的形式,如2013-05-04T21:53:59.234+0800)
    • -XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
    • -Xloggc:…/logs/gc.log 日志文件的输出路径
  2. 日志分析
    • 打开GC日志 -verbose:gc
    • 这个只会显示总的GC堆的变化,如下:
    • 参数解析





Minor GC 日志

full Gc

JVM-垃圾回收GC相关推荐

  1. jvm - 垃圾回收 gc

    2019独角兽企业重金招聘Python工程师标准>>> jvm - 垃圾回收 注意 : 本系列文章为学习系列,部分内容会取自相关书籍或者网络资源,在文章中间和末尾处会有标注 垃圾回收 ...

  2. JVM垃圾回收(GC)

    如何识别垃圾 垃圾回收主要方法 分代收集算法 垃圾收集器 JVM参数 测试 如何识别垃圾 引用计数法 对象被引用一次,在它的对象头上加一次引用次数,如果没有被引用(引用次数为 0),则此对象可回收 代 ...

  3. JVM—垃圾回收GC算法

    1 GC算法简介 算法 特点 标记-清除 分为"标记"和"清除"两个阶段 复制 可以解决效率问题,将可用的内存按容量划分为大小相等的两块. 标记-整理 先标记. ...

  4. java学习笔记-4 JVM垃圾回收(GC)

    引言 jvm垃圾回收相关的问题是老生常谈的问题了,相信大家都有所了解,这里再进行相关的探讨,以加深理解.若文中有不正之言,望不吝指正. 本文将围绕以下几个点展开 1.为什么要进行垃圾回收 我们知道jv ...

  5. 4、JVM垃圾回收机制、新生代的GC、GC(Minor GC、FullGC)、GC日志、JVM参数选项、元空间(笔记)

    4.JVM垃圾回收机制 4.1.新生代的GC 4.1.1.串行GC(SerialGC) 4.1.2.并行回收GC(Parallel Scavenge) 4.1.3.并行GC(ParNew) 4.2.G ...

  6. jvm gc垃圾回收机制和参数说明amp;amp;Java JVM 垃圾回收(GC 在什么时候,对什么东西,做了什么事情)

    jvm gc(垃圾回收机制) Java JVM  垃圾回收(GC 在什么时候,对什么东西,做了什么事情) 前言:(先大概了解一下整个过程) 作者:知乎用户 链接:https://www.zhihu.c ...

  7. JVM内存区域(Java内存区域)、JVM垃圾回收机制(GC)初探

    一.JVM内存区域(Java内存区域) 首先区分一下JVM内存区域(Java内存区域)和Java内存模型(JMM)的概念.Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Ja ...

  8. 【JVM】JVM垃圾回收机制GC

    文章目录 JVM垃圾回收机制 一.堆内存区域划分 1.1内存分配策略 1.2永久代(Permanent Generation) 1.3元空间(MetaSpace) 二.标记算法 2.1引用计数算法 2 ...

  9. JVM垃圾回收机制GC理解

    目录 JVM垃圾回收 分代收集 如何识别垃圾 引用计数法 可达性分析法 引用关系四种类型: 强.软.弱.虚 强引用 软引用 SoftReference 弱引用 WeakReference WeakHa ...

  10. 老李分享:jvm垃圾回收

    老李分享:jvm垃圾回收 1.垃圾收集算法核心思想 java语言建立了垃圾回收机制,用于跟踪正在被使用(引用)的对象和没有被使用(引用)的对象,该机制可以有效防范动态内存分配中可能发生的两个危险:因垃 ...

最新文章

  1. OpenStack服务组件介绍
  2. 嘿,是时候重新认识下海淘了
  3. SpringBoot接口参数校验
  4. mac python安装太慢_【已解决】Mac中给pip3添加代理以提升下载python包的速度
  5. Science杂志公布的机器学习资源
  6. 关于Layer UI表格列日期格式化及取消自动填充日期
  7. t32 emulation debug port failed
  8. 一些技能点语法糖(上)
  9. docker 部署nginx,挂载nginx.conf
  10. 使用UIDataDetectorTypes自动检测电话、网址和邮箱
  11. 大数据技术与应用4-4MapRuduce
  12. 家里电脑总是显示宽带连接服务器,电脑宽带连接
  13. Bartender打印
  14. Python量化交易04——基于机器学习的交易策略
  15. 20200417-SiC+移相全桥文献
  16. 微信小程序开发加载html富文本数据
  17. 模板 书店_去书店,感受一场戏剧派对
  18. 数据库 DB database
  19. 自动化测试和性能测试哪个有前途?
  20. JAVA_JSP超市管理系统

热门文章

  1. 开发者必备工具-掘金Chrome插件
  2. php图像处理类实现缩放 裁剪 加水印,ThinkPHP图像的裁剪、缩放、加水印
  3. 《“索卡尔事件”与科学大战》
  4. SecurityError: localStorage is not available for opaque orig
  5. 百度影音小偷PHP版 v2.0
  6. python 爬虫爬取腾讯新闻科技类的企鹅智酷系列(1)
  7. 常用px, pt, em 换算表
  8. 计算机视觉-相机内参数和外参数
  9. 配置MyBatis错误Cannot load connection class because of underlying exception: com.mysql.cj.exceptions..
  10. 织梦添加图片变量_织梦添加新变量和删除新变量的方法