1、JVM
→ Java 内存模型
计算机内存模型、缓存一致性、MESI 协议
计算机内存模型:
之前文章已经总结过。
CPU与缓存一致性
随着CPU的技术发展,CPU执行速度越来越快,但是内存技术并没有太大的变化,导致CPU操作内存都要耗费很多时间。所以在CPU和内存之间增加高速缓存。
那么,当程序在运行过程中,会将运算需要的数据从主存复制一分到CPU的高速缓存中,那么CPU在计算时就可以直接从他的高速缓存中读取和向其中写入数据,当运算结束后,再将高速缓存中的数据刷到主存当中。
而随着CPU能力的不断提升,一层缓存就慢慢的无法满足要求了,就逐渐的衍生出多级缓存。
MESI 协议
多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。
MESI 是指4种状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:
状态 | 描述 | 监听任务 |
---|---|---|
M 修改 (Modified) | 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 | 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。 |
E 独享、互斥 (Exclusive) | 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 | 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。 |
S 共享 (Shared) | 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 | 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。 |
I 无效 (Invalid) | 该Cache line无效。 | 无 |
注意:
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。
从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。
MESI状态转换
【参考链接】https://www.hollischuang.com/archives/2550
https://www.cnblogs.com/yanlong300/p/8986041.html
可见性、原子性、有序性、happens-before、
可见性:多个线程同时访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改后的值。
原子性:指原子性的操作是不可中断的,要么全部执行,要么全部不执行。
线程是CPU调度的基本单位,CPU有时间片的概念,根据不同的调度算法调度线程,一个线程获取时间片后开始执行,多线程情况下,容易发生原子性问题。
有序性:有序性即执行程序的顺序按照代码的先后顺序执行。
happens-before:这个原则会保证在发生指令重排情况下,程序还保证有序性,其原则在 Java基础篇(6)中写到过。其规则包含程序次序规则、监视器锁规则、volatile域规则、传递规则、线程规则等。
内存屏障、synchronized、volatile、final、锁
内存屏障:
是让一个CPU处理单元中的内存状态对其他处理单元可见的一种。其实就是强制刷新出各种CPU cache。
读屏障:在读指令前插入读屏障,保证从主内存同步最新的数据。
写屏障:在写指令后插入写屏障,保证写入的数据立马对其他线程可见。
全能屏障:具备读写屏障的能力。
synchronized:参考 synchronized知识点
volatile:参考 volatile知识点
锁:参考 各种锁总结
→ 垃圾回收
GC 算法:标记清除、引用计数、复制、标记压缩、分代回收、
标记清除:标记清除法是现在GC算法的基础,目前似乎没有哪个GC还在使用这种算法了。因为这种算法会产生大量的内存碎片。
标记清除算法的执行过程分为两个阶段:标记阶段、清除阶段。
- 标记阶段会通过可达性分析将不可达的对象标记出来。
- 清除阶段会将标记阶段标记的垃圾对象清除。
Java堆中,黄色对象为不可达对象,在标记阶段被标记。然后执行回收被标记的对象。
可见回收后会产生大量不连续的内存空间,即内存碎片。由于Java在分配内存时通常是按连续内存分配,那么当碎片空间不足以分配给新的对象时,就造成了内存浪费。
引用计数:
使用引用计数法,要先给每一个对象中添加一个计数器,一旦有地方引用了此对象,则该对象的计数器加1,如果引用失效了,则计数器减1。这样当计数器为0时,就代表此对象没有被任何地方引用。这种方法实现简单,判定效率也很高,在大部分情况下都是一个比较不错的方法。但是在Java虚拟机中并没有选用引用计数法来管理内存,其主要原因是它很难解决对象之间相互引用的问题,如果两个对应互相引用,导致他们的引用计数都不为0,最终不能回收他们。
复制算法:
复制算法会将内存空间分为两块,每次只使用其中一块内存。复制算法同样使用可达性分析法标记除垃圾对象,当GC执行时,会将非垃圾对象复制到另一块内存空间中,并且保证内存上的连续性,然后直接清空之前使用的内存空间。然后如此往复。
如下图所示,r1和r2作为GC Root对象,经过可达性分析后,标记除黄色对象为垃圾对象。
复制过程如下,GC会将五个存活对象复制到to区,并且保证在to区内存空间上的连续性。
最后,将from区中的垃圾对象清除。由此可见,该算法在存货对象少,垃圾对象多的情况下,非常高效。其好处是不会产生内存碎片,但坏处也是显而易见的,就是直接损失了一半的可用内存。
标记压缩算法:
标记压缩算法可以解决标记清除算法的内存碎片问题。
其算法可以看作三步:
- 标记垃圾对象
- 清除垃圾对象
- 内存碎片整理
第三步内存碎片整理:
分代回收:
分代算法基于复制算法和标记压缩算法。
标记清除算法会产生大量的内存碎片,复制算法会损失一半的内存,标记压缩算法的碎片整理会造成较大的消耗。
但是都有各自适合的场景:
1、复制算法适用于每次回收时,存活对象少的场景,这样就会减少复制量。
2、标记压缩算法适用于回收时,存活对象多的场景,这样就会减少内存碎片的产生,碎片整理的代价就会小很多。
分代算法将内存区域分为两部分:新生代和老年代。根据新生代和老年代中对象的不同特点,使用不同的GC算法。
新生代对象的特点是:创建出来没多久就可以被回收(例如虚拟机栈中创建的对象,方法出栈就会销毁)。也就是说,每次回收时,大部分是垃圾对象,所以新生代适用于复制算法。
老年代的特点是:经过多次GC,依然存活。也就是说,每次GC时,大部分是存活对象,所以老年代适用于标记压缩算法。
新生代分为eden区、from区、to区,老年代是一整块内存空间,如下所示:
分代算法执行过程:
首先简述一下新生代GC的整个过程(老年代GC会在下面介绍):新创建的对象总是在eden区中出生,当eden区满时,会触发Minor GC,此时会将eden区中的存活对象复制到from和to中一个没有被使用的空间中,假设是to区(正在被使用的from区中的存活对象也会被复制到to区中)。
有几种情况,对象会晋升到老年代:
- 超大对象会直接进入到老年代(受虚拟机参数-XX:PretenureSizeThreshold参数影响,默认值0,即不开启,单位为Byte,例如:3145728=3M,那么超过3M的对象,会直接晋升老年代)
- 如果to区已满,多出来的对象也会直接晋升老年代
- 复制15次(15岁)后,依然存活的对象,也会进入老年代
此时eden区和from区都是垃圾对象,可以直接清除。
PS:为什么复制15次(15岁)后,被判定为高龄对象,晋升到老年代呢?
因为每个对象的年龄是存在对象头中的,对象头用4bit存储了这个年龄数,而4bit最大可以表示十进制的15,所以是15岁。
总结: 垃圾回收的整体思路分两个流派 引用计数: 就是上面说的第二种 可达性: 就是标记清除那种, 判断一个对象是否可以到达. 引用计数的最大优势应该就是不需要暂停程序去进行回收了, 随使用随回收. 但劣势也很明显: 需要计数器额外空间以及循环引用的问题.
【参考链接】https://blog.csdn.net/xzm_rainbow/article/details/84997446
https://www.cnblogs.com/hujingnb/p/12638443.html
对象存活的判定、垃圾收集器(CMS、G1、ZGC、Epsilon)、GC 参数
对象存活的判定:
判断对象是否存活一般有两种方式:
引用计数:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,缺点是无法解决对象相互循环引用的问题。
可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
垃圾收集器:
CMS:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤:
初始标记(CMS initial mark):初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
并发标记(CMS concurrent mark):并发标记阶段就是进行GC Roots Tracing的过程.
重新标记(CMS remark):重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。
并发清除(CMS concurrent sweep):并发清除阶段会清除对象。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量、无法处理浮动垃圾(清理阶段,用户线程会产生新的垃圾,无法清理)
参数控制:-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
G1:G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:
- 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
- 空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
收集步骤:
1、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),并且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
2、Root Region Scanning,程序运行过程中会回收survivor区(存活到老年代),这一过程必须在young GC之前完成。
3、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收(图中打X)。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。
4、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
5、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
6、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
【原文链接】https://blog.csdn.net/qq_31997407/article/details/79735411
https://blog.csdn.net/chixushuchu/article/details/86169548