Cpu和内存不兼容之间会存在不兼容吗?老是听说电脑得考虑彼此的兼容性,那英特尔每升级一代的cpu,都得与

  核心(Die)又称为内核,是CPU最重要的组成部分。CPU中心那块隆起的芯片就是核心,是由单晶硅以一定的生产工艺制造出来的,CPU所有的计算、接受/存储命令、处理数据都由核心执行。各种CPU核心都具有固定的逻辑结构,一级缓存、二级缓存、执行单元、指令级单元和总线接口等逻辑单元都会有科学的布局。

  为了便于CPU设计、生产、销售的管理,CPU制造商会对各种CPU核心给出相应的代号,这也就是所谓的CPU核心类型。

  不同的CPU(不同系列或同一系列)都会有不同的核心类型(例如Pentium 4的Northwood,Willamette以及K6-2的CXT和K6-2+的ST-50等等),甚至同一种核心都会有不同版本的类型(例如Northwood核心就分为B0和C1等版本),核心版本的变更是为了修正上一版存在的一些错误,并提升一定的性能,而这些变化普通消费者是很少去注意的。每一种核心类型都有其相应的制造工艺(例如0.25um、0.18um、0.13um以及0.09um等)、核心面积(这是决定CPU成本的关键因素,成本与核心面积基本上成正比)、核心电压、电流大小、晶体管数量、各级缓存的大小、主频范围、流水线架构和支持的指令集(这两点是决定CPU实际性能和工作效率的关键因素)、功耗和发热量的大小、封装方式(例如S.E.P、PGA、FC-PGA、FC-PGA2等等)、接口类型(例如Socket

1.8GHz性能要高),但这也不是绝对的,这种情况一般发生在新核心类型刚推出时,由于技术不完善或新的架构和制造工艺不成熟等原因,可能会导致新的核心类型的性能反而还不如老的核心类型的性能。例如,早期Willamette核心Socket 423接口的Pentium 4的实际性能不如Socket 370接口的Tualatin核心的Pentium III和赛扬,现在的低频Prescott核心Pentium 4的实际性能不如同频的Northwood核心Pentium 4等等,但随着技术的进步以及CPU制造商对新核心的不断改进和完善,新核心的中后期产品的性能必然会超越老核心产品。

  CPU核心的发展方向是更低的电压、更低的功耗、更先进的制造工艺、集成更多的晶体管、更小的核心面积(这会降低CPU的生产成本从而最终会降低CPU的销售价格)、更先进的流水线架构和更多的指令集、更高的前端总线频率、集成更多的功能(例如集成内存控制器等等)以及双核心和多核心(也就是1个CPU内部有2个或更多个核心)等。CPU核心的进步对普通消费者而言,最有意义的就是能以更低的价格买到性能更强的CPU。

  在CPU漫长的历史中伴随着纷繁复杂的CPU核心类型,以下分别就Intel CPU和AMD CPU的主流核心类型作一个简介。主流核心类型介绍(仅限于台式机CPU,不包括笔记本CPU和服务器/工作站CPU,而且不包括比较老的核心类型)。

这是Intel公司的第一款双核心处理器的核心类型,于2005年4月发布,基本上可以认为Smithfield核心是简单的将两个Prescott核心松散地耦合在一起的产物,这是基于独立缓存的松散型耦合方案,其优点是技术简单,缺点是性能不够理想。目前Pentium D 8XX系列以及Pentium EE D则不支持。Smithfield核心的两个核心分别具有1MB的二级缓存,在CPU内部两个核心是互相隔绝的,其缓存数据的同步是依靠位于主板北桥芯片上的仲裁单元通过前端总线在两个核心之间传输来实现的,所以其数据延迟问题比较严重,性能并不尽如人意。按照Intel的规划,Smithfield核心将会很快被Presler核心取代。

775接口,核心电压1.3V左右,封装方式采用PLGA。其中,Pentium 4全部都为800MHz FSB、2MB二级缓存,都支持超线程技术、硬件防病毒技术EDB、节能省电技术EIST以及64位技术EM64T;而Celeron D则是533MHz FSB、512KB二级缓存,支持硬件防病毒技术EDB和64位技术EM64T,不支持超线程技术以及节能省电技术EIST。Cedar Mill核心也是Intel处理器在NetBurst架构上的最后一款单核心处理器的核心类型,按照Intel的规划,Cedar Mill核心将逐渐被Core架构的Conroe核心所取代。

Mill核心松散地耦合在一起的产物,是基于独立缓存的松散型耦合方案,其优点是技术简单,缺点是性能不够理想。Presler核心采用65nm制造工艺,全部采用Socket 775接口,核心电压1.3V左右,封装方式都采用PLGA,都支持硬件防病毒技术EDB、节能省电技术EIST和64位技术EM64T,并且除了Pentium D 9X5之外都支持虚拟化技术Intel D则不支持,并且两个核心分别具有2MB的二级缓存。在CPU内部两个核心是互相隔绝的,其缓存数据的同步同样是依靠位于主板北桥芯片上的仲裁单元通过前端总线在两个核心之间传输来实现的,所以其数据延迟问题同样比较严重,性能同样并不尽如人意。Presler核心与Smithfield核心相比,除了采用65nm制程、每个核心的二级缓存增加到2MB和增加了对虚拟化技术的支持之外,在技术上几乎没有什么创新,基本上可以认为是Smithfield核心的65nm制程版本。Presler核心也是Intel处理器在NetBurst架构上的最后一款双核心处理器的核心类型,可以说是在NetBurst被抛弃之前的最后绝唱,以后Intel桌面处理器全部转移到Core架构。按照Intel的规划,Presler核心从2006年第三季度开始将逐渐被Core架构的Conroe核心所取代。

