标签:多线程 多版本 任务 mst 空间 内存溢出 cli art sts
收集器组合章节来自第一篇参考文章,非原创,作者总结地非常好!
分代收集相关概念来自参考文章第二篇,非原创
第二篇参考资料的文章质量很高,推荐阅读!
在Java8的HotSpot虚拟机中一共包括了5个垃圾收集器,它们每一个都是基于分代收集的思想。在这一节中,我主要介绍一下各个分代区域以及对象是怎样被分配到这些区域的。这是官方文档给出的5个可得到的收集器:5 Available Collectors,并介绍了如何针对自己的应用选择出一个合适的收集器。
对于Generational Hypothesis的概念,Jeff Hammerbacher在Quora上已经给出一个很好地答案,我把它翻译一下。
Generational Hypothesis是一个关于对象生命周期分布的假设。准确地说,这个假设认为对象生命周期的分布是双峰的:大部分的对象要么是短命的,要么就是一直存活的。
Generational Hypothesis
基于这个假设,Hotspot虚拟机把内存分为年轻代(Young Generation)和老年代(Old Generation)。有了这样的内存区域划分,我们可以针对不同的区域选择合适的算法来进行垃圾收集,从而大大提高垃圾收集的效率。注意:分代收集是基于上面的假设来进行的,如果你的应用完全不符合上面的假设,那么你的垃圾收集效率一定很低。
因为年轻代空间通常很小,包含很多短命的对象,所以它的收集要频繁一些。经过了几轮年轻代收集,依然存活的对象被晋升(promoted)或者tenured到老年代。因为老年代的空间要比年轻代大很多并且它的对象大部分都是长命的,所以它的收集是不频繁的。由于年轻代的收集很频繁,因此针对这个区域的收集算法要很快。另一方面,由于老年代的收集不是很频繁的并且它占用了大多数的堆空间,因此这一区域的算法针对低频的垃圾收集要空间有效的。
在介绍各个分代区域之前,大家先看看下面这张图。
注意:在Java 8中已经移除了永久代。
年轻代是由一个Eden区域 + 2个survivor区域组成。大部分的对象最初都被分到Eden区域(特别大的对象可能直接被分配到老年代)。对于2个survivor区域来说,它们中的一个必须始终是空的。并且每个survivor区域中的对象至少是经历过一次年轻代垃圾收集的。假设几分钟前垃圾收集器已经进行了一次年轻代的垃圾收集了,Eden区域和其中的1个survivor区域都有对象,另一个survivor区域为空。现在,又要进行一次垃圾收集了,收集器做的就是:把Eden区域和那个有对象的survivor区域中活着的对象找出来并复制到另一个空的survivor区域中,然后清空Eden区域和先前有对象的那个survivor区域。
如果空的这个survivor区域的空间不够装下Eden区域和另一个survivor区域中活着的对象,那么收集器会把容纳不下的对象直接分配到老年代。如果老年代也容不下这些对象,那么会触发老年代的垃圾收集,然后去容纳这些对象。
由于Java应用支持多线程,那么在多线程应用的情况下对象的分配就会出现一些问题。比如,我上一个线程分配的对象会被下一个线程所分配的对象覆盖。如果用一个全局锁来把整个年轻代锁住,那么分配一个对象到年轻代的性能会非常低下。因此,虚拟机团队想出了一个解决方案叫做Thread-Local Allocation Buffers (TLABs).
Thread-Local Allocation Buffers
如上图所示,每一个线程都有一个自己的TLAB,分配对象时用指针碰撞(bump-the-pointer)技术去在各自的TLAB中分配对象,大大提升了年轻代分配对象的效率。设置‐XX:+UseTLAB来启用TLAB,通过‐XX:TLABSize来设置其大小,默认大小为0,0表示虚拟机动态计算其大小。
经过了几次垃圾收集还没有被回收的对象就被promoted到老年代了。那么如何去判断一个对象是否足够老可以晋升到老年代呢?垃圾收集器追踪每个活着对象被收集的次数,每挺过一次垃圾收集,对象的年龄就加1,当一个对象的年龄超过了指定的阙值(tenuring threshold),它就会被晋升到老年代。通过设置XX:+MaxTenuringThreshold来指定一个上限,如果设置为0,那么经过1次垃圾收集以后马上被晋升。
老年代的空间是非常大的并且它里面存在的对象成为垃圾的可能性很小。老年代的垃圾收集次数要比年轻代少很多,并且由于老年代的对象很少会成为垃圾对象,年轻代的做法(在survivor区域不断copy)并不适合老年代。老年代具体的收集算法我会在下面具体的垃圾收集器中介绍。
永久代在Java 8以前存在。 JVM用这里存储一些类的元数据还有一些被内在化的字符串。What is String interning?详细地解释了什么是内在化字符串。Hotspot虚拟机用永久代实现了方法区,因此如果你用动态代理技术或CGLib产生大量的增强代理类,都会使永久代出现异常。比如,当你用Spring的AOP时,它都会为想要增强的类产生一个代理类从而达到增强的目的,如果产生的类很多,你的永久代将会溢出。
永久代给Java开发者制造了很多的麻烦,因为很难预测出它将需要多少内存空间。如果出现溢出:产生java.lang.OutOfMemoryError: Permgen space.的错误。
由于上面永久代的缺点,它在Java 8中被移除,取而代之的是Metaspace,这块内存区域位于本地内存中。默认情况下,Metaspace的大小只被Java进程可得到的本地内存所限制。因此,这个区域并不会因为稍微增加一个类就导致溢出。注意:Metaspace没有限制地增长将会导致本地内存溢出。 你可以设置-XX:MaxMetaspaceSize来限制其大小。
垃圾回收必须要枚举到根节点经过判断才可以知道哪些对象是存活的,于是就会有以下的问题了:
第一个问题,JVM 使用了一组称为OopMap的数据结构来达到这个目的,在类加载完成的时候,HotSpot 就把对象内存多少偏移量上是什么类型的数据计算出来(有点像java中的unsafe类的方法,在AQS中或是concurrenthashmap中),在JIT编译过程中在特定位置纪录下栈和寄存器中哪些位置是引用。
第二个问题,这个时间点称之为 “Stop The World”----Safepoint,这个时间点在程序中的选定要是太少,那么GC 就要等待太久,要是太多个安全点,那么GC 次数变多,容易引起性能问题,所以安全点的选定基本上是以“是否具有让程序长时间执行的特征”,具有这类的最明显的特征就是指令序列复用,例如方法调用,循环跳转,异常跳转等。
第三个问题, JVM 使用主动式中断( Voluntary Suspension ),当要GC时,仅仅设置一个标志位,让线程自己去主动轮询这个标志,发现中断标志为真时,就自己挂起,轮徐标志的地方和安全点是重合的。
第四个问题,JVM设定了一个“安全区域(Safe Region)”,即在这个范围内,引用关系不会裱花,我们也可以把Safe Region 看做是被扩展的 Safe Point.
由于Hotspot虚拟机的垃圾收集是基于分代思想的,那么在不同的分代区域收集会产生不同的垃圾收集类型,本节我将会介绍这些垃圾收集类型以及它们发生的时机。
发生在年轻代的垃圾收集叫做minor gc,它具体的细节是什么样呢?
清理整个堆的过程叫做full gc,有时也叫做major collection. 当老年代太满了而不能要接受所有来自年轻代晋升的对象时,所有的收集器(除了CMS)将停止年轻代的收集算法运行,而是用老年代的收集算法清理整个堆内存。(CMS垃圾收集器的老年代收集算法不能收集年轻代)。
先上一张图。
说明:
注意:并行与并发
从图中可以看出,前者在新生代,后者在老生代,Serial只有一个线程去收集,并且必须暂停其他所有的工作线程,知道它收集结束。优点是简单和高效,缺点是不够灵活,适合运行在 Client 模式下的虚拟机。
Serial Old 就是 Serial 在老生代的版本,也是适合运行在 Client 模式下的虚拟机。要是运行在Server 模式下, Serial Old 有以下的作用 :
它存在在新生代,就是Serial收集器的多版本。
Paranlle Scavenge 是一个新生代收集器,也是使用复制算法被称为“吞吐量优先”的收集器,它的特点有两点 :
第一个特点,吞吐量就是运行用户代码时间占(运行用户代码时间+垃圾收集时间)的比值,可以通过参数调整吞吐量。Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,说明CPU利用率越高,所以主要用于处理很多的CPU计算任务而用户交互任务较少的情况)
第二个特点,JVM根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
Paranlle Old 是 Paranlle Scavenge 的老年代版本,使用多线程和“标记-整理”算法。在注重吞吐量以及CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 组合的收集器。
CMS收集器是一种以获取最短回收停顿时间为目标的基于“标记-清除”的收集器。
CMS Collector也叫做low-latency collector,它是专门为老年代设计的。因为年轻代的stop-the-world时间不会太长,而对于老年代来说,虽然它的收集并不频繁,但是每次收集都可能会出现较长的停顿时间,尤其是在堆空间很大的时候。而CMS Collector的出世就是解决老年代停顿时间长的问题。解决这个问题它主要通过下面2个手段:
回收过程分四个步骤 :
缺点 :
CMS 并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾
出现在标记过程之后,CMS 无法在当次收集中处理掉他们,这部分垃圾就是浮动垃圾。
也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间用用户线程使用, 因此CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了在收集,CMS会预留一部分空间提供并发收集时的程序运行使用。要是预留的空间无法程序需要(浮动垃圾太多,需要回收的东西太多),就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案 : 临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。
3. CMS 是基于“标记-清除”算法,空间碎片过多,可能出现无法分配大对象的情况,往往会出现老年代还有很大空间剩余,但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次 Full GC
可以独立运行,下面总结一下它的特点 :
原理:
特点:
适用场合:
说明:
ParNew除了采用多GC线程来实现复制算法以外,其他都与Serial一样,但是此组合中的Serial Old又是一个单GC线程,所以该组合是一个比较尴尬的组合,在单CPU情况下没有Serial/Serial Old速度快(因为ParNew多线程需要切换),在多CPU情况下又没有之后的三种组合快(因为Serial Old是单GC线程),所以使用其实不多。
-XX:ParallelGCThreads:指定ParNew GC线程的数量,默认与CPU核数相同,该参数在于CMS GC组合时,也可能会用到
特点:
说明:
参数设置:
适用场合:
说明:
特点:
1.年轻代ParNew收集器采用多个GC线程实现"复制"算法(包括扫描、复制)
2.年老代CMS收集器采用多线程实现"标记-清除"算法,整个过程分4个步骤,上面已介绍了
3.初始标记与重新标记都会暂停所有用户线程(即STW),但是时间较短;并发标记与并发清理时间较长,但是不需要STW
关于并发标记期间怎样记录发生变动的引用关系对象,在重新标记期间怎样扫描这些对象
参数设置:
适用场合:
用于处理很多的交互任务的情况
方法区的回收一般使用CMS,配置两个参数:-XX:+CMSPermGenSweepingEnabled与-XX:+CMSClassUnloadingEnabled
适用于一些需要长期运行且对相应时间有一定要求的后台程序
说明:
运作流程:
优点:
适用范围:
标签:多线程 多版本 任务 mst 空间 内存溢出 cli art sts
原文地址:https://www.cnblogs.com/Benjious/p/10265144.html