1显存条、总线与DMA
计算机组成中显存或则叫寻址是十分重要的部件。显存由于地位太重要,所以和CPU直接相连,通过数据总线进行数据传输,并通过地址总线来进行数学地址的轮询。
不仅数据总线、地址总线还有控制总线、IO总线等。IO总线是拿来联接各类外设的,比如USB全称就是通用串行总线。再例如PCIE是目前最常见的IO总线之一。这儿放一张B站硬件茶谈的一张图。
图1-1硬件图
图中CPU和两侧显存条直接连,并通过PCIE总线与下方的PCIE插孔联接,在PCIE插孔上可以插主板,网卡,声卡,硬碟等等。PCIE带宽是共享的,假如某个设备用了x1路带宽,则能用的就少一路,由于本质上每一路都是串行的。北桥和CPU之间也有PCIE通道,主要是提供给一些带宽占用很低的外设。
北桥芯片坐落显卡上,通常在右下角,有个被动散热下边压着。北桥中有个很重要的设备就是DMA控制器,或则叫DMAC。DMA直接显存访问,意思就是DMAC就能直接访问显存。即通常进行IO的时侯,cpu会把总线完全交给DMAC(DMAC和CPU会分时掌控总线),DMAC访问设备如c盘,将数据读到显存中,由于此时接管了总线,所以可以写显存。在这个过程中CPU可以进行其他的任务。这也是异步IO、非阻塞IO等理论的基础。
计算机常试题:
图1-2-1题目1
图1-2-2题目2
2操作系统显存管理与分类2.1虚拟显存(逻辑显存)
win32程序从程序上能操作的逻辑地址空间有4G如此大(尽管实际可能用不了这么多),4G的逻辑地址须要全部映射到化学显存上。映射的最小单位假如是字节的话,映射表将会十分大,且效率低下。提出page概念,即最小的映射单位是一个page,一页通常是4K这样的大小,我的机器是这样的,所以下边程序demo中页大小都是4K。
其实逻辑空间可能比实际要大,而且只要程序没有用这么多显存,就不须要去映射这么多page,且即使用了这么多显存,也可以映射到c盘上。
逻辑页是具象的,须要映射到化学的页上,就能完成对显存的操作。我们把逻辑页叫页(page)化学页叫帧(pageframe)。页号-帧号的映射表叫页表(pagetable)。
图2-1页表映射
由于每位程序见到的逻辑地址空间都很大,所以程序变多了以后,程序使用的显存小于了化学显存,此时通常通过将部份”不着急使用”的页映射到c盘的形式来解决。所以页表中映射项可能是c盘。
图2-2页表映射
同时每位进程都有自己的专属页表,如下:
图2-3多进程的页表
一种实际情况,4G逻辑地址有32bit地址空间,假定pageSize=4K偏斜量占12bit,因此页表的逻辑页号有20bit。再假定实际显存条只有256M28bit地址空间12bit偏斜量16bit页号。
逻辑地址0x000011a3,去映射的时侯00001就是逻辑页号,去查页表发觉映射到真实页帧号00f3,之后偏斜量不变还是1a3,最终就找到这个数学显存内容了。
图2-4页表的映射过程
这个过程中,可能会出现映射的帧号是disk,即映射到了c盘上。此时会触发缺页异常,步入内核态,内核从c盘中读取缺的这页内容,将其加载到化学显存中。并且化学显存的帧有可能所有帧都满了,此时就须要赶出不太”重要”的帧。
赶出的过程须要判定当前化学页(帧)是否是脏的(脏:与c盘中内容不一致,即从c盘加载到化学显存后被改过就是脏的),倘若是脏的还须要更新c盘中的内容保证一致。
赶出后就腾出了位置给从c盘中读到的这页的数据,之后须要更新页表的这一项的映射关系,将c盘改为帧号,之后重新进行查页表这一步。
逻辑层的作用:极大的增加了显存碎片;利用c盘可以实现”无限的显存”;各个进程间显存的安全性等。
一个地址中“住”的是一字节(8bit)的数据。
2.2快表TLB、多级页表
里面提及了逻辑-化学页的映射,这就是页表,而且里面的页表虽然不仅简单的页号映射,还储存了其他一些属性:是否有效,读写权限,更改位,访问位(淘汰算法和TLB中用),是否是脏(被更改过就是脏的,由于他和硬碟上的数据不一致),是否准许被高速缓存等等。
页表存于寻址中,每位进程都有自己的页表。
里面可以看见基于页表的轮询,须要两次访问寻址(页表是存在寻址的),效率低下。为了提升速率,引入了快表,快表是页表项的缓存,将近来一次的映射项存入快表,由于空间有限所以须要赶出最老的那一项。快表的设计是基于经验:程序常常访问的page通常就那几个,不会时常频繁的更换非常多的页。
快表可能存于硬件MMU中(也可能是软件TLB),通常只有8-256条,每位进程都有自己的快表。
另一个值得讨论的话题是页表占用空间太大,里面反例中(32位程序256M机器pageSize4K)页号有20bit即2百万个,所以须要有1百万条,每条大小假如只算逻辑页号(20bit)和化学页号(16bit)的话:
36bit * 2^20 = 4.5MB
假如有64个这样的程序在运行…后果可想而知。
一种挺好的解决方式是多级页表,第一级页表用于找寻第二级页表的编号。的单级映射可以改成和两级映射。此时占用显存为
20bit * 2^10 + 16bit * 2^20 = 2M
2.3分段
严格意义的分段是,每一段的虚拟地址都是从0开始。之后页表是段号+页号来映射帧号的。并且这些方式早已被废弃了,只有x8632位的intel的cpu还保留了这些段页结合的形式,即严格意义的分段早已用的极少。
那为何还常常看到段的概念?现今所说的段通常是程序在逻辑层面保留的概念,对逻辑地址有个简略的界定,以便程序编撰,并且并不影响os的显存管理(还是分页管理)。
以32位程序为例,在逻辑空间中最高的0xcxc0000000-0xffffffff这1G的显存是给内核留出的,这部份是所有进程共享的。剩余3G显存从低到高分别是Text、Data、Heap、Lib、Stack。64位程序则远小于这儿的值。
Heap是从低往高下降,Stack是从高往低下降,且有个最大限制。Data储存静态变量Text储存程序二补码码,Lib储存库函数须要占用的显存,多个程序假如都使用了相同的库,显存是共用的(共享显存)。各个部份的留有随机的一段偏斜量,可以保护程序,这也促使每次重新执行程序的时侯变量所在的显存地址总是不同的。
图2-532位系统下显存地址的组成
分段是逻辑空间上的,不影响分页的显存管理方法,前面进行分页,映射到化学显存上各部份跨多个页虽然并不连续。
2.4cache
cpu的五级缓存饰演着缓存寻址数据的作用,而cache在显存管理中的位置是如何的呢?
PIPT,化学级cache,cpu剖析完映射关系,先到cache找有没有该化学地址的cache。这样会特别的慢,而且所有进程可以共享cache。
VIVT,逻辑级cache,cpu直接通过逻辑地址找cache,miss后再查TLB页表这种。这样很快linux社区,而且逻辑地址只能对当期进程使用,其他进程完全不能复用,尤其是库函数这些共享的不能借助好cache。
VIPT,将二者结合,用逻辑地址查找cache,cache中数据部份后面添加一个对应化学地址的tag。这样领到这个tag后到tlb、页表中查看下这个对应关系是否正确,假如正确就直接读cache。这样速率和共享性都是折中的。
以上三种形式各有利弊,在不同的cpu中可能使用的不一样。
2.5显存地址大小
好多人轻率的会觉得32位系统的虚拟地址是32位,这是没错的,并且64位系统下真正的可用的虚拟地址却不到64位。
#include
int main(){
int x = 10;
printf("%p",&x)
}
图2-6C语言复印地址
显著见到是48位,即使这个表针大小是8byte,并且只有48bit是有效的地址位,后面是多个0。通过cat/proc/cpuinfo最后几行能看见化学地址和虚拟地址的大小,这主要是cpu单方面订制的,我的这台机器是13年买的intel赛扬i53230的CPU。其实我的系统显存只有2G,其实物理地址不会有43位,只是cpu最多支持43位化学地址。
图2-7cpuinfo中的虚拟地址和化学地址
小细节:栈是仅次于内核的低位地址,参考图2-5.所以看见上面这个地址基本能推测出分给内核的虚拟空间应当是0xffffffffffff-0x8。
2.6显存分类
在生活中我们常常见到各类显存的种类,例如在linux调用free-h的时侯可以看见图2-6的分类。
在linux中通过free-h可以看见当前系统的显存情况:
图2-8free指令下的显存分类
mem是化学显存,swap是交换分区,是拿来将显存暂时放在c盘上的。
total总显存大小,used用户使用的显存大小,free空闲的显存大小,shared共享显存大小,buff/cache文件缓存大小,available可用显存大小是free和buff/cache加上去。
total = used(含shared) + free + buff/cache
这儿须要理解buff/cache,她们在老一些的内核中是分开显示的分别是buffercache和pagecache,都是对c盘的缓存。其中buffercache是硬件层面,对c盘块中的数据进行缓存,缓存的单位其实也是块。而pagecache是文件系统层面,对文件进行缓存,缓存单位就是页。buffercache的提出特别的早,二者并存时会碰到重复缓存了相同的内容的情况。
较新的内核早已将二者合并,或则说将buffercache合到了pagecache。其实也还是能缓存c盘块,并且储存单位也是页了。而且buffer使用前会先检测pagecahce是否早已缓存了对应内容,假如是则直接指过去。在机器维度查看显存的时侯也能发觉BufferCache都是0,由于都合到了pageCache,有Buffer的都是很老的内核的机器。
buff/cache占用大,会不会影响后续程序申请显存?
不会linux格式化命令,一旦用户程序须要申请显存,buff/cache都会释放掉一部份。换句话说buff/cache是在显存比较空闲的时侯,尽量借助一出来加速文件读写的。假如有大婶须要用显存,是会拱手让出的。
假如想进一步了解三者的演进,这篇文章从内核源码的角度展示了,几个理成本版本下buffcache和pagecache的变化。
在windows任务管理器中又可以看见右图的几种状态的显存别称,而在Jprofile查看jvm显存的时侯也有图2-8的一些别称。
图2-9windows任务管理器显存分类
图2-10jprofile显存分类
已递交的意思是早已向操作系统申请了如此多的显存,操作系统可以早已给了如此多显存了,而且也可能没有给这么多。贴一张谷歌自己的解释如图
图2-11几种显存的解释
递交的显存由于是虚拟显存,并不一定系统会立即给那么多,所以可能递交远超过化学显存上限的大小。我之前看过一个视频,小哥用malloc申请了130000+GB的显存程序才退出,而假如在malloc后给申请的地址填写值,事情就不这么顺利了。感兴趣可以去看下这个视频。其实不了解C语言也没关系我在本文后半段会用java的Unsafe同样申请超过化学上限的显存大小做demo。
3显存相关的系统调用3.1内核态和用户态
内核态、用户态、内核空间、用户空间,是常常说起的概念。由于操作系统不容许用户直接操作硬件,所以须要用户程序通知内核,内核帮你下达指令给硬件。在进行读文件的时侯,就须要用到c盘这个设备,所以须要步入内核态,将文件内容读到内核buffer,之后拷贝到用户buffer并从内核态切换为用户态,程序能够真正领到数据。
用户态进内核态,通常有三种触发条件,中断、异常和系统调用,中断和异常有时侯界限比较模糊,比如缺页中断也有地方叫缺页异常。这儿我们引出了系统调用,大多数须要主动操作或读写硬件的都是通过系统调用。诸如读写文件的open/read/write是系统调用,网路传输常见的select/poll/epoll也是系统调用,申请显存的malloc底层也是通过brk或mmap这俩系统调用实现的。
系统调用伴随了好多设计的优化,比如通过epoll等系统调用实现的IO多路复用提升了网路包的处理效率,mmap、sendfile等系统调用实现的零拷贝,降低了用户空间和内核空间之间的数据拷贝和上下文切换次数等等。在java的NIO中有大量的函数是直接封装了系统调用。
3.2brk
malloc大于128K(阀值可更改)的显存时,用的是brk申请显存。C语言中sbrk(可函数)是brk(系统调用)的简单封装,下边代码复印的值可以看出first由于申请了0大小,所以和second表针位置相同。而third则表示的是second的尾部地址。可以看见虚拟地址是连续分配的,brk虽然就是向下扩充heap的下界,配合查看图2-5。
#include
#include
int main(){
void *first = sbrk(0);
void *second = sbrk(1);
void *third = sbrk(0);
printf("%pn",first);
printf("%pn",second);
printf("%pn",third);
}
图3-1brk代码输出
假如此时在third+1地址处去初始化一个int值,是可以成功的,并不报错。
#include
#include
int main(){
void *first = sbrk(0);
void *second = sbrk(1);
void *third = sbrk(0);
int *p = (int *)third+1;
*p = 1;
}
这是由于页大小是4K,sbrk(1)虽然也是申请一页,所以third+1位置也是安全的。假如我们将second这行改为4096,那就是另一个故事了,会触发段错误。
void *second = sbrk(4096);
图3-2brk代码输出2
3.3mmap
malloc小于128K的显存时,用的是mmap。
// addr传NULL则不关心起始地址,关心地址的话应传个4k的倍数,不然也会归到4k倍数的起始地址。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
//释放内存munmap
int munmap(void *addr, size_t length);
mmap用法有两种,一种是将文件映射到显存,另一种空文件映射,也就是把fd传入-1,都会从映射区申请到一块显存。malloc就是调用的第二种实现。
#include
#include
#include
int main(){
int* a =(int *) mmap(NULL, 100 * 4096, PROT_READ| PROT_WRITE, MAP_PRIVATE| MAP_ANONYMOUS, -1, 0);
int* b =a;
for(int i=0;i<100;i++){
b = (void *)a + (i*4096);
*b =1;
}
while(1){
sleep(1);
}
}
这儿递交400K显存的申请,但是在每页中都进行显存的使用。可以看见不映射文件的话触发的是minflt次数是100次。
图3-3进程的显存minflt
这儿是mmap显存的惰性加载,一开始mmap100页时虽然都没有分配给进程,在用到的时侯开始真正领到显存,此时触发minflt缺页,由于不是映射的文件,不用从c盘中调显存,所以是小错误。并且仍是消耗性能的。
假如mmap是映射的c盘文件,也会惰性加载,在初次加载或则页被赶出后再加载的时侯,也会缺页,这个时侯就不是小错误minflt了,而是majflt。诸如下边使用mmap来读文件。
#include
#include
#include
#include
#include
#include
int main(){
sleep(4);
int fd = open("./1.txt", O_RDONLY, S_IRUSR|S_IWUSR);
struct stat sb;
if(fstat(fd, &sb) == -1){
perror("cannot get file sizen");
}
printf("file size is %ldn",sb.st_size);
char *file_in_memory = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
for(int i=0;i<sb.st_size;i++){
printf("%c", file_in_memory[i]);
}
munmap(file_in_memory, sb.st_size);
close(fd);
}
右图是线程窃听的结果,为了便捷观察我在开始读之前sleep4s。可以看见红框第一行,有一次majflt,这是第一次去读文件,直接触发了缺页异常,且指向c盘。是最历时的错误。
图3-3-2进程mmap读文件引起majflt
read和mmap都可以读文件,后者有状态转换和多次拷贝,并且前者有缺页中断。在单纯读c盘文件场景,二者虽然无法在孰优孰劣上有定论。
3.4共享显存
共享显存是进程间通讯的一种形式,(管线讯号讯号量套接字也是进程通讯的形式)。共享显存的反例比比皆是,windows下最显著,例如这个上传文件的对话框就是共享显存里的,同一时间windows下不会弹出两个该对话框。再例如动态链接库,也是共享显存中的,多个进程可以共享,两个进程mmap相同文件的形式可以实现共享显存,shmget则是更广泛的共享显存的系统调用。
图3-4共享显存的典型事例
共享显存原理就是两个进程中页,映射到了相同的帧。代码这儿不写了,直接参考geeks这篇的代码。
4java中的显存4.1java显存概述
jvm显存结构主要如图4-1.本文不想对“常考”的知识点再度进行讲解,网上有大量的文章来讲显存结构各自的用途和GC相关的内容,这儿我就不展开讲了。下边几节会讲一些比较”小众”的知识。
图4-1java的显存五区
4.2对象头与表针压缩
在另一篇讲估算java对象大小的文章中提及,java对象是由对象头,对象内容组成,但是是8字节对齐的。其中对象头有以下三部份组成:
我们这儿来看下Klass,有没有想过我们反射的时侯操作的都是Class对象而不是这儿的Klass,二者关系是:
Klass是C++对象InstanceKlass,上面有个_java_mirror数组指向对应的Class对象。
图4-2java对象头指向metaspace
这儿还提及了表针压缩,64位系统,假如jvm堆显存大于32GB是可以开启表针压缩的,此时Klass表针只须要4个字节,同时对象表针也只须要4个字节。这儿会衍生出两个问题:
第一个就是4字节最多表示2^32个地址,每位地址里住的是一个字节,所以只能表示4GB,如何还说32G下都能压缩呢?
由于:前面提及对象都是8字节对齐,所以每位地址里住的是8字节,所以可以表示32GB,实际地址移3位。
第二个问题就是普通对象表针压缩CompressedObjectPointers(“CompressedOops”)linux查看硬盘总容量,压缩的是java堆上的对象的表针(引用)大小,而对象头指向的是Klass,这是个C++的结构,这个表针也压缩了吗?
是的,CompressOops和CompressKlass是相随而生,默认同时开启的,Klass这部份须要连续的4.3metaspace
metaspace储存的是类的元数据信息,里面提及的Klass就是在metaspace中的,通常开启压缩的metaspace有CompressClassSpace和NonClassSpace两部份组成,其中后者显存占用较少,是前者的5-100分之一,后者又叫压缩类空间,实际上这部份显存本身并没有压缩,只是对象头中记录的指向这儿的表针进行了压缩。
图4-3metaspace两部份:非类区和压缩类空间
压缩类空间中Klass是c++的对象有着好多元数据数组,vtable是记录虚方式表针,itable是插口方式表针。Non-class中则记录了更详尽的元数据信息。开启表针压缩后,假如设置MaxMetaspaceSize参数实际上是限定的Non-class部份的大小,而不包括压缩类空间。通过Jprofile中也能发觉Metaspace只包括Non-class部份,那为何我上来说Metaspace有两部份呢,主要是从概念上讲三者都是元数据,在美国好多文章中也都归为了Metaspace。这儿只须要注意这个小细节就可以了。设置MaxMetaspaceSize参数也可以对压缩类空间起到间接的限制,由于上面说了Non-class部份是class部份的n倍。
图4-4表针压缩开启时非堆
将压缩类空间和非类空间分开的缘由之一,就是压缩类空间是对象关联的,只有4G上限,而将更多其他元数据剥离出去后,元空间可以远超过4G。而倘若不开启表针压缩,虽然二者就没必要分开了。关掉表针压缩后,-XX:-UseCompressedOops两部份会合为一个。也称Metaspace
图4-5表针压缩关掉时非堆
Q1:元空间显存哪些时侯分配?
一个新的类在须要被加载的时侯,会使用ClassLoader在元空间申请显存,并储存类的元数据信息。
Q2:元空间哪些时侯释放显存?
元空间的显存是ClassLoader持有的,所以说只有对应的ClassLoader卸载掉的时侯才能释放。ClassLoader又是须要他所加载的类都消失的时侯才会消失。通常是伴随在一次GC的过程中进行这个释放。另外元空间假如超过了上限也会造成OOM。
Q3:metaspace溢出会不会造成OOM?
其实会造成OOM,所以metaspace限制大小的配置,须要依照程序慎重订制。通常通过不断创建新的类,如加载新类(如hsf配置中下发groovy文件才会动态的加载新的class),或则动态代理类(spring中的提高类都是动态代理类)就会造成metaspace的下降。
cglib
cglib
3.2.4
//设置metaspace大小:-XX:MaxMetaspaceSize=200m
public class T {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
enhancer.setUseCache(false);
enhancer.setCallback((FixedValue)()->":)");
enhancer.create();
}
}
}
监视会发觉压缩类空间和非类空间都在减小,前者在200M上有道红线,在2分钟左右溢出,程序死掉,这个程序中压缩类空间大约是分类的六分之一。
图4-5a压缩类空间
图4-5b非类空间
4.4堆外显存
里面的CodeCache和Metaspace毫无疑惑是jvm管理下的堆外空间。并且不仅这种常规的堆外空间,jvm还可以使用一些native方式,直接申请堆外显存。
比如做如此个demo,我们设置一个简单的java程序的堆大小是10M,此时用jprofile查看显存堆递交了10M实际使用9M多,堆外递交了12M实际使用11M左右。所以算出来是20M+。直接查看进程显存会略小于这个值,由于这个20M是虚拟机内部的显存,本身运行还是须要一些额外显存的,进程递交的显存有90M,实际使用显存47M
图4-6进程的递交显存和实际显存
接出来我们使用Unsafe申请1G堆外显存(也可以用NIO中的ByteBuffer.allocateDirect())
public static void main(String[] args) throws InterruptedException, IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe us = (Unsafe) f.get(null);
long addr = us.allocateMemory(1024 * 1024 * 1024);
System.out.println("Hello World");
System.out.println(addr);
while(true){
Thread.sleep(1000L);
}
}
可以看见递交的显存1G多,实际使用显存也是47M。
图4-7进程的递交显存和实际显存2
我甚至可以调整申请65G的显存,要晓得我的笔记本也只有64G的显存,但这仍不会报错,可以看见递交的显存早已超过了化学显存上限,并且得益于上面讲的虚拟显存的管理模式,致使应用申请了超过化学大小的显存,而假如真的使用上去的话linux查看硬盘总容量,会有页置换来协调。
图4-8进程可以递交超过现实存在的显存
里面的递交显存很大并且实际使用显存却并不大:
图4-9任务管理器此时的状态
Unsafe是很危险的一个类,不建议使用。并且可以帮助我们理解有些框架是怎样工作的。例如前一阵子看的Ehcache就提供了堆外缓存就是用类似Unsafe申请的。堆外缓存须要自己实现序列化,由于Unsafe设置显存只能设置01字节码不能设置为java对象。
堆外缓存的益处:缓存通常是短时间不须要清除的,假如在堆上则肯定会步入老年代,占用固定的一大块空间,致使触发fullGC的门槛增加了,很容易到了那种门限值。并且GC过程中还要去遍历这种对象,效率较低。
堆外显存的益处:序列化须要自己实现,清除也须要自己实现,访问速率比heap要慢。