M也采用了此核心,Yonah是Intel于2006年初推出的。这是一种单/双核心处理器的核心类型,其在应用方面的特点是具有很大的灵活性,既可用于桌面平台,也可用于移动平台;既可用于双核心,也可用于单核心。Yonah核心来源于移动平台上大名鼎鼎的处理器Pentium M的优秀架构,具有流水线级数少、执行效率高、性能强大以及功耗低等等优点。Yonah核心采用65nm制造工艺,核心电压依版本不同在1.1V-1.3V左右,封装方式采用PPGA,接口类型是改良了的新版Socket 478接口(与以前台式机的Socket 478并不兼容)。在前端总线频率方面,目前Core Duo和Core Solo都是667MHz,而Yonah核心Celeron M是533MHz。在二级缓存方面,目前Core Duo和Core Solo都是2MB,而即Yonah核心Celeron M是1MB。Yonah核心都支持硬件防病毒技术EDB以及节能省电技术EIST,并且多数型号支持虚拟化技术Intel VT。但其最大的遗憾是不支持64位技术,仅仅只是32位的处理器。值得注意的是,对于双核心的Core Duo而言,其具有的2MB二级缓存在架构上不同于目前所有X86处理器,其它的所有X86处理器都是每个核心独立具有二级缓存,而Core Duo的Yonah核心则是采用了与IBM的多核心处理器类似的缓存方案----两个核心共享2MB的二级缓存!共享式的二级缓存配合Intel的“Smart cache”共享缓存技术,实现了真正意义上的缓存数据同步,大幅度降低了数据延迟,减少了对前端总线的占用。这才是严格意义上的真正的双核心处理器!Yonah核心是共享缓存的紧密型耦合方案,其优点是性能理想,缺点是技术比较复杂。不过,按照Intel的规划,以后Intel各个平台的处理器都将会全部转移到Core架构,Yonah核心其实也只是一个过渡的核心类型,从2006年第三季度开始,其在桌面平台上将会被Conroe核心取代,而在移动平台上则会被Merom核心所取代。

E6x00系列和Core 2 Extreme X6x00系列。与上代采用NetBurst微架构的Pentium D和Pentium EE相比,Conroe核心具有流水线级数少、执行效率高、性能强大以及功耗低等等优点。Conroe核心采用65nm制造工艺,核心电压为1.3V左右,封装方式采用PLGA,接口类型仍然是传统的Socket 775。在前端总线频率方面,目前Core 2 Extreme将会升级到1333MHz;在一级缓存方面,每个核心都具有32KB的数据缓存和32KB的指令缓存,并且两个核心的一级数据缓存之间可以直接交换数据;在二级缓存方面,Conroe核心都是两个内核共享4MB。Conroe核心都支持硬件防病毒技术EDB、节能省电技术EIST和64位技术EM64T以及虚拟化技术Intel VT。与Yonah核心的缓存机制类似,Conroe核心的二级缓存仍然是两个核心共享,并通过改良了的Intel Advanced Smart Cache(英特尔高级智能高速缓存)共享缓存技术来实现缓存数据的同步。Conroe核心是目前最先进的桌面平台处理器核心,在高性能和低功耗上找到了一个很好的平衡点,全面压倒了目前的所有桌面平台双核心处理器,加之又拥有非常不错的超频能力,确实是目前最强劲的台式机CPU核心。

FSB的Core 2 Duo E4x00系列。Allendale核心的二级缓存机制与Conroe核心相同,但共享式二级缓存被削减至2MB。Allendale核心仍然采用65nm制造工艺,核心电压为1.3V左右,封装方式采用PLGA,接口类型仍然是传统的Socket 775,并且仍然支持硬件防病毒技术EDB、节能省电技术EIST和64位技术EM64T以及虚拟化技术Intel VT。除了共享式二级缓存被削减到2MB以及二级缓存是8路64Byte而非Conroe核心的16路64Byte之外,Allendale核心与Conroe核心几乎完全一样,可以说就是Conroe核心的简化版。当然由于二级缓存上的差异,在频率相同的情况下Allendale核心性能会稍逊于Conroe核心。

    这是与Conroe同时发布的Intel移动平台双核心处理器的核心类型,其名称来源于以色列境内约旦河旁边的一个湖泊“Merom”。Merom核心于2006年7月27日正式发布,仍然基于全新的Core(酷睿)微架构,这也是Intel全平台(台式机、笔记本和服务器)处理器首次采用相同的微架构设计,目前采用此核心的有667MHz FSB的Core T5x00系列的共享式二级缓存为2MB。Merom核心的主要技术特性与Conroe核心几乎完全相同,只是在Conroe核心的基础上利用多种手段加强了功耗控制,使其TDP功耗几乎只有Conroe核心的一半左右,以满足移动平台的节电需求。

  其与Clawhammer的最主要区别就是二级缓存降为512KB(这也是AMD为了市场需要和加快推广64位CPU而采取的相对低价政策的结果),其它性能基本相同。

64CPU核心,是64位CPU,一般为939接口,0.09微米制造工艺。这种核心使用200MHz外频,支持1GHyperTransprot总线,512K二级缓存,性价比较好。Wincheste集成双通道内存控制器,支持双通道DDR内存,由于使用新的工艺,Wincheste的发热量比旧的Athlon小,性能也有所提升。

KB)二级缓存。同样使用200MHz外频,支持1GHyperTransprot总线,集成了内存控制器,支持双通道DDR400内存,并且可以支持ECC 内存。此外,Troy核心还提供了对SSE-3的支持,和Intel的Xeon相同,总的来说,Troy是一款不错的CPU核心。

Liner (简称DSL)技术,可以将半导体晶体管的响应速度提高24%,这样是CPU有更大的频率空间,更容易超频;二是提供了对SSE-3的支持,和Intel的CPU相同;三是进一步改良了内存控制器,一定程度上增加处理器的性能,更主要的是增加内存控制器对不同DIMM模块和不同配置的兼容性。此外Venice核心还使用了动态电压,不同的CPU可能会有不同的电压。

64处理器之上,甚至用于服务器CPU。可以将SanDiego看作是Venice核心的高级版本,只不过缓存容量由512KB提升到了1MB。当然由于L2缓存增加,SanDiego核心的内核尺寸也有所增加,从Venice核心的84平方毫米增加到115平方毫米,当然价格也更高昂。

64同样也分为TDP功耗62W的标准版(核心电压1.35V左右)和TDP功耗35W的超低功耗版(核心电压1.25V左右)。除了支持双通道DDR2内存以及支持虚拟化技术之外,Orleans核心Athlon 64相对于以前的Socket 754接口和Socket 940接口的Athlon 64并无架构上的改变,性能并无多少出彩之处。

