CPU模型,内存分页与调优,内核与用户空间
学习此篇文章需要
先学习java内存基础
再学习JVM8基础结构图理解
1 CPU模型
去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个核,这就意味着多个CPU或者多个核可以同时(并发)工作
1.1 CPU Register
CPU Register
也就是 CPU 寄存器
。CPU寄存器
是 CPU
内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级
在CPU
中至少要有六类寄存器:指令寄存器(IR)、程序计数器(PC)、地址寄存器(AR)、数据寄存器(DR)、累加寄存器(AC)、程序状态字寄存器(PSW)。这些寄存器用来暂存一个计算机字,其数目可以根据需要进行扩充
按与CPU远近来分
,离得最近的是寄存器
,然后缓存
,最后内存
。所以,寄存器是最贴近CPU
的,而且CPU只与寄存器中进行存取。寄存器从内存中读取数据,但由于寄存器和内存读取速度相差太大,所以有了缓存
。即读取数据的方式为:
CPU〈------〉寄存器〈---->缓存<----->内存
当寄存器没有从缓存中读取到数据时,也就是没有命中,那么就从内存中读取数据
1.2 CPU Cache Memory
CPU Cache Memory
也就是CPU
高速缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU
还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。
CPU
内部集成的缓存称为一级缓存(L1 Cache
),外部的称为二级缓存(L2 Cache
)。
一级缓存中又分为数据缓存(D-Cache
)和指令缓存(I-Cache
)。二者可以同时被CPU进行访问,减少了争用Cache
所造成的冲突,提高了CPU的效能。
CPU的一级缓存通常都是静态RAM
(Static RAM/SRAM),速度非常快,但是贵
二级缓存
是CPU
性能表现的关键之一,在CPU核心不变化的情况下,增加二级缓存容量能使性能大幅度提高。而同一核心的CPU高低端之分往往也是在二级缓存上存在差异
三级缓存
是为读取二级缓存后未命中的数据设计的一种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率,从某种意义上说,预取效率的提高,大大降低了生产成本却提供了非常接近理想状态的性能
1.3 Main Memory
Main Memory
就是主存,主存比 L1、L2
缓存要大很多
注意:部分高端机器还有 L3
三级缓存。
内存中相关概念:
- ROM(Read Only Memory)
只读储存器 ,对于电脑来讲就是硬盘,在系统停止供电的时候仍然可以保持数据 - PROM
PROM
是可编程的ROM
,PROM
和EPROM
(可擦除可编程ROM)两者区别是,PROM
是一次性的,也就是软件灌入后,就无法修改了,现在已经不可能使用了,而EPROM
是通过紫外光的照射擦除原先的程序,是一种通用的存储器。另外一种EEPROM
是通过电子擦除,价格很高,写入时间很长,写入很慢。 - RAM(Random Access Memory)
随机储存器 ,就是电脑内存条
。用于存放动态数据。(也叫运行内存)系统运行的时候,需要把操作系统从ROM
中读取出来,放在RAM
中运行,而RAM
通常都是在掉电之后就丢失数据,典型的RAM
就是计算机的内存 - 静态RAM(Static RAM/SRAM)
当数据被存入其中后不会消失。SRAM
速度非常快,是目前读写最快的存储设备。当这个SRAM
单元被赋予0 或者1 的状态之后,它会保持这个状态直到下次被赋予新的状态或者断电之后才会更改或者消失。需要4-6 只晶体管实现, 价格昂贵。
一级,二级,三级缓存都是使用SRAM
- 动态RAM(Dynamic RAM/DRAM)
DRAM
必须在一定的时间内不停的刷新才能保持其中存储的数据。DRAM
只要1 只晶体管就可以实现。
DRAM
保留数据的时间很短,速度也比SRAM
慢,不过它还是比任何的ROM都要快,但从价格上来说DRAM
相比SRAM
要便宜很 多,
计算机内存就是DRAM
的
1.4 主存存取原理
主存有两个指标:寻址
(ns,纳秒级别),带宽
(很大,指单位时间内字节流大小),在寻址上磁盘比内存慢了十万倍
目前计算机使用的主存基本都是随机读写存储器(RAM),现代RAM的结构和存取原理比较复杂,这里本文抛却具体差别,抽象出一个十分简单的存取模型来说明RAM的工作原理。
从抽象角度看,主存是一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据。每个存储单元有唯一的地址,现代主存的编址规则比较复杂,这里将其简化成一个二维地址:通过一个行地址和一个列地址可以唯一定位到一个存储单元。上图展示了一个4 x 4的主存模型。
主存的存取过程如下:
- 当系统需要读取主存时,则将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到数据总线上,供其它部件读取。
- 写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。
这里可以看出,主存存取的时间仅与存取次数呈线性关系,因为不存在机械操作,两次存取的数据的“距离”不会对时间有任何影响,例如,先取A0再取A1和先取A0再取D3的时间消耗是一样的。
1.5 磁盘存取原理
在磁盘的维度里有两个指标:寻址
(m/s:每秒几M)、带宽
(G/M,带宽是G或者M的,指单位时间内字节流大小)
与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。
下图是磁盘的整体结构示意图。
一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动(实际是斜切向运动),每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有多磁头独立技术,可不受此限制)。
下图是磁盘结构的示意图。
盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元。为了简单起见,我们下面假设磁盘只有一个盘片和一个磁头。
当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道
,所耗费时间叫做寻道时间
,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间
。
磁盘有磁道和扇区,一扇区 512Byte
。如果一个区域足够小,带来一个成本变大(索引)。操作系统,无论读多少数据,都是最少 4k
磁盘中拿,即:每个存储块称为一页
的大小
1.6 局部性原理与磁盘预读
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。
这样做的理论依据是计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)
的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为4k
),主存和磁盘以页为单位交换数据
。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
1.7 索引存储位置
假如文件很大了,对应得索引也会很大,以B+
树索引为例解析索引和文件存储位置:
数据
和索引
其实都是存储在硬盘当中的,然后这时候真正查的时候是要用到一个东西,就是在内存
里边准备一个B+
树,内存是速度最快的地方,所以在内存里边准备了一个B+
树, B+
树所有的叶子就是4k小格子。B+树其实数干是在内存里的
,比如说的区间和偏移,然后这个时候如果用户想查,只要查询的时候,注意索引在where
条件里,只要命中索引了,那么这个查询在B+
树会走树干,最终找到某一个叶子
比如身份证号,刚好在叶子代表这个区间里,那么把它从磁盘读到内存,把它解析完之后,最笨的方法,遍历完了之后,可以知道应该下一次把哪个data page
放到内存里边读进来,那么就可以找到我们那笔记录了。
由于需要从磁盘读到内存,如果把这些索引再堆到内存里的话,内存不够,存不下这些索引,所以 索引和数据都放在磁盘,内存里只存一个树干,只存一些区间
。
这样的话是充分利用了各自的能力,磁盘能存很多东西,然后内存速度快
,然后用一种数据结构可以加快遍历的一个查找的速度,然后数据又是分而治之的存储,所以这时候获取数据速度极快,最终的目的就是为了什么?减少I/O的流量
2 内存分页
虽然Java语言的这些特点很容易“惯坏”开发人员,使得我们不需要太关心到底程序是怎么使用内存的,使用了多少内存。但是我们最好也了解Java
是如何管理内存的,当我们真的遇到OutOfMemoryError
时不会奇怪地问,为什么Java也有内存泄漏:要快速地知道到底什么地方导致了 OutOfMemoryError
,并能根据错误日志快速地定位出错原因
2.1 物理内存与虚拟内存
2.1.1 物理内存
所谓物理内存就是我们通常所说的RAM
(随机存储器)。在计算机中,还有一个存储单元叫寄存器
,它用于存储计算单元执行指令(如浮点、整数等运算时)的中间结果。寄存器的大小决定了一次计算可使用的最大数值。
连接处理器和RAM
或者处理器和寄存器的是地址总线,这个地址总线的宽度影响了物理地址的索引范围,因为总线的宽度决定了处理器一次可以从寄存器或者内存中获取多少个bit
。同时也决定了处理器最大可以寻址的地址空间,如32位地址总线可以寻址的范围为(0x0000 0000〜0ffff ffff
。这个范围是2^32=4294967296个内存位置,每个地址会引用一个字节,所以32位
总线宽度可以有4GB
的内存空间。
通常情况下,地址总线
和寄存器
或者RAM
有相同的位数,因为这样更容易传输数据,但是也有不一致的情况,如x86
的32位
寄存器宽度的物理地址可能有两种大小,分别是32位
物理地址和36位
物理地址,拥有36位物理地址的是Pentium Pro和更高型号。
不管是在Windows
系统还是Linux
系统下,我们要运行程序,都要向操作系统先申请内存地址。 通常操作系统管理内存的申请空间是按照进程来管理的,每个进程拥有一段独立的地址空间,每个进程之间不会相互重合,操作系统也会保证每个进程只能访问自己的内存空间。
这主要是从程序的安全性来考虑的,也便于操作系统来管理物理内存。
其实上面所说的进程的内存空间的独立主要是指逻辑上独立,也就是这个独立是由操作系统来保证的,但是真正的物理空间是不是只能由一个进程来使用就不一定 了。因为随着程序越来越庞大和设计的多任务性,物理内存无法满足程序的需求,在这种情况下就有了虚拟内存的出现。
2.1.2 虚拟内存
虚拟内存的出现使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上共享
,在逻辑上它们仍然是不能相互访问的。虚拟地址不但可以让进程共享物理内存、提高内存利用率,而且还能够扩展内存的地址空间,如一个虚拟地址可能被映射到一段物理内存、文件或者其他可以寻址的存储上。
一个进程在不活动的情况下,操作系统将这个物理内存中的数据移到一个磁盘文件中(也就是通常windows
系统上的页面文件,或者Linux
系统上的SWAP
交换分区),而真正高效的物理内存留给正在活动的程序使用。在这种情况下,在我们重新唤醒一个很长时间没有使用的程序时,磁盘会吱吱作响,并且会有一个短暂的停顿得到印证,这时操作系统又会把磁盘上的数据重新交互到物理内存中。但是我们必须要避免这种情况的经常出现,如果操作系统频繁地交互物理内存的数据和磁盘数据,则效率将会非常低,尤其是在Linux服务器上,我们要关注Linux中swap的分区的活跃度。
如果swap分区被频繁使用,系统将会非常缓慢,很可能意味着物理内存已经严重不足或者某些程序没有及时释放内存。
在Linux
分区中SWAP
意思是交换,顾名思义,当某进程向OS
请求内存发现不足时,OS
会把内存中暂时不用的数据交换出去,放在SWAP
分区中,这个过程称为SWAP OUT
。当某进程又需要这些数据且OS
发现还有空闲物理内存时,又会把SWAP
分区中的数据交换回物理内存中,这个过程称为SWAP IN
2.2 内存分页大小对性能提升原理
首先,我们需要回顾一小部分计算机组成原理,这对理解大内存分页至于JVM
性能的提升是有好处的。
2.2.1 什么是内存分页
我们知道,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
能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。
页表其实是一个数组用于把虚拟页号
和物理帧号
对应起来
其索引为VPN
(虚拟地址) 索引值对应的项为PTE
(页表项) 项中的值为PFN
(物理页帧),每个PTE
中还有很多别的内容,比如有很多不同的位:有效位、保护位、存在位、脏位、参考位
虚拟地址
与物理地址
需要通过映射,才能使CPU
正常工作。而映射就需要存储映射表
。在现代CPU
架构中,映射关系通常被存储在物理内存上一个被称之为页表(page table)
的地方。
如下图:
从这张图中,可以清晰地看到CPU
与页表,物理内存之间的交互关系。
由于页表是被存储在内存中的。我们知道CPU
通过总线访问内存,肯定慢于直接访问寄存器的。为了进一步优化性能,现代CPU
架构引入了TLB
(Translation lookaside buffer
,页表寄存器缓冲
),用来缓存一部分经常访问的页表内容。
如下图:
2.2.2 为什么要支持大内存分页
TLB
是有限的,这点毫无疑问。当超出TLB
的存储极限时,就会发生 TLB miss
,之后,OS
就会命令CPU
去访问内存上的页表。如果频繁的出现TLB miss
,程序的性能会下降地很快。
为了让TLB
可以存储更多的页地址映射关系,我们的做法是调大内存分页大小。
如果一个页4M
,对比一个页4K,前者可以让TLB多存储1000个页地址映射关系,性能的提升是比较可观的。
2.3 调整OS和JVM内存分页
在Linux
和windows
下要启用大内存页,有一些限制和设置步骤。
2.3.1 Linux
Linux限制:需要2.6内核以上或2.4内核已打大内存页补丁。
确认是否支持,请在终端敲如下命令:
# cat /proc/meminfo | grep Huge
HugePages_Total: 0
HugePages_Free: 0
Hugepagesize: 2048 kB
如果有HugePage
字样的输出内容,说明OS
是支持大内存分页的。Hugepagesize
就是默认的大内存页size
接下来,为了让JVM
可以调整大内存页size
,需要设置下OS
共享内存段最大值和大内存页数量。
2.3.1.1 共享内存段最大值
建议这个值大于Java Heap size
,这个例子里设置了4G内存。
# echo 4294967295 > /proc/sys/kernel/shmmax
2.3.1.2 大内存页数量
# echo 154 > /proc/sys/vm/nr_hugepages
这个值一般是 Java
进程占用最大内存/单个页的大小 ,比如java设置 1.5G,单个页 10M,那么数量为 1536/10 = 154。
注意
:因为proc
是内存FS
,为了不让设置在重启后被冲掉,建议写个脚本放到 init 阶段(rc.local)。
2.3.2 Windows
Windows限制:仅支持 windows server 2003 以上server版本
操作步骤:
- Control Panel -> Administrative Tools -> Local Security Policy
- Local Policies -> User Rights Assignment
- 双击
Lock pages in memory
, 添加用户和组 - 重启电脑
注意: 需要管理员操作。
2.3.3 单个页大小调整
JVM
启用时加参数 -XX:LargePageSizeInBytes=10m
如果JDK是在1.5 update5以前的,还需要手动加 -XX:+UseLargePages
,作用是启用大内存页支持。
2.4 大内存分页的副作用
因为每页size
变大了,导致JVM
在计算Heap
内部分区(perm, new, old)内存占用比例时,会出现超出正常值的划分。最坏情况下是,某个区会多占用一个页的大小。不过后续jvm版本也在调整这个策略。
一般情况,不建议将页size
调得太大,4-64M
,是可以接受的(默认是4M)
3 内核空间与用户空间
3.1 定义
一个计算机通常有一定大小的内存空间,如使用的计算机是4GB
的地址空间,但是程序并不能完全使用这些地址空间,因为这些地址空间被划分为内核空间
和用户空间
。程序只能使用用户空间的内存,这里所说的使用是 指程序能够申请的内存空间
,并不是程序真正访问的地址空间。
内核空间
主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源等的程序逻辑。
3.2 为何需要内核空间和用户空间的划分
由于每个进程都独立使用属于自己的内存一样,为了保证操作系统的稳定性,运行在操作系统中的 用户程序不能访问操作系统所使用的内存空间
。这也是从安全性上考虑的,如访问硬件资源只能由操作系统来发起,用户程序不允许直接访问硬件资源。如果用户程序需要访问硬件资源,如网络连接等,.可以调用操作系统提供的接口来实现,这个调用接口的过程也就是系统调用。
每一次系统调用都会存在两个内存空间的切换,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户空间的数据复制很费时,虽然保住了程序运行的安全性和稳定性,但是也牺牲了一部分效率。但是现在已经出现了很多其他技术能够减少这种从内核空间到用户空间的数据复制的方式,如Linux系统提供了 sendflle文件传输方式。
内核空间和用户空间的大小如何分配也是一个问题,是更多地分配给用户空间供用户程序使用,还是首先保住内核有足够的空间来运行,这要平衡一下。如果是一台登录服务器,很显然,要分配更多的内核空间,因为每一个登录用户操作系统都会初始化一个用户进程,这个进程大部分都在内核空间里运行。在当前的 Windows 32
位操作系统中默认内核空间和用户空间的比例是1:1
(2GB的内核空间,2GB的用户空间),而在32位Linux
系统中默认的比例是1:3
(1GB的内核空间,3GB的用户空间)
3.3 内核用户空间切换的影响
java
的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态
与核心态
之间切换,这种切换会消耗大量的系统资源,因为用户态
与内核态
都有各自专用的内存空间,专用的寄存器等,用户态
切换至内核态
需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间
3.4 单核多核cpu与多线程
3.4.1 单核CPU为何也支持多线程呢
多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU
数,然而一颗CPU
同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢? 操作系统的设计者巧妙地利用了时间片轮转
的方式
时间片是CPU
分配给各个任务(线程)的时间
线程上下文
是指某一时间点CPU寄存器
和程序计数器(PC寄存器)
的内容,CPU
通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU
通过不停地切换线程执行。
换言之,单CPU
这么频繁,多核CPU
一定程度上可以减少上下文切换
3.4.2 超线程
现代CPU
除了处理器核心之外还包括寄存器、L1L2缓存这些存储设备、浮点运算单元、整数运算单元等一些辅助运算设备以及内部总线等。一个多核的CPU
也就是一个CPU
上有多个处理器核心,就意味着程序的不同线程需要经常在CPU
之间的外部总线上通信,同时还要处理不同CPU
之间不同缓存导致数据不一致的问题。
超线程
这个概念是Intel提出的,简单来说是在一个CPU
上真正的并发两个线程,由于CPU
都是分时的(如果两个线程A和B,A正在使用处理器核心,B正在使用缓存或者其他设备,那AB两个线程就可以并发执行,但是如果AB都在访问同一个设备,那就只能等前一个线程执行完后一个线程才能执行
)。实现这种并发的原理是 在CPU
里加了一个协调辅助核心,根据Intel提供的数据,这样一个设备会使得设备面积增大5%,但是性能提高15%~30%
3.4.3 线程上下文切换
线程上下文
:指某一时间点CPU寄存器
和程序计数器(PC寄存器)
的内容
线程上下文切换:
- 线程切换,同一进程中的两个线程之间的切换
- 进程切换,两个进程之间的切换
- 模式切换,在给定线程中,用户模式和内核模式的切换
- 地址空间切换,将虚拟内存切换到物理内存
CPU
切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行。任务的状态保存及再加载, 这段过程就叫做上下文切换
。
每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。
程序计数器
是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。
- 挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
- 恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
-
跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中]
3.4.4 线程上下文切换的影响
线程上下文
切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。
-
直接消耗
:指的是CPU
寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉 -
间接消耗
:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小
3.4.5 引起线程上下文切换的因素
引起线程上下文切换的因素:
- 当前执行任务(线程)的时间片用完之后,系统
CPU
正常调度下一个任务 - 中断处理,在中断处理中,其他程序
打断
了当前正在运行的程序。当CPU接收到中断请求时,会把正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断
和软件中断
,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。 - 用户态切换,对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
- 多个任务抢占锁资源,在多任务处理中,
CPU
会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换
因此优化手段有:
- 无锁并发编程,多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
-
CAS
算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁
使用最少线程 - 协程,单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销。
- 高并发,低耗时的情况,建议少线程。
- 低并发,高耗时的情况:建议多线程。
- 高并发高耗时,要分析任务类型、增加排队、加大线程数
3.4.6 切换查看
Linux
系统下可以使用vmstat
命令来查看上下文切换的次数, 其中cs
列就是指上下文切换的数目(一般情况下, 空闲系统的上下文切换每秒大概在1500以下)
3.4.7 Java线程调度
Java线程调度:
-
抢占式调度
指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
java
使用的线程调度使用抢占式调度,Java
中线程会按优先级分配CPU
时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。 -
协同式调度
指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
共有 0 条评论