【JVM】垃圾收集器与内存分配策略

发布时间:2026/7/18 12:06:13

【JVM】垃圾收集器与内存分配策略 【JVM】垃圾收集器与内存分配策略一基本介绍二对象已死吗垃圾回收机制【1】引用计数算法主流不用【2】可达性分析算法【3】引用1-强引用StrongReference2-软引用SoftReference3-弱引用WeakReference4-虚引用PhantomReference【4】生存还是死亡不可达的对象并非“非死不可”【5】如何判断一个常量是废弃常量【6】如何判断一个类是无用的类三垃圾收集算法内存回收的方法论【1】标记-清除算法最基础其他算法对其不足进行改进【2】复制算法针对新生代死的多活的少【3】标记-整理算法针对老年区活的多空间小【4】分代收集算法四认识GC【1】Minor GC和Full GC触发条件【2】Minor GC 和 Full GC 有什么不一样吗【3】GC中Stop the worldSTW四HotSpot的算法实现4.1枚举根节点4.2安全点4.3安全区域五垃圾收集器内存回收的具体实现【1】Serial收集器单线程、新生代【2】ParNew收集器Serial的多线程、与CMS配合【3】Parallel Scavenge收集器多线程、新生代、复制算法【4】Serial Old收集器Serial老年代、单线程、标记整理算法【5】Parallel Old收集器Parallel老年代、多线程、标记整理算法【6】CMS收集器【7】G1收集器1介绍2特点3详细介绍4G1 收集的运作过程5G1 的 GC 模式可以分为两种【8】理解GC日志【9】垃圾收集器参数总结【1】查看 JVM 使用的默认垃圾收集器【2】设置运行的垃圾回收器类型【3】GC的优化配置命令【4】使用JVM GC 参数的例子六内存分配与回收策略【1】对象优先在Eden分配【2】大对象直接进入老年代【3】长期存活的对象将进入老年代【4】动态对象年龄判定【5】空间分配担保一基本介绍为什么要了解GC和内存分配垃圾收集GC当我们需要找出各种内存溢出、内存泄露问题时当垃圾收集成为系统达到更高并发量的阻碍时我们就不能任由其自动化处理了需要对其实施必要的监控和调节。就是说自动化处理出现了内存溢出等问题时就需要技术人员上了二对象已死吗垃圾回收机制堆中几乎放着所有的对象实例对堆垃圾回收前的第一步就是要判断那些对象已经死亡只有那些不能再被使用的对象才能判定为死亡死亡的对象才能被垃圾回收。【1】引用计数算法主流不用给对象添加一个引用计数器只要有一个地方引用了这个对象计数器就加1。同样当一个引用失效后计数器就减1,。这样通过引用计数器就可以统计当前对象被多少地方引用了当计数器为0时这个对象就是不可能再被使用的。引用计数算法实现简单效率高但是现在主流的虚拟机并没有选择这个算法来管理内存主要的原因就是这个方法很难解决对象之间相互循环引用的问题。什么是对象之间相互循环引用例如对象A和B互相引用但是它俩只有互相引用没有别的引用了又因为它俩互相引用所以计数器都不为0这样的话使用引用计数算法就不能通知GC回收器回收它们了。【2】可达性分析算法这个算法的基本思路就是用一系列的对象GC Roots作为起点从这些对象开始往下检索检索走过的路径叫“引用链”。当一个对象到 GC Roots 没有任何引用链相连的话则证明此对象是不可用的就可以判定是可回收的对象。可以作为GC Roots的对象虚拟机栈栈帧中的本地变量表里引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中JNINative方法引用的对象GC 管理的区域是 Java 堆虚拟机栈、方法区和本地方法栈不被 GC 所管理因此选用这些区域内引用的对象作为 GC Roots是不会被 GC 所回收的。其中虚拟机栈和本地方法栈都是线程私有的内存区域只要线程没有终止就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活因此也可能是 GC roots 的一部分。【3】引用无论是通过引用计数法判断对象引用数量还是通过可达性分析法判断对象的引用链是否可达判定对象的存活都与“引用”有关。JDK1.2 之前Java 中引用的定义很传统如果 reference 类型的数据存储的数值代表的是另一块内存的起始地址就称这块内存代表一个引用。JDK1.2 以后Java 对引用的概念进行了扩充将引用分为强引用、软引用、弱引用、虚引用四种引用强度逐渐减弱1-强引用StrongReference以前我们使用的大部分引用实际上都是强引用这是使用最普遍的引用。如果一个对象具有强引用那就类似于必不可少的生活用品垃圾回收器绝不会回收它。当内存空间不足Java 虚拟机宁愿抛出 OutOfMemoryError 错误使程序异常终止也不会靠随意回收具有强引用的对象来解决内存不足问题。2-软引用SoftReference如果一个对象只具有软引用那就类似于可有可无的生活用品。如果内存空间足够垃圾回收器就不会回收它如果内存空间不足了就会回收这些对象的内存。只要垃圾回收器没有回收它该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。软引用可以和一个引用队列ReferenceQueue联合使用如果软引用所引用的对象被垃圾回收JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。3-弱引用WeakReference如果一个对象只具有弱引用那就类似于可有可无的生活用品。弱引用与软引用的区别在于只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中一旦发现了只具有弱引用的对象不管当前内存空间足够与否都会回收它的内存。不过由于垃圾回收器是一个优先级很低的线程 因此不一定会很快发现那些只具有弱引用的对象。弱引用可以和一个引用队列ReferenceQueue联合使用如果弱引用所引用的对象被垃圾回收Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。4-虚引用PhantomReference虚引用顾名思义就是形同虚设与其他几种引用都不同虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用那么它就和没有任何引用一样在任何时候都可能被垃圾回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于 虚引用必须和引用队列ReferenceQueue联合使用。当垃圾回收器准备回收一个对象时如果发现它还有虚引用就会在回收对象的内存之前把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列那么就可以在所引用的对象的内存被回收之前采取必要的行动。特别注意在程序设计中一般很少使用弱引用与虚引用使用软引用的情况较多这是因为软引用可以加速 JVM 对垃圾内存的回收速度可以维护系统的运行安全防止内存溢出OutOfMemory等问题的产生。【4】生存还是死亡不可达的对象并非“非死不可”即使在可达性分析法中不可达的对象也并非是“非死不可”的这时候它们暂时处于“缓刑阶段”要真正宣告一个对象死亡至少要经历两次标记过程如果对象在进行可达性分析后发现没有与 GC Roots 相连接的引用链那它将会被第一次标记并且进行一次筛选筛选的条件是此对象是否有必要执行 finalize() 方法。当对象没有覆盖 finalize() 方法或者 finalize() 方法已经被虚拟机调用过虚拟机将这两种情况都视为“没有必要执行”直接进行第二次标记。如果这个对象被判定为有必要执行 finalize() 方法那么这个对象将会放置在一个叫做 F-Queue 的队列之中并在稍后由一个由虚拟机自动建立的、低优先级的 Finalizer 线程去执行它。被判定为需要执行的对象将会被放在一个队列中进行第二次标记除非这个对象与引用链上的任何一个对象建立关联否则就会被真的回收。这里所谓的“执行”是指虚拟机会触发这个方法但并不承诺会等待它运行结束因为如果一个对象在 finalize() 方法中执行缓慢将很可能会一直阻塞 F-Queue 队列甚至导致整个内存回收系统崩溃。值得注意的是使用 finalize() 方法来“拯救”对象是不值得提倡的它的运行代价高昂不确定性大无法保证各个对象的调用顺序。finalize() 能做的工作使用 try-finally 或者其它方法都更适合、及时。【5】如何判断一个常量是废弃常量运行时常量池主要回收的是废弃的常量。那么我们如何判断一个常量是废弃常量呢假如在常量池中存在字符串 “abc”但是当前系统没有任何一个 String 对象是叫做abc的也没有其他地方引用了这个字面量就说明常量 “abc” 就是废弃常量如果这时发生内存回收而且必要的话这个abc常量就会被系统清理出常量池。常量池中的其他类接口、方法、字段的符号引用也与此类似。【6】如何判断一个类是无用的类方法区主要回收的是无用的类那么如何判断一个类是无用的类的呢判定一个常量是否是“废弃常量”比较简单而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” 该类所有的实例都已经被回收也就是 Java 堆中不存在该类的任何实例。加载该类的 ClassLoader 已经被回收。该类对应的 java.lang.Class 对象没有在任何地方被引用无法在任何地方通过反射访问该类的方法。虚拟机可以对满足上述 3 个条件的无用类进行回收这里说的仅仅是“可以”而并不是和对象一样不使用了就会必然被回收。三垃圾收集算法内存回收的方法论【1】标记-清除算法最基础其他算法对其不足进行改进标记—清除算法是最基础的收集算法为了解决引用计数法的问题而提出。它使用了根集的概念它分为“标记”和“清除”两个阶段首先标记出所需回收的对象在标记完成后统一回收掉所有被标记的对象它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题1效率问题标记和清除过程的效率都不高。这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量。2空间问题标记清除后会产生大量不连续的碎片空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。优点不需要进行对象的移动并且仅对不存活的对象进行处理在存活对象比较多的情况下极为高效。【2】复制算法针对新生代死的多活的少为了解决效率问题“复制”收集算法出现了。它可以将内存分为大小相同的两块每次使用其中的一块。当这一块的内存使用完后就将还存活的对象复制到另一块去然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。复制算法比较适合于新生代短生存期的对象在老年代长生存期的对象中对象存活率比较高如果执行较多的复制操作效率将会变低所以老年代一般会选用其他算法如标记—整理算法。一种典型的基于Coping算法的垃圾回收是stop-and-copy算法它将堆分成对象区和空闲区在对象区与空闲区的切换过程中程序暂停执行。1清理的过程1、新生代的对象98%是朝生夕死的所以把内存分为一块较大的Eden空间和两块较小的Survivor空间每次使用Eden和其中一块Survivor。2、回收时把Eden和Survivor里还活着的对象一次性赋值到另外一块Survivor空间上。3、最后清理掉刚才用过的Eden和Survivor空间。2优点1、标记阶段和复制阶段可以同时进行。2、每次只对一块内存进行回收运行高效。3、只需移动栈顶指针按顺序分配内存即可实现简单。4、内存回收时不用考虑内存碎片的出现得活动对象所占的内存空间之间没有空闲间隔3缺点需要一块能容纳下所有存活对象的额外的内存空间。因此可一次性分配的最大内存缩小了一半。【3】标记-整理算法针对老年区活的多空间小复制算法在对象存活率较高时就要进行较多的复制操作效率将会变低。更关键的是如果不想浪费 50% 的空间就需要有额外的空间进行分配担保以应对被使用的内存中所有对象都 100% 存活的极端情况所以在老年代一般不能直接选用这种算法。根据老年代的特点提出的一种标记算法标记过程仍然与“标记-清除”算法一样但后续步骤不是直接对可回收对象回收而是让所有存活的对象向一端移动然后直接清理掉端边界以外的内存。1优点1、经过整理之后新对象的分配只需要通过指针碰撞便能完成Pointer Bumping相当简单。2、使用这种方法空闲区域的位置是始终可知的也不会再有碎片的问题了。2缺点GC暂停的时间会增长因为你需要将所有的对象都拷贝到一个新的地方还得更新它们的引用地址。【4】分代收集算法当前虚拟机的垃圾收集都采用分代收集算法这种算法没有什么新的思想只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中每次收集都会有大量对象死去所以可以选择复制算法只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的而且没有额外的空间对它进行分配担保所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。延伸面试问题 HotSpot 为什么要分为新生代和老年代答一般是把 Java 堆分为新生代和老年代这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每次垃圾收集时都发现有大批对象死去只有少量存活那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保就必须使用“标记—清理”或者“标记—整理”算法来进行回收。四认识GC【1】Minor GC和Full GC触发条件1-Minor GC触发条件当Eden区满时触发Minor GC。2-Full GC触发条件System.gc()方法的调用老年代空间不足方法区空间不足通过Minor GC后进入老年代的平均大小大于老年代的可用内存由Eden区、From Space区向To Space区复制时对象大小大于To Space可用内存则把该对象转存到老年代且老年代的可用内存小于该对象大小【2】Minor GC 和 Full GC 有什么不一样吗1-新生代 GCMinor GC指发生在新生代的垃圾收集动作因为 Java 对象大多都具备朝生夕灭的特性所以 Minor GC 非常频繁一般回收速度也比较快。2-老年代 GCMajor GC/Full GC指发生在老年代的 GC出现了 Major GC经常会伴随至少一次的 Minor GC但非绝对的在 Parallel Scavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程。Major GC 的速度一般会比 Minor GC 慢 10 倍以上。【3】GC中Stop the worldSTW垃圾回收首先是要经过标记的对象被标记后就会根据不同的区域采用不同的收集方法。垃圾回收并不会阻塞我们程序的线程他是与当前程序并发执行的。所以问题就出在这里当GC线程标记好了一个对象的时候此时我们程序的线程又将该对象重新加入了“关系网”中当执行二次标记的时候该对象也没有重写finalize()方法因此回收的时候就会回收这个不该回收的对象。 虚拟机的解决方法就是在一些特定指令位置设置一些“安全点”当程序运行到这些“安全点”的时候就会暂停所有当前运行的线程Stop The World 所以叫STW暂停后再找到“GC Roots”进行关系的组建进而执行标记和清除。这些特定的指令安全点位置主要在1-循环的末尾 2-方法临返回前 / 调用方法的call指令后 3-可能抛异常的位置停顿类型就是STW至于有GC和Full GC之分主要是Full GC时STW的时间相对GC来说时间很长因为Full GC针对整个堆以及永久代的因此整个GC的范围大大增加还有就是他的回收算法就是“标记–清除–整理”这里也会损耗一定的时间。所以我们在优化JVM的时候减少Full GC的次数也是经常用到的办法。四HotSpot的算法实现4.1枚举根节点4.2安全点4.3安全区域五垃圾收集器内存回收的具体实现如果说收集算法是内存回收的方法论那么垃圾收集器就是内存回收的具体实现。【1】Serial收集器单线程、新生代Serial串行收集器收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程 “Stop The World” 直到它收集结束。新生代采用复制算法老年代采用标记-整理算法。虚拟机的设计者们当然知道 Stop The World 带来的不良用户体验所以在后续的垃圾收集器设计中停顿时间在不断缩短仍然还有停顿寻找最优秀的垃圾收集器的过程仍然在继续。但是 Serial 收集器有没有优于其他垃圾收集器的地方呢当然有它简单而高效与其他收集器的单线程相比。Serial 收集器由于没有线程交互的开销自然可以获得很高的单线程收集效率。Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。通过JVM参数-XX:UseSerialGC可以使用串行垃圾回收器。【2】ParNew收集器Serial的多线程、与CMS配合ParNew 收集器其实就是 Serial 收集器的多线程版本除了使用多线程进行垃圾收集外其余行为控制参数、收集算法、回收策略等等和 Serial 收集器完全一样。并行收集器充分利用了多处理器的优势采用多个 GC 线程并行收集。可想而知多条 GC 线程执行显然比只使用一条 GC 线程执行的效率更高。一般来说与串行收集器相比在多处理器环境下工作的并行收集器能够极大地缩短 Stop-the-world 时间。ParNew 是针对新生代的垃圾回收器采用“复制”算法它是许多运行在 Server 模式下的虚拟机的首要选择除了 Serial 收集器外只有它能与 CMS 收集器真正意义上的并发收集器后面会介绍到配合工作。适用于多CPU、对暂停时间要求较短的应用上是server级别默认采用的GC方式。可用-XX:UseParallelGC来强制指定用-XX:ParallelGCThreads4来指定线程数。并行和并发概念补充并行Parallel 指多条垃圾收集线程并行工作但此时用户线程仍然处于等待状态并发Concurrent指用户线程与垃圾收集线程同时执行但不一定是并行可能会交替执行用户程序在继续运行而垃圾收集器运行在另一个 CPU 上。【3】Parallel Scavenge收集器多线程、新生代、复制算法Parallel Scavenge 收集器也是使用复制算法的多线程收集器它看上去几乎和ParNew都一样。 在 ParNew 的基础上演化而来的 Parallel Scanvenge 收集器被誉为“吞吐量优先”收集器。吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值即吞吐量 运行用户代码时间 /运行用户代码时间 垃圾收集时间。如虚拟机总运行了 100 分钟其中垃圾收集花掉 1 分钟那吞吐量就是99%。那么它有什么特别之处呢-XX:UseParallelGC 使用 Parallel 收集器 老年代串行-XX:UseParallelOldGC 使用 Parallel 收集器 老年代并行Parallel Scanvenge 收集器在 ParNew 的基础上提供了一组参数用于配置期望的收集时间或吞吐量然后以此为目标进行收集。通过 VM 选项可以控制吞吐量的大致范围-XXMaxGCPauseMills期望收集时间上限用来控制收集对应用程序停顿的影响。-XXGCTimeRatio期望的GC时间占总时间的比例用来控制吞吐量。-XXUseAdaptiveSizePolicy自动分代大小调节策略。但要注意停顿时间与吞吐量这两个目标是相悖的降低停顿时间的同时也会引起吞吐的降低。因此需要将目标控制在一个合理的范围中。新生代采用复制算法老年代采用标记-整理算法。【4】Serial Old收集器Serial老年代、单线程、标记整理算法Serial 收集器的老年代版本它同样是一个单线程收集器。它主要有两大用途一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用另一种用途是作为 CMS 收集器的后备方案。【5】Parallel Old收集器Parallel老年代、多线程、标记整理算法Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。【6】CMS收集器CMSConcurrent Mark Swee收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。CMS 收集器仅作用于老年代的收集采用“标记-清除”算法它的运作过程分为 4 个步骤1初始标记CMS initial mark暂停所有的其他线程并记录下直接与 root 相连的对象速度很快 2并发标记CMS concurrent mark同时开启 GC 和用户线程用一个闭包结构去记录可达对象。但在这个阶段结束这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。3重新标记CMS remark重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始标记阶段的时间稍长远远比并发标记阶段时间短4并发清除CMS concurrent sweep开启用户线程同时 GC 线程开始对为标记的区域做清扫。其中初始标记、重新标记这两个步骤仍然需要 Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象速度很快并发标记阶段就是进行GC Roots Tracing的过程而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始阶段稍长一些但远比并发标记的时间短。CMS 以流水线方式拆分了收集周期将耗时长的操作单元保持与应用线程并发执行。只将那些必需 STW 才能执行的操作单元单独拎出来控制这些单元在恰当的时机运行并能保证仅需短暂的时间就可以完成。这样在整个收集周期内只有两次短暂的暂停初始标记和重新标记达到了近似并发的目的。CMS 收集器之所以能够做到并发根本原因在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面已经介绍过“标记-清除”算法将产生大量的内存碎片这对新生代来说是难以接受的因此新生代的收集器并未提供 CMS 版本。1优点并发收集低停顿。2缺点1、CMS 收集器对 CPU 资源非常敏感2、CMS 收集器无法处理浮动垃圾3、CMS 收集器是基于“标记-清除”算法该算法的缺点都有会导致收集结束时会有大量空间碎片产生。【7】G1收集器1介绍G1Garbage First重新定义了堆空间打破了原有的分代模型将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行这是它最显著的特点。区域划分的好处就是带来了停顿时间可预测的收集模型用户可以指定收集操作在多长时间内完成即 G1 提供了接近实时的收集特性。G1 与 CMS 的特征对比如下2特点1并行与并发G1 能充分利用多 CPU、多核环境下的硬件优势使用多个 CPU 来缩短 Stop-the-world 停顿的时间部分其他收集器原来需要停顿 Java 线程执行的 GC 操作G1 收集器仍然可以通过并发的方式让 Java 程序继续运行。2分代收集打破了原有的分代模型将堆划分为一个个区域。3空间整合与 CMS 的“标记-清除”算法不同G1 从整体来看是基于“标记-整理”算法实现的收集器从局部两个 Region 之间上来看是基于“复制”算法实现的。但无论如何这两种算法都意味着 G1 运作期间不会产生内存空间碎片收集后能提供规整的可用内存。这种特性有利于程序长时间运行分配大对象时不会因为无法找到连续内存空间而提前触发下一次 GC。4可预测的停顿这是 G1 相对于 CMS 的一个优势降低停顿时间是 G1 和 CMS 共同的关注点。但 G1 除了追求低停顿外还能建立可预测的停顿时间模型能让使用者明确指定在一个长度为 M 毫秒的时间片段内。3详细介绍在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老年代而 G1 不再是这样。在堆的结构设计时G1 打破了以往将收集范围固定在新生代或老年代的模式G1 将堆分成许多相同大小的区域单元每个单元称为 RegionRegion 是一块地址连续的内存空间G1 模块的组成如下图所示堆内存会被切分成为很多个固定大小的 Region每个是连续范围的虚拟内存。堆内存中一个 Region 的大小可以通过-XX:G1HeapRegionSize参数指定其区间最小为 1M、最大为 32M默认把堆内存按照 2048 份均分。每个 Region 被标记了 E、S、O 和 H这些区域在逻辑上被映射为 EdenSurvivor 和老年代。存活的对象从一个区域转移即复制或移动到另一个区域区域被设计为并行收集垃圾可能会暂停所有应用线程。如上图所示区域可以分配到 EdenSurvivor 和老年代。此外还有第四种类型被称为巨型区域Humongous Region。Humongous 区域是为了那些存储超过 50% 标准 Region 大小的对象而设计的它用来专门存放巨型对象。如果一个 H 区装不下一个巨型对象那么 G1 会寻找连续的 H 分区来存储。为了能找到连续的 H 区有时候不得不启动 Full GC。G1 收集器之所以能建立可预测的停顿时间模型是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。G1 会通过一个合理的计算模型计算出每个 Region 的收集成本并量化这样一来收集器在给定了“停顿”时间限制的情况下总是能选择一组恰当的 Region 作为收集目标让其收集开销满足这个限制条件以此达到实时收集的目的。对于打算从 CMS 或者 ParallelOld 收集器迁移过来的应用按照官方的建议如果发现符合如下特征可以考虑更换成 G1 收集器以追求更佳性能1实时数据占用了超过半数的堆空间2对象分配率或“晋升”的速度变化明显3期望消除耗时较长的GC或停顿超过 0.5 ~ 1 秒。4G1 收集的运作过程1初始标记Initial Marking仅仅只是标记一下GC Roots能直接关联到的对象并且修改 TAMSNext Top at Mark Start的值让下一阶段用户程序并发运行时能在正确可用的 Region 中创建新对象这阶段需要停顿线程但耗时很短。2并发标记Concurrent Marking是从GC Roots开始堆中对象进行可达性分析找出存活的对象这阶段耗时较长但可与用户程序并发执行。3最终标记Final Marking是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中这阶段需要停顿线程但是可并行执行。4筛选回收Live Data Counting and Evacuation首先对各个 Region 的回收价值和成本进行排序根据用户所期望的 GC 停顿时间来制定回收计划。这个阶段也可以做到与用户程序一起并发执行但是因为只回收一部分 Region时间是用户可控制的而且停顿用户线程将大幅提高收集效率。5G1 的 GC 模式可以分为两种1Young GC在分配一般对象非巨型对象时当所有 Eden 区域使用达到最大阀值并且无法申请足够内存时会触发一次 YoungGC。每次 Young GC 会回收所有 Eden 以及 Survivor 区并且将存活对象复制到 Old 区以及另一部分的 Survivor 区。2Mixed GC当越来越多的对象晋升到老年代时为了避免堆内存被耗尽虚拟机会触发一个混合的垃圾收集器即 Mixed GC该算法并不是一个 Old GC除了回收整个新生代还会回收一部分的老年代这里需要注意是一部分老年代而不是全部老年代可以选择哪些 Old 区域进行收集从而可以对垃圾回收的耗时时间进行控制。G1 没有 Full GC概念需要 Full GC 时调用 Serial Old GC 进行全堆扫描。【8】理解GC日志【9】垃圾收集器参数总结【1】查看 JVM 使用的默认垃圾收集器java-XX:PrintCommandLineFlags-version【2】设置运行的垃圾回收器类型【3】GC的优化配置命令【4】使用JVM GC 参数的例子java-Xmx12m-Xms3m-Xmn1m-XX:PermSize20m-XX:MaxPermSize20m-XX:UseSerialGC-jar java-application.jar 堆内存最大值12m堆内存最小值3m新生代1m永久代20m永久代最大20m使用串行垃圾回收器六内存分配与回收策略Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时Java 自动内存管理最核心的功能是 堆 内存中对象的分配与回收。Java 堆是垃圾收集器管理的主要区域因此也被称作GC 堆Garbage Collected Heap.从垃圾回收的角度由于现在收集器基本都采用分代垃圾收集算法所以 Java 堆还可以细分为新生代和老年代再细致一点有Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存或者更快地分配内存。堆空间的基本结构上图所示的 eden 区、s0(“From”) 区、s1(“To”) 区都属于新生代tentired 区属于老年代。大部分情况对象都会首先在 Eden 区域分配在一次新生代垃圾回收后如果对象还存活则会进入 s1(“To”)并且对象的年龄还会加 1(Eden 区-Survivor 区后对象的初始年龄变为 1)当它的年龄增加到一定程度默认为 15 岁就会被晋升到老年代中。对象晋升到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold 来设置。经过这次GC后Eden区和From区已经被清空。这个时候“From和To会交换他们的角色也就是新的To就是上次GC前的“From”新的From就是上次GC前的To”。不管怎样都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程直到“To”区被填满To区被填满之后会将所有对象移动到年老代中。【1】对象优先在Eden分配目前主流的垃圾收集器都会采用分代回收算法因此需要将堆内存分为新生代和老年代这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。没有一种算法是万能的所以要分情况使用不同的算法比如在老年代和新生代分别使用不同的回收算法大多数情况下对象在新生代中 eden 区分配。当 eden 区没有足够空间进行分配时虚拟机将发起一次 Minor GC.下面我们来进行实际测试以下。在测试之前我们先来看看 Minor GC 和 Full GC 有什么不同呢新生代 GCMinor GC:指发生新生代的的垃圾收集动作Minor GC 非常频繁回收速度一般也比较快。老年代 GCMajor GC/Full GC:指发生在老年代的 GC出现了 Major GC 经常会伴随至少一次的 Minor GC并非绝对Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。【2】大对象直接进入老年代大对象就是需要大量连续内存空间的对象比如字符串、数组。为什么要这么做为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。【3】长期存活的对象将进入老年代既然虚拟机采用了分代收集的思想来管理内存那么内存回收时就必须能识别哪些对象应放在新生代哪些对象应放在老年代中。为了做到这一点虚拟机给每个对象一个对象年龄Age计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活并且能被 Survivor 容纳的话将被移动到 Survivor 空间中并将对象年龄设为 1.对象在 Survivor 中每熬过一次 MinorGC,年龄就增加 1 岁当它的年龄增加到一定程度默认为 15 岁就会被晋升到老年代中。对象晋升到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold 来设置。【4】动态对象年龄判定为了更好的适应不同程序的内存情况虚拟机不是永远要求对象年龄必须达到了某个值才能进入老年代如果 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半年龄大于或等于该年龄的对象就可以直接进入老年代无需达到要求的年龄。【5】空间分配担保

相关新闻