Paris核心是Barton核心的继任者,主要用于AMD的闪龙,早期的754接口闪龙部分使用Paris核心。Paris采用90nm制造工艺,支持iSSE2指令集,一般为256K二级缓存,200MHz外频。Paris核心是32位CPU,来源于K8核心,因此也具备了内存控制单元。CPU内建内存控制器的主要优点在于内存控制器可以以CPU频率运行,比起传统上位于北桥的内存控制器有更小的延时。使用Paris核心的闪龙与Socket A接口闪龙CPU相比,性能得到明显提升。

754接口、90nm制造工艺,1.4V左右电压,200MHz外频,128K或者256K二级缓存。Palermo核心源于K8的Wincheste核心,新的E6步进版本已经支持64位。除了拥有与AMD高端处理器相同的内部架构,还具备了EVP、Cool‘n’Quiet;和HyperTransport等AMD独有的技术,为广大用户带来更“冷静”、更高计算能力的优秀处理器。由于脱胎与ATHLON64处理器,所以Palermo同样具备了内存控制单元。CPU内建内存控制器的主要优点在于内存控制器可以以CPU频率运行,比起传统上位于北桥的内存控制器有更小的延时。

这是AMD于2005年4月发布的在桌面平台上的第一款双核心处理器的核心类型,是在Venice核心的基础上演变而来,基本上可以看作是两个Venice核心耦合在一起,只不过协作程度比较紧密罢了,这是基于独立缓存的紧密型耦合方案,其优点是技术简单,缺点是性能仍然不够理想。Manchester核心采用90nm制造工艺,整合双通道内存控制器,支持1000MHz的HyperTransprot总线,全部采用Socket 939接口。Manchester核心的两个内核都独立拥有512KB的二级缓存,但与Intel的Smithfield核心和Presler核心的缓存数据同步要依靠主板北桥芯片上的仲裁单元通过前端总线传输方式大为不同的是,Manchester核心中两个内核的协作程度相当紧密,其缓存数据同步是依靠CPU内置的SRI(System Request Interface,系统请求接口)控制,传输在CPU内部即可实现。这样一来,不但CPU资源占用很小,而且不必占用内存总线资源,数据延迟也比Intel的Smithfield核心和Presler核心大为减少,协作效率明显胜过这两种核心。不过,由于Manchester核心仍然是两个内核的缓存相互独立,从架构上来看也明显不如以Yonah核心为代表的Intel的共享缓存技术Smart Cache。当然,共享缓存技术需要重新设计整个CPU架构,其难度要比把两个核心简单地耦合在一起要困难得多。

    这是AMD于2005年4月在桌面平台上的新款高端双核心处理器的核心类型,它和Manchester核心非常相似,差别在于二级缓存不同。Toledo是在San Diego核心的基础上演变而来,基本上可以看作是两个San diego核心简单地耦合在一起,只不过协作程度比较紧密罢了,这是基于独立缓存的紧密型耦合方案,其优点是技术简单,缺点是性能仍然不够理想。Toledo核心采用90nm制造工艺,整合双通道内存控制器,支持1000MHz的HyperTransprot总线,全部采用Socket 939接口。Toledo核心的两个内核都独立拥有1MB的二级缓存,与Manchester核心相同的是,其缓存数据同步也是通过SRI在CPU内部传输的。Toledo核心与Manchester核心相比,除了每个内核的二级缓存增加到1MB之外,其它都完全相同,可以看作是Manchester核心的高级版。

interface,系统请求接口)传输在CPU内部实现,除了支持双通道DDR2内存以及支持虚拟化技术之外,相对于以前的Socket 939接口Athlon 64 X2和双核心Athlon 64 FX并无架构上的改变,性能并无多少出彩之处,其性能仍然不敌Intel即将于2006年7月底发布的Conroe核心Core 2 Duo和Core 2

