14. 简述一下垃圾回收器?说下各自的优缺点?有了解过cms和G1吗?能详细说明一下吗?三色标记法是什么? Ø 如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。 年轻代收集器:Serial、ParNew、Parallel Scavenge 老年代收集器:Serial Old、Parallel Old、CMS收集器 特殊收集器:G1收集器[新型,不在年轻、老年代范畴内] 一般搭配:Serial + Serial Old; ParNew + CMS; Parallel Scavenge + Parallel Old 并发及并行收集器的概念? ●并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 ●并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。 Ø 查看jdk查看默认的垃圾回收器:java -XX:+PrintCommandLineFlags -version 自JDK7u4开始的JDK7u系列与JDK8系列,如果指定了:-XX:+UseParallelGC,则会默认开启:XX:+UseParallelOldGC。所以jdk8默认的垃圾回收器是: Parallel Scavenge(新生代) + Parallel Old(老年代),即PS + PO。 Ø Serial收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。它是虚拟机运行在Client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生ConcurrentMode Failure时使用。 ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样。它是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。 ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。 ParNew其实是Parallel Scavenge的变种,主要是为了个CMS结合使用。 Parallel Scavenge收集器是一个新生代收集器,也是使用复制算法的收集器。它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。 Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。 MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。不过大家不要认为如果把这个参数的值设置得稍小一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每 次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。 GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即1/(1+19)),默认值为99,就是允许最大1%(即1/(1+99))的垃圾收集时间。 Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。 Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供。 CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,在JDK 1.5时期诞生,是基于“标记—清除”算法实现的。是HotSpot虚拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作,用前面那个例子的话来说,就是做到了在你的妈妈打扫房间的时候你还能一边往地上扔纸屑。 不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作[1],所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个。 它的核心思想是将STW打散,让一部分GC线程与用户线程并发执行,整个过程分为4个步骤,包括: 初始标记(CMS initial mark):只标记处根对象直接引用的对象,速度很快。该阶段是STW; 并发标记(CMS concurrent mark):进行GC RootsTracing继续标记其他对象,与应用程序并发执行; 重新标记(CMS remark):对并发标记阶段因用户程序继续运作而导致标记产生变动的那一部分对象重新标记,该阶段是STW;这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短; 并发清除(CMS concurrent sweep):清除垃圾。和应用程序并行执行。清除过程中,应用程序又会不断的产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。 CMS有以下3个明显的缺点: · CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。 · CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置得太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。 · CMS是一款基于“标记—清除”算法实现的收集器,收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。 ØØ G1(Garbage-First)收集是一款面向服务端应用的垃圾收集器,基于“标记—整理”算法实现,不在区分年轻代和老年代。与其他GC收集器相比,G1具备如下特点: · 并行性:G1在回收期间,能够由多个GC线程同时工做,有效利用多核计算能力。 · 并发性:G1拥有与应用程序交替执行的能力,所以通常来讲,不会在整个回收期间彻底阻塞应用程序。 · 分代GC:与以前回收器不一样,其余回收器,它们要么工作在年轻代要么工作在老年代。G1能够同时兼顾年轻代与老年代。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 · 空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。 · 可预见性:因为分区的缘由,G1能够只选取部分区域进行内存回收,这样缩小了回收范围,所以对于全局停顿也能获得更好的控制。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。 · 软实时(soft real-time):所谓的实时垃圾回收,是指在要求的时间内完成垃圾回收。“软实时”则是指,用户可以指定垃圾回收时间的限时,G1会努力在这个时限内完成垃圾回收,但是G1并不担保每次都能在这个时限内完成垃圾回收。通过设定一个合理的目标,可以让达到90%以上的垃圾回收时间都在这个时限内。 G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。在每个分区内部又被分成了若干个大小为512 Byte卡片(Card),标识堆内存最小可用粒度。所有分区的卡片将会记录在全局卡片表(Global Card Table)中,分配的对象会占用物理上连续的若干个卡片,当查找对分区内对象的引用时便可通过记录卡片来查找该引用对象(见RSet)。每次对内存的回收,都是对指定分区的卡片进行处理。 https://blog.csdn.net/fedorafrog/article/details/104503829/ 在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。 如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤: 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region中创建新对象,这阶段需要停顿线程,但耗时很短; 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行; 最终标记(Final Marking):修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率,所以其实是STW的; G1当前的测试数据还太少,但是随着Oracle对G1的持续改进,我相信G1会是最终的胜利者。如果你现在采用的收集器没有出现问题,那就没有任何理由现在去选择G1,如果你的应用追求低停顿,那G1现在已经可以作为一个可尝试的选择,如果你的应用追求吞吐量,那G1并不会为你带来什么特别的好处。 Ø CMS和G1在并发标记时使用的是同一个算法:三色标记法,使用白灰黑三种颜色标记对象。白色是未标记;灰色自身被标记,引用的对象未标记;黑色自身与引用对象都已标记。 在remark过程中,黑色指向了白色,如果不对黑色重新扫描,则会漏标。会把白色D对象当作没有新引用指向从而回收掉。 产生漏标问题的条件有两个: 1.黑色对象指向了白色对象 2.灰色对象指向白色对象的引用消失 所以要解决漏标问题,打破两个条件之一即可: 跟踪黑指向白的增加 incremental update:增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS采用该方法。 记录灰指向白的消失 SATB snapshot at the beginning:关注引用的删除,当灰–>白消失时,要把这个 引用 推到GC的堆栈,保证白还能被GC扫描到。G1采用该方法。 satb 算法认为开始标记的都认为是活的对象,所以叫satb。在开始标记的时候,生成一个存活对象的快照,在并发标记的过程中,所有引用改变的对象,都入队(在产生写屏障的时候,将旧引用指向的对象置为非白的),这种方式可能会产生浮动垃圾,将在下一次进行回收。 SATB 利用 write barrier 将全部即将被删除的引用关系的旧引用记录下来(这点理解不了,记录B再遍历B也没用呀,不是应该遍历A吗),最后以这些旧引用为根 Stop The World 地从新扫描一遍便可避免漏标问题。 所以G1 Remark阶段 Stop The World 与 CMS了的remark有一个本质上的区别,那就是这个暂停只须要扫描有 write barrier 所追踪对象为根的对象, 而 CMS 的remark 须要从新扫描整个根集合,于是CMS remark有可能会很是慢。 http://www.javashuo.com/article/p-yjwwjukv-gk.html 为什么G1采用SATB而不用incremental update? 因为采用incremental update把黑色重新标记为灰色后,之前扫描过的还要再扫描一遍,效率太低。 G1有RSet与SATB相配合。Card Table里记录了RSet,RSet里记录了其他对象指向自己的引用,这样就不需要再扫描其他区域,只要扫描RSet就可以了。 也就是说 灰色–>白色 引用消失时,如果没有 黑色–>白色,引用会被push到堆栈,下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高。SATB配合RSet浑然天成。 https://blog.csdn.net/fedorafrog/article/details/104503829/ 15. 说说你了解的JVM参数和其作用? Ø 堆设置 -Xms:初始堆大小,堆内存的最小大小,默认为物理内存的1/64 -Xmx(MaxHeapSize):最大堆大小,堆内存的最大大小,默认为物理内存的1/4,一般不要大于物理内存的80%; 注:如果确定该机器就给这个服务用的话,初始堆和最大堆的大小最好保持一致,内存扩大和收缩会消耗cpu资源。 -XX:NewSize(-Xns):年轻代内存初始大小 -XX:MaxNewSize(-Xmn):年轻代内存最大允许大小,也可以缩写 -Xss来调整Stack Space的大小,一般说来默认的大小是512K -XX:NewRatio:设置新生代和老年代的比值。如:为3,表示年轻代与老年代比值为1:3,即年轻代占堆的1/4 -XX:SurvivorRatio:新生代中Eden区与两个Survivor区的比值。注意Survivor区有两个。默认为8,表示两个Survivor :eden=2:8,即一个Survivor占年轻代的1/10 -XX:+HeapDumpOnOutOfMemoryError:内存溢出时,导出堆信息到文件; -XX:MaxTenuringThreshold:表示如果在幸存区移动多少次没有被垃圾回收,进入老年代。如果是0,则直接跳过新生代进入老年代 -XX:OnOutOfMemoryError:当发生OOM内存溢出时,执行一个脚本;-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p,%p表示线程的id pid; -XX:PermSize、-XX:MaxPermSize:分别设置永久代最小大小与最大大小(Java8以前) -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分别设置元空间最小大小与最大大小(Java8以后) Ø 收集器设置 -XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行老年代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器 Ø 垃圾回收统计信息 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename Ø 并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n) Ø 并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。 -XX:ParallelGCThreads=n:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。 16. JVM调优 Ø 目的:因为stop the world会停掉所有的线程,来专门做垃圾收集,会带来系统的卡顿。java虚拟机调优的目的是为了减少full gc,减少STW的时间,整个系统就会比较流畅。 Ø 朝生夕死的对象尽量放到年轻代,通过mimor gc干掉。对于订单系统,每秒几千并发,可能产生近百兆数据,且几s过后就变成了垃圾对象,这时候可以调下老年代和年轻代的比例,让年轻代有更大的内存空间,防止survior区过小,大量对象跑到老年代,导致频繁GC.需要根据实际的业务进行预估,具体问题具体分析。 arthas唯一替代不了的是jmap命令,比方说jmap -histo查看内存里有哪些对象。注意Jmap不能在线用 17. Object o = new Object() 占用了多少个字节? Ø 16字节 在开启指针压缩的情况下,markword占用8字节,classpoint占用4字节,Instance data无数据,总共是12字节,由于对象需要为8的整数倍,Padding会补充4个字节,总共占用16字节的存储空间。 在没有指针的情况下,markword占用8字节,classpoint占用8字节,Instance data无数据,总共是16字节。 18. 在HotSpot虚拟机中,对象在内存中存储的布局分为:对象头、实例数据、对齐填充; 在java中对象的内存布局分为两种情况,非数组对象和数组对象,数组对象和非数组对象的区别就是需要额外的空间存储数组的长度length。 https://www.jianshu.com/p/d42ac3ab41f7 对象头又分为MarkWord和Class Pointer两部分。 · MarkWord 在32位系统下,对象头8字节,64位则是16个字节【未开启压缩指针,开启后12字节】。markword很像网络协议报文头,划分为多个区间,并且会根据对象的状态复用自己的存储空间。 为什么这么做:省空间,对象需要存储的数据很多,32bit/64bit是不够的,它被设计成非固定的数据结构以便在极小的空间存储更多的信息,假设当前为32bit,在对象未被锁定情况下。25bit为存储对象的哈希码、4bit用于存储分代年龄,2bit用于存储锁标志位,1bit固定为0。 不同状态时对象头的区间含义,如图所示。 markOop中提供了大量方法用于查看当前对象头的状态,以及更新对象头的数据,为synchronized锁的实现提供了基础。[比如说我们知道synchronized锁的是对象而不是代码,而锁的状态保存在对象头中,进而实现锁住对象]。 · ClassPointer:用来指向对象对应的Class对象(其对应的元数据对象)的内存地址。在32位系统占4字节,在64位系统中占8字节。 · Length:只在数组对象中存在,用来记录数组的长度,占用4字节; 实例数据 对象实际数据,对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。(这里不包括静态成员变量,因为其是在方法区维护的) 分配策略:相同宽度的字段总是放在一起,比如double和long 对齐填充(Padding) 这部分没有特殊的含义,仅仅起到占位符的作用满足JVM要求。由于HotSpot规定对象的大小必须是8的整数倍,对象头刚好是整数倍,如果实例数据不是8字节的整数倍的话,就需要占位符对齐填充。 指针压缩 -XX:+UseCompressedOops 这个参数就是JVM提供给你的解决方案,可以压缩指针,将占用的空间压缩为原来的一半,起到节约空间的作用,lasspointer参数大小就受到其影响。 19. 类初始化过程 Ø Student s = new Student();在内存中做了哪些事情? ·加载Student.class文件进内存 ·在栈内存为s开辟空间 ·在堆内存为学生对象开辟空间 ·对学生对象的成员变量进行默认初始化 ·对学生对象的成员变量进行显示初始化 ·通过构造方法对学生对象的成员变量赋值 ·学生对象初始化完毕,把对象地址赋值给s变量 20. 内存屏障是什么?如何保证多个处理器运算涉及到同一个内存区域时,多线程场景下会存在缓存一致性问题,那么运行时保证数据一致性? Ø 随着cpu的发展,内存的读写速度也远远赶不上cpu。因此cpu厂商在每颗cpu上加上高速缓存,用于缓解这种情况。cpu上加入了高速缓存这样做解决了处理器和内存的矛盾(一快一慢),但是引来的新的问题 - 缓存一致性。 在多核cpu中,每个处理器都有各自的高速缓存(L1,L2,L3),而主内存确只有一个 。CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找,每个cpu有且只有一套自己的缓存。 Ø 在CPU层面,内存屏障提供了个充分必要条件 内存屏障(Memory Barrier) CPU中,每个CPU又有多级缓存【上图统一定义为高速缓存】,一般分为L1,L2,L3,因为这些缓存的出现,提高了数据访问性能,避免每次都向内存索取,但是弊端也很明显,不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。 硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。通常这些内存屏障的行为由硬件层面实现,对于上层语言的程序员来说是透明的(不需要太关心具体的内存屏障如何实现)。 对于读屏障:在读指令前插入读屏障,可以让高速缓存中的数据失效,强制从主内存取。 对于写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。 内存屏障的两个比较重要的作用: 1.cpu执行指令可能是无序的,阻止屏障两侧指令重排序 2.强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。 Ø volatile型变量 当我们声明某个变量为volatile修饰时,这个变量就有了线程可见性,volatile通过在读写操作前后添加内存屏障。 volatile型变量拥有如下特性: 可见性,对于一个该变量的读,一定能看到读之前最后的写入。 防止指令重排序,执行代码时,为了提高执行效率,会在不影响最后结果的前提下对指令进行重新排序,使用volatile可以防止,比如单例模式双重校验锁的创建中有使用到; 注意的是volatile不具有原子性,如volatile++这样的复合操作; 至于volatile底层是怎么实现保证不同线程可见性的,这里涉及到的就是硬件上的,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据。 https://www.jianshu.com/p/76959115d486