目录
1、Serial(-XX:+UseSerialGC) ——串行收集器。
2、SerialOld(-XX:+UseSerialGC) —— Serial 收集器的老年代收集器版本,
4、ParallelScavenge(-XX:+UseParallelGC)
5、ParallelOld(-XX:+UseParallelOldGC) ——并行收集器,
6、CMS (-XX:+UseConcMarkSweepGC)
在 Java 中可以作为 GC Roots 的对象有以下几种:
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
简述 Java 内存分配与回收策率以及 Minor GC 和 Major GC。
System.gc() 和 Runtime.gc() 会做什么事情?
finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?
如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?
GC 是什么? 为什么要有 GC?
GC 是垃圾收集的意思(GabageCollection),内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。
在 Java 程序中不能显式的分配和注销缓存,因为这些事情 JVM 都帮我们做了, 那就是 GC。
人工显示GC:有些时候我们可以将相关的对象设置成 null 来试图显示的清除缓存,但是并不是设置为 null 就会一定被标记为可回收,有可能会发生逃逸。 将对象设置成 null 至少没有什么坏处,但是使用 System.gc() 便不可取了,使用 System.gc() 时候并不是马上执行 GC 操作,而是会等待一段时间,甚至不执行, 而且 System.gc() 如果被执行,会触发 Full GC ,这非常影响性能。
垃圾回收的优点和原理。并考虑 2 种回收机制。
Java 语言中一个显著的特点就是引入了垃圾回收机制,使 C++ 程序员最头疼的内存管理的问题迎刃而解,它使得 Java 程序员在 编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制, Java 中的对象不再有“作用域”的概念,只有对象的引用才有" 作用域"。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。
垃圾回收器通常是作为一个单独的低级别的线程运行, 不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清楚和回收,程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。
回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。
在 Java 中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在 JVM 中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象, 并将它们添加到要回收的集合中,进行回收。
stop-the-world 。它会在任何一 种 GC 算法中发生。stop-the-world 意味着 JVM 因为需要执行 GC 而停止了应用 程序的执行。当 stop-the-world 发生时,除 GC 所需的线程外,所有的线程都进 入等待状态,直到 GC 任务完成。GC 优化很多时候就是减少 stop-the-world 的发 生。
JVM GC 回收哪个区域内的垃圾?需要注意的是,JVM GC 只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被 JVM 自动释放掉,所以其不在 JVM GC 的管理范围内。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值, 会触发完全垃圾回收(Full GC)。
注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的 native 内存区。
- 对象没有引用
- 作用域发生未捕获异常
- 程序在作用域正常执行完毕
- 程序执行了 System.exit()
- 程序发生意外终止(被杀线程等)
判断一个对象是否存活有两种方法:
1. 引用计数法 :给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用, 也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对 象 A 引用对象 B,对象 B 又引用者对象 A,那么此时 A、B 对 象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流 的虚拟机都没有采用这种算法。
2. 可达性算法(引用链法) 该算法的思想是:从一个被称为 GC Roots 的对象开始向下搜索, 如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,这个对象并不一定会被回收。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记.
如果对象在可达性分析中没有与 GC Root 的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法或者已被虚拟机调用过,那么就认为是没必要的。 如果该对象有必要执行 finalize() 方法,那么这个对象将会放在一个称为 F-Queue 的对队列中,虚拟机会触发一个 Finalize() 线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果 finalize() 执行缓慢或者发生了死锁,那么就会造成 F-Queue 队列一直等待,造成了内存回收系统的崩溃。GC 对处于 F-Queue 中的对象进行第二次被标记,这时,该对象将被移除” 即将回收” 集合,等待回收。
eden 区空间不够存放新对象的时候,执行 Minro GC。升到老年代的对象大于老年代剩余空间的时候执行 Full GC,或者小于的时候被 HandlePromotionFailure 参数强制 Full GC 。调优主要是减少 Full GC 的触发次数,可以通过 NewRatio 控制新生代转老年代的比例,通过 MaxTenuringThreshold 设置对象进入老年代的 年龄阀值(后面会介绍到)。
按代的垃圾回收机制
1. 新生代(Young generation):绝大多数最新被创建的对象都会被分配到这里, 由于大部分在创建后很快变得不可达,很多对象被创建在新生代,然后“消失”。 对象从这个区域“消失”的过程我们称之为:Minor GC 。
2. 老年代(Old generation):对象没有变得不可达,并且从新生代周期中存活了 下来,会被拷贝到这里。其区域分配的空间要比新生代多。也正由于其相对大的 空间,发生在老年代的 GC 次数要比新生代少得多。对象从老年代中消失的过程, 称之为:Major GC 或者 Full GC。
3. 持久代(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 带 来了很大的性能提升,虽然由此可能带来一些开销,但完全是值得的。
默认的新生代(Young generation)、老年代(Old generation)所占空间比例为 1 : 2 。
新生代空间的构成与逻辑
为了更好的理解 GC,我们来学习新生代的构成,它用来保存那些第一次被创建的对象,它被分成三个空间:
· 一个伊甸园空间(Eden)
· 两个幸存者空间(Fron Survivor、To Survivor)
默认新生代空间的分配:Eden : Fron : To = 8 : 1 : 1
每个空间的执行顺序如下:
1、绝大多数刚刚被创建的对象会存放在伊甸园空间(Eden)。
2、在伊甸园空间执行第一次 GC(Minor GC)之后,存活的对象被移动到其中一 个幸存者空间(Survivor)。
3、此后,每次伊甸园空间执行 GC 后,存活的对象会被堆积在同一个幸存者空 间。
4、当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。然后会清空已经饱和的那个幸存者空间。
5、在以上步骤中重复 N 次( N = MaxTenuringThreshold(年龄阀值设定,默认 15)) 依然存活的对象,就会被移动到老年代。
从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。如果两个幸存者空间都有数据,或两个空间都是空的,那一定是你的系统出现了某种错误。
我们需要重点记住的是,对象在刚刚被创建之后,是保存在伊甸园空间的 (Eden)。那些长期存活的对象会经由幸存者空间(Survivor)转存到老年代空 间(Old generation)。 也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间) 则直接进入到老年代。一般在 Survivor 空间不足的情况下发生。
老年代空间的构成与逻辑
老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有 一个区域,里面存储的对象并不像新生代空间绝大部分都是朝闻道,夕死矣。这 里的对象几乎都是从 Survivor 空间中熬过来的,它们绝不会轻易的狗带。因此,
Full GC(Major GC)发生的次数不会有 Minor GC 那么频繁,并且做一次 Major GC 的时间比 Minor GC 要更长(约 10 倍)。
1、根搜索算法
根搜索算法是从离散数学中的图论引入的,程序把所有引用关系看作一张图,从 一个节点 GC ROOT 开始,寻找对应的引用节点,找到这个节点后,继续寻找这个节点的引用节点。当所有的引用节点寻找完毕后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
上图红色为无用的节点,可以被回收。
目前 Java 中可以作为 GC ROOT 的对象有:
1、虚拟机栈中引用的对象(本地变量表)
2、方法区中静态属性引用的对象
3、方法区中常量引用的对象
4、本地方法栈中引用的对象(Native 对象)
基本所有 GC 算法都引用根搜索算法这种概念。
2、标记 - 清除算法
标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后, 再扫描整个空间中未被标记的对象进行直接回收,如上图。
缺点:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对 象,并没有对还存活的对象进行整理,因此会导致内存碎片。
3、复制算法
复制算法将内存划分为两个区间,使用此算法时,所有动态分配的对象都只能分配在其中一 个区间(活动区间),而另外一个区间(空间区间)则是空闲的。 复制算法采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会的将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。 下次 GC 时候又会重复刚才的操作,以此循环。
适用场景:复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服 50%内存的浪费。
4、标记 - 整理算法
标记-整理算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。标记-整理 算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。
JVM 为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收 (Minor GC)主要采用复制算法。而对于老年代的回收(Major GC),大多采用 标记-整理算法。
需要注意的是,每一个回收器都存在 Stop The World 的问题,只不过各个回收器在 Stop The World 时间优化程度、算法的不同,可根据自身需求选择适合的回 收器。
新生代和老年代的收集器。
新生代收集器: 复制算法
- Serial (-XX:+UseSerialGC)串行收集器
- ParNew(-XX:+UseParNewGC) Serial 收集器的多线程版本, Server 模式
- ParallelScavenge(-XX:+UseParallelGC) ——吞吐量优先收集器
- G1 收集器 ——重新划分内存区域,整合优化 CMS
老年代收集器: 标记 - 整理算法
- SerialOld(-XX:+UseSerialOldGC) ——Client 模式下
- ParallelOld(-XX:+UseParallelOldGC) ——并行收集器
- CMS(-XX:+UseConcMarkSweepGC) ——两次短暂的暂停来 代替 串行或并行标记整理算法时候 的长暂停
- G1 收集器
1、Serial(-XX:+UseSerialGC) ——串行收集器。
Serial 收集器是 Java 虚拟机中最基本、历史最悠久的收集器。在 JDK1.3 之前是Java 虚拟机新生代收集器的唯一选择。目前也是 ClientVM 下 ServerVM 4 核 4GB 以下 机器默认垃圾回收器。Serial 收集器并不是只能使用一个 CPU 进行收集,而是当 JVM 需要进行垃圾回收的时候,需暂停所有的用户线程,直到回收结束。
使用算法:复制算法
适用场景:Serial 收集器虽然是最老的,但是它对于限定单个 CPU 的环境来说,由于没有线程交互的开销,专心做垃圾收集,所以它在这种情况下是相对于其他收集器中最 高效的。
2、SerialOld(-XX:+UseSerialGC) —— Serial 收集器的老年代收集器版本,
它同样是一个单线程收集器,这 个收集器目前主要用于 Client 模式下使用。如果在 Server 模式下,它主要还有两 大用途:一个是在 JDK1.5 及之前的版本中与 Parallel Scavenge 收集器搭配使用, 另外一个就是作为 CMS 收集器的后备预案,如果 CMS 出现 Concurrent Mode Failure,则 SerialOld 将作为后备收集器。
使用算法:标记 - 整理算法 ,老年代收集器
3、ParNew(-XX:+UseParNewGC)
ParNew 其实就是 Serial 收集器的多线程版本。除了 Serial 收集器外,只有它能与 CMS 收集器配合工作。
使用算法:复制算法 ,新生代收集器
ParNew 是许多运行在 Server 模式下的 JVM 首选的新生代收集器。但是在单 CPU 的情况下,它的效率远远低于 Serial 收集器,所以一定要注意使用场景。
4、ParallelScavenge(-XX:+UseParallelGC)
ParallelScavenge 又被称为吞吐量优先收集器,和 ParNew 收集器类似,是一个新生代收集器。
使用算法:复制算法 ,新生代收集器
ParallelScavenge 收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即吞吐量 = 运行用户代码 时间 / (运行用户代码时间 + 垃圾收集时间)。如果虚拟机总共运行了 100 分 钟,其中垃圾收集花了 1 分钟,那么吞吐量就是 99% 。
5、ParallelOld(-XX:+UseParallelOldGC) ——并行收集器,
这个收集器在 JDK1.6 之后才开始提供的, 在此之前,ParallelScavenge 只能选择 SerialOld 来作为其老年代的收集器,这严 重拖累了 ParallelScavenge 整体的速度。而 ParallelOld 的出现后,“吞吐量优先” 收集器才名副其实!
使用算法:标记 - 整理算法 ,老年代收集器
在注重吞吐量与 CPU 数量大于 1 的情况下,都可以优先考虑 ParallelScavenge + ParalleloOld 收集器。
6、CMS (-XX:+UseConcMarkSweepGC)
CMS 是一个老年代收集器,全称 Concurrent Low Pause Collector,是 JDK1.4 后期开始引用的新 GC 收集器,在 JDK1.5、1.6 中得到了进一步的改进。它是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的情况下,使用 CMS 非常合适。
CMS 的一大特点,就是用两次短暂的暂停来 代替 串行或并行标记整理算法时候 的长暂停。
使用算法:标记 - 清理
CMS 的执行过程如下:
- 初始标记(STW initial mark) :在这个阶段,需要虚拟机停顿正在执行的应用线程,官方的叫法 STW(Stop Tow World)。这个过程从根对象扫描直接关联的对象,并作标记。这个过程会很快的完成。
- 并发标记(Concurrent marking) :这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程可以和 GC 线程一起并发执行,这个阶段不会暂停用户的线程哦。
- 并发预清理(Concurrent precleaning) :这个阶段仍然是并发的,JVM 查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象从新生代晋升到老年代,或被分配到老年代)。通过重新扫描,减少在一个阶段“重新标记”的工作,因为下一阶段会 STW。
- 重新标记(STW remark) :这个阶段会再次暂停正在执行的应用线程,重新对根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新导致),并处理对象关联。 这一次耗时会比“初始标记”更长,并且这个阶段可以并行标记。
- 并发清理(Concurrent sweeping) :这个阶段是并发的,应用线程和 GC 清除线程可以一起并发执行。
- 并发重置(Concurrent reset) :这个阶段任然是并发的,重置 CMS 收集器的数据结构,等待下一次垃圾回收。
CMS 的缺点:
1、内存碎片。由于使用了 标记-清理 算法,导致内存空间中会产生内存碎片。 不过 CMS 收集器做了一些小的优化,就是把未分配的空间汇总成一个列表,当有 JVM 需要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储 这个对象。但是内存碎片的问题依然存在,如果一个对象需要 3 块连续的空间来存储,因为内存碎片的原因,寻找不到这样的空间,就会导致 Full GC。
2、需要更多的 CPU 资源。由于使用了并发处理,很多情况下都是 GC 线程和应 用线程并发执行的,这样就需要占用更多的 CPU 资源,也是牺牲了一定吞吐量 的原因。
3、需要更大的堆空间。因为 CMS 标记阶段应用程序的线程还是执行的,那么就 会有堆空间继续分配的问题,为了保障 CMS 在回收堆空间之前还有空间分配给 新加入的对象,必须预留一部分空间。CMS 默认在老年代空间使用 68%时候启动 垃圾回收。可以通过-XX:CMSinitiatingOccupancyFraction=n 来设置这个阀值。
7、GarbageFirst(G1)
这是一个新的垃圾回收器,既可以回收新生代也可以回收老年代, SunHotSpot1.6u14 以上 EarlyAccess 版本加入了这个回收器,Sun 公司预期 SunHotSpot1.7 发布正式版本。通过重新划分内存区域,整合优化 CMS,同时注 重吞吐量和响应时间。杯具的是 Oracle 收购这个收集器之后将其用于商用收费 版收集器。因此目前暂时没有发现哪个公司使用它,这个放在之后再去研究吧。
在 Java 中可以作为 GC Roots 的对象有以下几种:- 虚拟机栈中引用的对象
- 方法区类静态属性引用的对象
- 方法区常量池引用的对象
- 本地方法栈引用的对象
垃圾回收器的基本原理是什么?垃圾回收器可以马上回收内存吗?有什么办法主动通知虚拟机进行垃圾回收?
- 对于 GC 来说,当程序员创建对象时,GC 就开始监控这个对象 的地址、大小以及使用情况。通常,GC 采用有向图的方式记录和 管理堆(heap)中的所有对象。通过这种方式确定哪些对象是” 可达的”,哪些对象是”不可达的”。当 GC 确定一些对象为“不 可达”时,GC 就有责任回收这些内存空间。
- 可以。
- 程序员可以手动执行 System.gc(),通知 GC 运行,但是 Java 语言规范并不 保证 GC 一定会执行。
- 对象优先在堆的 Eden 区分配
- 大对象直接进入老年代
- 长期存活的对象将直接进入老年代
- 当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次 Minor GC。Minor GC 通常发生在新生代的 Eden 区,在这个区的对象生存期短,往往发生 Gc 的频率较高,回收速度比较快;
- Full GC/Major GC 发生在老年代,一般情况下,触发老年代 GC 的时候不会触发 Minor GC,但是通过配置,可以在 Full GC 之 前进行一次 Minor GC 这样可以加快老年代的回收速度。
System.gc() 和 Runtime.gc() 会做什么事情?
这两个方法用来提示 JVM 要进行垃圾回收。但是,立即开始还是 延迟进行垃圾回收是取决于 JVM 的。
finalize() 方法什么时候被调用?析构函数 (finalization) 的目的是什么?垃圾回收器(garbage colector)决定回收某对象时,就会运行该 对象的 finalize() 方法 但是在 Java 中很不幸,如果内存总是充 足的,那么垃圾回收可能永远不会进行,也就是说 filalize() 可能 永远不被执行,显然指望它做收尾工作是靠不住的。 那么 finalize() 究竟是做什么的呢? 它最主要的用途是回收特殊渠道 申请的内存。Java 程序有垃圾回收器,所以一般情况下内存问题 不用程序员操心。但有一种 JNI(Java Native Interface)调用 non-Java 程序(C 或 C++), finalize() 的工作就是回收这部 分的内存。
如果对象的引用被置为 null,垃圾收集器是否会立即释放对象占用的内存?不会,在下一个垃圾回收周期中,这个对象将是可被回收的。
什么是分布式垃圾回收(DGC)?它是如何工作的?DGC 叫做分布式垃圾回收。RMI 使用 DGC 来做自动垃圾回收。 因为 RMI 包含了跨虚拟机的远程对象的引用,垃圾回收是很困难 的。DGC 使用引用计数算法来给远程对象提供自动内存管理。
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?- 吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模 和大规模数据的应用程序。
- 串行收集器对大多数的小应用(在 现代处理器上需要大概 100M 左右的内存)就足够了
一些配置解释
-XX:+UseG1GC 使用 G1 (Garbage First) 垃圾收集器
-XX:MaxGCPauseMillis=n 设置最大GC停顿时间(GC pause time)指标(target). 这是一个软性指标(soft goal), JVM 会尽量去达成这个目标.
-XX:InitiatingHeapOccupancyPercent=n 启动并发GC周期时的堆内存占用百分比. G1之类的垃圾收集器用它来触发并发GC周期,基于整个堆的使用率,而不只是某一代内存的使用比. 值为 0 则表示"一直执行GC循环". 默认值为 45.
这个参数是一个常用的GC调优参数,上面的百分比控制的Minor GC的百分比
可以适当调低此参数,提高MinorGC的频次,从而延迟FullGC的到来
-XX:NewRatio=n 新生代与老生代(old/new generation)的大小比例(Ratio). 默认值为 2.
-XX:SurvivorRatio=n eden/survivor 空间大小的比例(Ratio). 默认值为 8.
-XX:MaxTenuringThreshold=n 提升老年代的最大临界值(tenuring threshold). 默认值为 15.
-XX:ParallelGCThreads=n 设置垃圾收集器在并行阶段使用的线程数,默认值随JVM运行的平台不同而不同.一般设置的数量=服务器的核数
-XX:ConcGCThreads=n 并发垃圾收集器使用的线程数量. 默认值随JVM运行的平台不同而不同(调节CMS)
-XX:G1ReservePercent=n 设置堆内存保留为假天花板的总量,以降低提升失败的可能性. 默认值是 10.
假天花板的概念:比如可用内存100GB,假天花板是10%,意味着G1最多能用90GB,预留出10GB,从而降低失败的可能性,
-XX:G1HeapRegionSize=n 使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解. 最小值为 1Mb, 最大值为 32Mb.