又是一年秋招季,哎呀妈呀我被虐的惨来~这不,前几阵失踪没更新博客,其实是我偷偷把时间用在复习课本了(雾

坚持在社区分享博客也很久了,由于过去的文章有很多疏漏之处,很多大佬都在评论指出我的过错,我很开心也很失望,开心的是有大家帮我指出错误,失望的鄙人学识浅薄总没法做到完美。总之,欢迎评论区各种pr~

好,回到正题。复习的时候,无意间看到java虚拟机的有关知识点,我产生了非常浓厚的兴趣,今天我来结合计算机内存模型的相关知识,与Java内存模型Java对象模型JVM内存结构等相关的知识串联起来,本篇文章共1.5W字,分享给大家,感谢阅读。

想要解锁更多新姿势?请访问我的个人博客(?

相信每个人都有一台电脑,也有diy电脑的经历。现在一台功能强大的diy电脑大概3k就能组装起来,一个i5-8400 的cpu 869元,DDR4 内存 1200块钱,b360主板300元 散热器50元 机械硬盘200元 350w电源300元 机箱100元 ,没错,只要3k就能拿到一个性能强大的6C6T电脑。

要说一台PC中最重要的部件是什么?大家看价格也会看明白,是cpu和内存,下面我来介绍一下cpu和内存之间的关系。

cpu与内存缓存的千丝万缕

首先说明一下相关的cpu术语:

  • core:也就是物理核心了。core这个词是英特尔起的,起初是为了与竞争对手AMD区别开,后面用的多了也淡了。
  • thread:就是硬件线程数。一个程序执行可能需要多个线程一起进行~而现在也就比较强大的超线程技术,过去的cpu往往一个cpu核心只支持一个线程,现在一些强大的cpu中,就譬如IBM 的POWER 9 ,支持8核心32个线程(平均一个核心4个线程),理论性能非常强大。

我们都知道,cpu将要处理的数据会放到内存中保存,可是,为什么会这样,将内存缓存硬盘行不行呢?

答案当然是不行的。cpu的处理速度很强大,内存的速度虽然非常快速但是根本跟不上cpu的步伐,所以,就出现的缓存。与来自DRAM家族的内存不同,缓存SRAM与内存最大的特点是,特别快,容量小,结构复杂,成本也高。

造成内存和缓存性能差异,主要有以下原因:

  1. DRAM储存一位数据只需要一个电容加上一个晶体管,而SRAM需要6个晶体管。由于DRAM保存数据其实是在电容里面的,电容需要充放电才能进行读写操作,这就导致其读写数据就有比较大的延迟问题。
  2. 存储可以看错一个二维数组,每个存储单元都有其行地址列地址。SRAM的容量很小,其存储单元比较短(行列短),可以一次性传输到SRAM中;而DRAM,需要分别传送行列地址。
  3. SRAM的频率和cpu频率比较接近;而DRAM的频率和cpu差距比较大。

近代的缓存通常被集成到cpu当中,为了适应性能与成本的需要,现实中的缓存往往使用金字塔型多级缓存架构。也就是当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。

下面是英特尔最近以来用的初代skylake架构

可以看到,每个个核心有专属的L1,L2缓存,他们共享一个L3缓存。如果cpu如果要访问内存中的数据,必须要经过L1,L2,L3,LLC(或者L4)四层缓存。

最开始的cpu,其实只是一个核心一个线程的,当时根本不需要考虑缓存一致性问题,单线程,也就是cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。

后来超线程技术来到我们视野,''单核CPU多线程'',也就是进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。

时代不断发展,“多核CPU多线程”来了,即多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。

这就是我们说的缓存一致性问题。

目前公认最好的解决方案是英特尔的MESI协议,下面我们着重介绍。

首先说说I/O操作的单位问题,大部分人都知道,在内存中操作I/O不是以字节为单位,而是以“块”为单位,这是为什么呢?

其实这是因为I/O操作的数据访问有空间连续性特征,即需要访问内存空间很多数据,但是I/O操作比较慢,读一个字节和读N个字节的时间基本相同。

机智的intel就规定了,cpu缓存中最小的存储单元是缓存行cache line,在x86的cpu中,一个cache line储存64字节,每一级的缓存都会被划分成许多组cache line

接下来我们看看MESI规范,这其实是用四种缓存行状态命名的,我们定义了CPU中每个缓存行使用4种状态进行标记(使用额外的两位(bit)表示),分别是:

  • 该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。

  • 该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。

  • 该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。

  • 该缓存是无效的(可能有其它CPU修改了该缓存行)。

然而,只是有这四种状态也会带来一定的问题。下面引用一下oracle的。

同时更新来自不同处理器的相同缓存代码行中的单个元素会使整个缓存代码行无效,即使这些更新在逻辑上是彼此独立的。每次对缓存代码行的单个元素进行更新时,都会将此代码行标记为无效。其他访问同一代码行中不同元素的处理器将看到该代码行已标记为无效。即使所访问的元素未被修改,也会强制它们从内存或其他位置获取该代码行的较新副本。这是因为基于缓存代码行保持缓存一致性,而不是针对单个元素的。因此,互连通信和开销方面都将有所增长。并且,正在进行缓存代码行更新的时候,禁止访问该代码行中的元素。

MESI协议,可以保证缓存的一致性,但是无法保证实时性。这种情况称为伪共享

伪共享问题其实在Java中是真实存在的一个问题。假设有如下所示的java class

按照java规范,MyObiect对象是在堆空间中分配的,a、b、c这三个变量在内存空间中是近邻,分别占8字节,长度之和为24字节。而我们的x86的缓存行是64字节,这三个变量完全有可能会在一个缓存行中,并且被两个不同的cpu核心共享!

根据MESI协议,如果不同物理核心cpu中的线程1和线程2要互斥的对这几个变量进行操作,很有可能要互相抢占资源,导致原来的并行变成串行,大大降低了系统的并发性,这就是缓存的伪共享。

其实解决伪共享很简单,只需要将这几个变量分别放到不同的缓存行即可。在java8中,就已经提供了普适性的解决方案,即采用@Contended注解来保证对象中的变量或者属性不在一个缓存行中~

上面我说了MESI协议在多核心cpu中解决缓存一致性的问题,下面我们说说cpu的内存不一致性问题。

首先,要了解三个名词:

,对称多处理系统内有许多紧耦合多处理器,在这样的系统中,所有的CPU共享全部资源,如总线,内存和I/O系统等,操作系统或管理数据库的复本只有一个,这种系统有一个最大的特点就是共享所有资源。多个CPU之间没有区别,平等地访问内存、外设、一个操作系统。操作系统管理着一个队列,每个处理器依次处理队列中的进程。如果两个处理器同时请求访问一个资源(例如同一段内存地址),由硬件、软件的锁机制去解决资源争用问题。
所谓对称多处理器结构,是指服务器中多个 CPU 对称工作,无主次或从属关系。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。

SMP 服务器的主要特征是共享,系统中所有资源 (CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了 SMP 服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成 SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着 CPU 数量的增加,内存访问冲突将迅速增加,最终会造成 CPU 资源的浪费,使 CPU 性能的有效性大大降低。实验证明, SMP 服务器 CPU 利用率最好的情况是 2 至 4 个 CPU 。

  由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA 就是这种努力下的结果之一。利用 NUMA 技术,可以把几十个 CPU( 甚至上百个 CPU) 组合在一个服务器内。其NUMA 服务器 CPU 模块结构如图所示:
NUMA 服务器的基本特征是具有多个 CPU 模块,每个 CPU 模块由多个 CPU( 如 4 个 ) 组成,并且具有独立的本地内存、 I/O 槽口等。由于其节点之间可以通过互联模块 ( 如称为 Crossbar Switch) 进行连接和信息交互,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) 。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它节点的内存 ) 的速度,这也是非一致存储访问 NUMA 的由来。由于这个特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。

利用 NUMA 技术,可以较好地解决原来 SMP 系统的扩展问题,在一个物理服务器内可以支持上百个 CPU 。比较典型的 NUMA 服务器的例子包括 HP 的 Superdome 、 SUN15K 、 IBMp690 等。

  但 NUMA 技术同样有一定缺陷,由于访问远地内存的延时远远超过本地内存,因此当 CPU 数量增加时,系统性能无法线性增加。如 HP 公司发布 Superdome 服务器时,曾公布了它与 HP 其它 UNIX 服务器的相对性能值,结果发现, 64 路 CPU 的 Superdome (NUMA 结构 ) 的相对性能值是 20 ,而 8 路 N4000( 共享的 SMP 结构 ) 的相对性能值是 6.3 。从这个结果可以看到, 8 倍数量的 CPU 换来的只是 3 倍性能的提升。

  和 NUMA 不同, MPP 提供了另外一种进行系统扩展的方式,它由多个 SMP 服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务,从用户的角度来看是一个服务器系统。其基本特征是由多个 SMP 服务器 ( 每个 SMP 服务器称节点 ) 通过节点互联网络连接而成,每个节点只访问自己的本地资源 ( 内存、存储等 ) ,是一种完全无共享 (Share Nothing) 结构,因而扩展能力最好,理论上其扩展无限制,目前的技术可实现 512 个节点互联,数千个 CPU 。目前业界对节点互联网络暂无标准,如 NCR 的 Bynet , IBM 的 SPSwitch ,它们都采用了不同的内部实现机制。但节点互联网仅供 MPP 服务器内部使用,对用户而言是透明的。

  在 MPP 系统中,每个 SMP 节点也可以运行自己的操作系统、数据库等。但和 NUMA 不同的是,它不存在异地内存访问的问题。换言之,每个节点内的 CPU 不能访问另一个节点的内存。节点之间的信息交互是通过节点互联网络实现的,这个过程一般称为数据重分配 (Data Redistribution) 。

但是 MPP 服务器需要一种复杂的机制来调度和平衡各个节点的负载和并行处理过程。目前一些基于 MPP 技术的服务器往往通过系统级软件 ( 如数据库 ) 来屏蔽这种复杂性。举例来说, NCR 的 Teradata 就是基于 MPP 技术的一个关系数据库软件,基于此数据库来开发应用时,不管后台服务器由多少个节点组成,开发人员所面对的都是同一个数据库系统,而不需要考虑如何调度其中某几个节点的负载。

MPP (Massively Parallel Processing),大规模并行处理系统,这样的系统是由许多松耦合的处理单元组成的,要注意的是这里指的是处理单元而不是处理器。每个单元内的CPU都有自己私有的资源,如总线,内存,硬盘等。在每个单元内都有操作系统和管理数据库的实例复本。这种结构最大的特点在于不共享资源。

NUMA结构下的缓存一致性

要知道,MESI协议解决的是传统SMP结构下缓存的一致性,为了在NUMA架构也实现缓存一致性,intel引入了MESI的一个拓展协议--MESIF,但是目前并没有什么资料,也没法研究,更多消息请查阅intel的wiki。

我们写程序,为什么要考虑内存模型呢,我们前面说了,缓存一致性问题、内存一致问题是硬件的不断升级导致的。解决问题,最简单直接的做法就是废除CPU缓存,让CPU直接和主存交互。但是,这么做虽然可以保证多线程下的并发问题。但是,这就有点时代倒退了。

所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。

即为了保证共享内存的正确性(可见性、有序性、原子性),需要内存模型来定义了共享内存系统中多线程程序读写操作行为的相应规范~

Java内存模型是根据英文Java Memory Model(JMM)翻译过来的。其实JMM并不像JVM内存结构一样是真实存在的。它是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。就像JSR-133: Java Memory Model and Thread Specification 中描述了,JMM是和多线程相关的,他描述了一组规则或规范,这个规范定义了一个线程对共享变量的写入时对另一个线程是可见的。

那么,简单总结下,Java的多线程之间是通过共享内存进行通信的,而由于采用共享内存进行通信,在通信过程中会存在一系列如可见性、原子性、顺序性等问题,而JMM就是围绕着多线程通信以及与其相关的一系列特性而建立的模型。JMM定义了一些语法集,这些语法集映射到Java语言中就是volatilesynchronized等关键字。

在JMM中,我们把多个线程间通信的共享内存称之为主内存,而在并发编程中多个线程都维护了一个自己的本地内存(这是个抽象概念),其中保存的数据是主内存中的数据拷贝。而JMM主要是控制本地内存和主内存之间的数据交互的

在Java中,JMM是一个非常重要的概念,正是由于有了JMM,Java的并发编程才能避免很多问题。

了解Java多线程的朋友都知道,在Java中提供了一系列和并发处理相关的关键字,比如volatilesynchronizedfinalconcurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给我们使用的一些关键字。

在开发多线程的代码的时候,我们可以直接使用synchronized等关键字来控制并发,从来就不需要关心底层的编译器优化、缓存一致性等问题。所以,Java内存模型,除了定义了一套规范,还提供了一系列原语,封装了底层实现后,供开发者直接使用。

并发编程要解决原子性、有序性和可见性的问题,我们就再来看下,在Java中,分别使用什么方式来保证。

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。

JMM提供保证了访问基本数据类型的原子性(其实在写一个工作内存变量到主内存是分主要两步:store、write),但是实际业务处理场景往往是需要更大的范围的原子性保证。

在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit,而这两个字节码,在Java中对应的关键字就是synchronized

因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。这里推荐一篇文章。

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

有序性即程序执行的顺序按照代码的先后顺序执行。

在Java中,可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别:

volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。

但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

我们都知道,Java代码是要运行在虚拟机上的,而虚拟机在执行Java程序的过程中会把所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途。下面我们来说说JVM运行时内存区域结构

JVM运行时内存区域结构

在《Java虚拟机规范(Java SE 8)》中描述了JVM运行时内存区域结构如下:

Register),也有称作为PC寄存器。想必学过汇编语言的朋友对程序计数器这个概念并不陌生,在汇编语言中,程序计数器是指CPU中的寄存器,它保存的是程序当前执行的指令的地址(也可以说保存下一条指令的所在存储单元的地址),当CPU需要执行指令时,需要从程序计数器中得到当前需要执行的指令所在存储单元的地址,然后根据得到的地址获取到指令,在得到指令之后,程序计数器便自动加1或者根据转移指针得到下一条指令的地址,如此循环,直至执行完所有的指令。

  虽然JVM中的程序计数器并不像汇编语言中的程序计数器一样是物理概念上的CPU寄存器,但是JVM中的程序计数器的功能跟汇编语言中的程序计数器的功能在逻辑上是等同的,也就是说是用来指示 执行哪条指令的。

  由于在JVM中,多线程是通过线程轮流切换来获得CPU执行时间的,因此,在任一具体时刻,一个CPU的内核只会执行一条线程中的指令,因此,为了能够使得每个线程都在线程切换后能够恢复在切换之前的程序执行位置,每个线程都需要有自己独立的程序计数器,并且不能互相被干扰,否则就会影响到程序的正常执行次序。因此,可以这么说,程序计数器是每个线程所私有的。

  在JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。

  由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的。

  Java栈也称作虚拟机栈(Java Vitual Machine Stack),也就是我们常常所说的栈,跟C语言的数据段中的栈类似。事实上,Java栈是Java方法执行的内存模型。为什么这么说呢?下面就来解释一下其中的原因。

  Java栈中存放的是一个个的栈帧,每个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之创建一个对应的栈帧,并将建立的栈帧压栈。当方法执行完毕之后,便会将栈帧出栈。因此可知,线程当前执行的方法所对应的栈帧必定位于Java栈的顶部。讲到这里,大家就应该会明白为什么 在 使用 递归方法的时候容易导致栈内存溢出的现象了以及为什么栈区的空间不用程序员去管理了(当然在Java中,程序员基本不用关系到内存分配和释放的事情,因为Java有自己的垃圾回收机制),这部分空间的分配和释放都是由系统自动实施的。对于所有的程序设计语言来说,栈这部分空间对程序员来说是不透明的。下图表示了一个Java栈的模型:

  局部变量表,顾名思义,想必不用解释大家应该明白它的作用了吧。就是用来存储方法中的局部变量(包括在方法中声明的非静态变量以及函数形参)。对于基本数据类型的变量,则直接存储它的值,对于引用类型的变量,则存的是指向对象的引用。局部变量表的大小在编译器就可以确定其大小了,因此在程序执行期间局部变量表的大小是不会改变的。

  操作数栈,想必学过数据结构中的栈的朋友想必对表达式求值问题不会陌生,栈最典型的一个应用就是用来对表达式求值。想想一个线程执行方法的过程中,实际上就是不断执行语句的过程,而归根到底就是进行计算的过程。因此可以这么说,程序中的所有计算过程都是在借助于操作数栈来完成的。

  指向运行时常量池的引用,因为在方法执行的过程中有可能需要用到类中的常量,所以必须要有一个引用指向运行时常量。

  方法返回地址,当一个方法执行完毕之后,要返回之前调用它的地方,因此在栈帧中必须保存一个方法返回地址。

  由于每个线程正在执行的方法可能不同,因此每个线程都会有一个自己的Java栈,互不干扰。

  本地方法栈与Java栈的作用和原理非常相似。区别只不过是Java栈是为执行Java方法服务的,而本地方法栈则是为执行本地方法(Native Method)服务的。在JVM规范中,并没有对本地方发展的具体实现方法以及数据结构作强制规定,虚拟机可以自由实现它。在HotSopt虚拟机中直接就把本地方法栈和Java栈合二为一。

  在C语言中,堆这部分空间是唯一一个程序员可以管理的内存区域。程序员可以通过malloc函数和free函数在堆上申请和释放空间。那么在Java中是怎么样的呢?

  Java中的堆是用来存储对象本身的以及数组(当然,数组引用是存放在Java栈中的)。只不过和C语言中的不同,在Java中,程序员基本不用去关心空间释放的问题,Java的垃圾回收机制会自动进行处理。因此这部分空间也是Java垃圾收集器管理的主要区域。另外,堆是被所有线程共享的,在JVM中只有一个堆。

  方法区在JVM中也是一个非常重要的区域,它与堆一样,是被线程共享的区域。在方法区中,存储了每个类的信息(包括类的名称、方法信息、字段信息)、静态变量、常量以及编译器编译后的代码等。

  在Class文件中除了类的字段、方法、接口等描述信息外,还有一项信息是常量池,用来存储编译期间生成的字面量和符号引用。

  在方法区中有一个非常重要的部分就是运行时常量池,它是每一个类或接口的常量池的运行时表示形式,在类和接口被加载到JVM后,对应的运行时常量池就被创建出来。当然并非Class文件常量池中的内容才能进入运行时常量池,在运行期间也可将新的常量放入运行时常量池中,比如String的intern方法。

  在JVM规范中,没有强制要求方法区必须实现垃圾回收。很多人习惯将方法区称为“永久代”,是因为HotSpot虚拟机以永久代来实现方法区,从而JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。不过自从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。

Java对象模型的内存布局

java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的。而这个关于Java对象自身的存储模型称之为Java对象模型。

每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,对象在内存中存储的布局可以分为3块区域:对象头(Header)、 实例数据(Instance

  1. 对象头:标记字(32位虚拟机4B,64位虚拟机8B) + 类型指针(32位虚拟机4B,64位虚拟机8B)+ [数组长(对于数组对象才需要此部分信息)]
  2. 实例数据:存储的是真正有效数据,如各种字段内容,各字段的分配策略为longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面。
  3. 对齐填充:对于64位虚拟机来说,对象大小必须是8B的整数倍,不够的话需要占位填充

为了理解现有收集器,我们需要先了解一些术语。最基本的垃圾收集涉及识别不再使用的内存并使其可重用。现代收集器在几个阶段进行这一过程,对于这些阶段我们往往有如下描述:

  • 并行- 在JVM运行时,同时存在应用程序线程和垃圾收集器线程。 并行阶段是由多个gc线程执行,即gc工作在它们之间分配。 不涉及GC线程是否需要暂停应用程序线程。
  • 串行- 串行阶段仅在单个gc线程上执行。与之前一样,它也没有说明GC线程是否需要暂停应用程序线程。
  • STW - STW阶段,应用程序线程被暂停,以便gc执行其工作。 当应用程序因为GC暂停时,这通常是由于Stop The World阶段。
  • 并发 -如果一个阶段是并发的,那么GC线程可以和应用程序线程同时进行。 并发阶段很复杂,因为它们需要在阶段完成之前处理可能使工作无效(译者注:因为是并发进行的,GC线程在完成一阶段的同时,应用线程也在工作产生操作内存,所以需要额外处理)的应用程序线程。
  • 增量 -如果一个阶段是增量的,那么它可以运行一段时间之后由于某些条件提前终止,例如需要执行更高优先级的gc阶段,同时仍然完成生产性工作。 增量阶段与需要完全完成的阶段形成鲜明对比。

Serial收集器是最基本的收集器,这是一个单线程收集器,它仍然是JVM在Client模式下的默认新生代收集器。它有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比较),Serial收集器由于没有线程交互的开销,专心只做垃圾收集自然也获得最高的效率。在用户桌面场景下,分配给JVM的内存不会太多,停顿时间完全可以在几十到一百多毫秒之间,只要收集不频繁,这是完全可以接受的。

ParNew是Serial的多线程版本,在回收算法、对象分配原则上都是一致的。ParNew收集器是许多运行在Server模式下的默认新生代垃圾收集器,其主要在于除了Serial收集器,目前只有ParNew收集器能够与CMS收集器配合工作。

Parallel Scavenge收集器是一个新生代垃圾收集器,其使用的算法是复制算法,也是并行的多线程收集器。

Parallel Scavenge 收集器更关注可控制的吞吐量,吞吐量等于运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。

停顿时间越短就越适合需要与用户交互的程序;而高吞吐量则可以最高效的利用CPU的时间,尽快的完成计算任务,主要适用于后台运算。

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的收集器,CMS收集器采用标记--清除算法,运行在老年代。主要包含以下几个步骤:

其中初始标记和重新标记仍然需要“Stop the world”。初始标记仅仅标记GC Root能直接关联的对象,并发标记就是进行GC Root Tracing过程,而重新标记则是为了修正并发标记期间,因用户程序继续运行而导致标记变动的那部分对象的标记记录。

由于整个过程中最耗时的并发标记和并发清除,收集线程和用户线程一起工作,所以总体上来说,CMS收集器回收过程是与用户线程并发执行的。虽然CMS优点是并发收集、低停顿,很大程度上已经是一个不错的垃圾收集器,但是还是有三个显著的缺点:

  1. CMS收集器对CPU资源很敏感。在并发阶段,虽然它不会导致用户线程停顿,但是会因为占用一部分线程(CPU资源)而导致应用程序变慢。
  2. CMS收集器不能处理浮动垃圾。所谓的“浮动垃圾”,就是在并发标记阶段,由于用户程序在运行,那么自然就会有新的垃圾产生,这部分垃圾被标记过后,CMS无法在当次集中处理它们,只好在下一次GC的时候处理,这部分未处理的垃圾就称为“浮动垃圾”。也是由于在垃圾收集阶段程序还需要运行,即还需要预留足够的内存空间供用户使用,因此CMS收集器不能像其他收集器那样等到老年代几乎填满才进行收集,需要预留一部分空间提供并发收集时程序运作使用。要是CMS预留的内存空间不能满足程序的要求,这是JVM就会启动预备方案:临时启动Serial Old收集器来收集老年代,这样停顿的时间就会很长。
  3. 由于CMS使用标记--清除算法,所以在收集之后会产生大量内存碎片。当内存碎片过多时,将会给分配大对象带来困难,这是就会进行Full GC。

G1收集器与CMS相比有很大的改进:

· G1收集器采用标记--整理算法实现。

· 可以非常精确地控制停顿。

? G1收集器可以实现在基本不牺牲吞吐量的情况下完成低停顿的内存回收,这是由于它极力的避免全区域的回收,G1收集器将Java堆(包括新生代和老年代)划分为多个区域(Region),并在后台维护一个优先列表,每次根据允许的时间,优先回收垃圾最多的区域 。

Java 11 新加入的ZGC垃圾收集器号称可以达到10ms 以下的 GC 停顿,ZGC给Hotspot Garbage Collectors增加了两种新技术:着色指针和读屏障。下面引用国外文章说的内容:

着色指针是一种将信息存储在指针(或使用Java术语引用)中的技术。因为在64位平台上(ZGC仅支持64位平台),指针可以处理更多的内存,因此可以使用一些位来存储状态。 ZGC将限制最大支持4Tb堆(42-bits),那么会剩下22位可用,它目前使用了4位: finalizableremapmark0mark1。 我们稍后解释它们的用途。

着色指针的一个问题是,当您需要取消着色时,它需要额外的工作(因为需要屏蔽信息位)。 像SPARC这样的平台有内置硬件支持指针屏蔽所以不是问题,而对于x86平台来说,ZGC团队使用了简洁的多重映射技巧。

要了解多重映射的工作原理,我们需要简要解释虚拟内存和物理内存之间的区别。 物理内存是系统可用的实际内存,通常是安装的DRAM芯片的容量。 虚拟内存是抽象的,这意味着应用程序对(通常是隔离的)物理内存有自己的视图。 操作系统负责维护虚拟内存和物理内存范围之间的映射,它通过使用页表和处理器的内存管理单元(MMU)和转换查找缓冲器(TLB)来实现这一点,后者转换应用程序请求的地址。

多重映射涉及将不同范围的虚拟内存映射到同一物理内存。 由于设计中只有一个remapmark0mark1在任何时间点都可以为1,因此可以使用三个映射来完成此操作。 ZGC源代码中有一个很好的图表可以说明这一点。

读屏障是每当应用程序线程从堆加载引用时运行的代码片段(即访问对象上的非原生字段non-primitive field):

// 因为需要从heap读取引用

在上面的代码中,String name = person.name 访问了堆上的person引用,然后将引用加载到本地的name变量。此时触发读屏障。 Systemt.out那行不会直接触发读屏障,因为没有来自堆的引用加载(name是局部变量,因此没有从堆加载引用)。 但是System和out,或者println内部可能会触发其他读屏障。

这与其他GC使用的写屏障形成对比,例如G1。读屏障的工作是检查引用的状态,并在将引用(或者甚至是不同的引用)返回给应用程序之前执行一些工作。 在ZGC中,它通过测试加载的引用来执行此任务,以查看是否设置了某些位。 如果通过了测试,则不执行任何其他工作,如果失败,则在将引用返回给应用程序之前执行某些特定于阶段的任务。

现在我们了解了这两种新技术是什么,让我们来看看ZG的GC循环。

GC循环的第一部分是标记。标记包括查找和标记运行中的应用程序可以访问的所有堆对象,换句话说,查找不是垃圾的对象。

ZGC的标记分为三个阶段。 第一阶段是STW,其中GC roots被标记为活对象。 GC roots类似于局部变量,通过它可以访问堆上其他对象。 如果一个对象不能通过遍历从roots开始的对象图来访问,那么应用程序也就无法访问它,则该对象被认为是垃圾。从roots访问的对象集合称为Live集。GC roots标记步骤非常短,因为roots的总数通常比较小。

该阶段完成后,应用程序恢复执行,ZGC开始下一阶段,该阶段同时遍历对象图并标记所有可访问的对象。 在此阶段期间,读屏障针使用掩码测试所有已加载的引用,该掩码确定它们是否已标记或尚未标记,如果尚未标记引用,则将其添加到队列以进行标记。

在遍历完成之后,有一个最终的,时间很短的的Stop The World阶段,这个阶段处理一些边缘情况(我们现在将它忽略),该阶段完成之后标记阶段就完成了。

GC循环的下一个主要部分是重定位。重定位涉及移动活动对象以释放部分堆内存。 为什么要移动对象而不是填补空隙? 有些GC实际是这样做的,但是它导致了一个不幸的后果,即分配内存变得更加昂贵,因为当需要分配内存时,内存分配器需要找到可以放置对象的空闲空间。 相比之下,如果可以释放大块内存,那么分配内存就很简单,只需要将指针递增新对象所需的内存大小即可。

ZGC将堆分成许多页面,在此阶段开始时,它同时选择一组需要重定位活动对象的页面。选择重定位集后,会出现一个Stop The World暂停,其中ZGC重定位该集合中root对象,并将他们的引用映射到新位置。与之前的Stop The World步骤一样,此处涉及的暂停时间仅取决于root的数量以及重定位集的大小与对象的总活动集的比率,这通常相当小。所以不像很多收集器那样,暂停时间随堆增加而增加。

移动root后,下一阶段是并发重定位。 在此阶段,GC线程遍历重定位集并重新定位其包含的页中所有对象。 如果应用程序线程试图在GC重新定位对象之前加载它们,那么应用程序线程也可以重定位该对象,这可以通过读屏障(在从堆加载引用时触发)

这可确保应用程序看到的所有引用都已更新,并且应用程序不可能同时对重定位的对象进行操作。

GC线程最终将对重定位集中的所有对象重定位,然而可能仍有引用指向这些对象的旧位置。 GC可以遍历对象图并重新映射这些引用到新位置,但是这一步代价很高昂。 因此这一步与下一个标记阶段合并在一起。在下一个GC周期的标记阶段遍历对象对象图的时候,如果发现未重映射的引用,则将其重新映射,然后标记为活动状态。

在《深入理解Java虚拟机》一书中讲了很多jvm优化思路,下面我来简单说说。

堆内存都有一定的大小,能容纳的数据是有限制的,当Java堆的大小太大时,垃圾收集会启动停止堆中不再应用的对象,来释放内存。现在,内存抖动这个术语可用于描述在极短时间内分配给对象的过程。 具体如何优化请谷歌查询~

CPU是通过寻址来访问内存的。32位CPU的寻址宽度是 0~0xFFFFFFFF,即4G,也就是说可支持的物理内存最大是4G。但在实践过程中,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。为了解决此类问题,现代CPU引入了MMU(Memory Management Unit,内存管理单元)。

MMU 的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由MMU负责将虚址映射为物理地址。MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。

内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是虚拟内存。

要知道,虚拟地址与物理地址需要通过映射,才能使CPU正常工作。而映射就需要存储映射表。在现代CPU架构中,映射关系通常被存储在物理内存上一个被称之为页表(page table)的地方。 页表是被存储在内存中的,CPU通过总线访问内存,肯定慢于直接访问寄存器的。为了进一步优化性能,现代CPU架构引入了TLB(Translation lookaside buffer,页表寄存器缓冲),用来缓存一部分经常访问的页表内容 。

为什么要支持大内存分页?

TLB是有限的,这点毫无疑问。当超出TLB的存储极限时,就会发生 TLB miss,于是OS就会命令CPU去访问内存上的页表。如果频繁的出现TLB miss,程序的性能会下降地很快。

为了让TLB可以存储更多的页地址映射关系,我们的做法是调大内存分页大小。

如果一个页4M,对比一个页4K,前者可以让TLB多存储1000个页地址映射关系,性能的提升是比较可观的。

通过软引用和弱引用提升JVM内存使用性能

只要引用存在,垃圾回收器永远不会回收

而这样 obj对象对后面new Object的一个强引用,只有当obj这个引用被释放之后,对象才会被释放掉,这也是我们经常所用到的编码形式。

  1. 软引用(可以实现缓存):

非必须引用,内存溢出之前进行回收,可以通过以下代码实现

这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null;软引用主要用户实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。

  1. 弱引用(用来在回调函数中防止内存泄露):

第二次垃圾回收时回收,可以通过如下代码实现

wf.isEnQueued();//返回是否被垃圾回收器标记为即将回收的垃圾

弱引用是在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。弱引用主要用于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。

垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现

虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被成为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。

简单来说,可以使用软引用还引用数量巨大的对象,详情请参考

此篇文章总共1.5W字,我从计算机物理内存体系讲到了java内存模型,在通过java内存模型引出了JVM内存的相关知识点。觉得写的好的请给个赞。本篇文章我会率先发布在我的个人博客,随后会在掘金等平台相继发出。最后,非常感谢你的阅读~

《深入理解Java虚拟机》

《Java并发编程的艺术》

《架构解密从分布式到微服务》

【版权申明】此片为原创内容,使用MIT授权条款,请遵守对应的义务,即被授权人有义务在所有副本中都必须包含版权声明。谢谢合作~

想要解锁更多新姿势?请访问我的个人博客(?

您购买此商品可享受专属价

选择搭配赠品(共0个)

我要回帖

更多关于 内存不兼容 的文章

 

随机推荐