详情联系:朱老师 18170060794 微信同号
传说人类建造通天塔触怒上帝,上帝施法使人类语言混乱彼此之间无法交流。这就是说各种各样的语言其实就是祸乱之源——只会导致交流的不便。可是看看如今的程序设计语言的数量,你会怀疑人类是否又在造“通天塔”了?真的有这么多语言的必要吗?我到底要学习多少种程序设计语言才够用呢?
为什么会有这么多种程序设计语言?
根据维基的资料,可以称得上相对“主流”(有人用、有文档)的程序设计语言至少有600种, 还有大量的商业化失败、实用性不高、语言小众(这里的“语言”指的是编码的语言,一般的程序设计语言都习惯用拉丁字母或其超集来作为的字符集,也 有用日文、俄文编程、汉语编程的,比如易语言)就难以统计了(保守估计可以上万种),尽管已经有了这么多种程序设计语言,仍然有大量的人投入了大量的时间 来研发新的语言,这难道不是重复通天塔的故事吗?dunsijiaoyu zz
正常情况下,我们读取文件的流程为,先通过系统调用从磁盘读取数据,存入操作系统的内核缓冲区,然后在从内核缓冲区拷贝到用户空间,而内存映射,是将磁盘文件直接映射到用户的虚拟存储空间中,通过页表维护虚拟地址到磁盘的映射,通过内存映射的方式读取文件的好处有,因为减少了从内核缓冲区到用户空间的拷贝,直接从磁盘读取数据到内存,减少了系统调用的开销,对用户而言,仿佛直接操作的磁盘上的文件,另外由于使用了虚拟存储,所以不需要连续的主存空间来存储数据。
程序设计的5个底层逻辑,决定你能走多快
在 Java 中,我们使用 MappedByteBuffer 来实现内存映射,这是一个堆外内存,在映射完之后,并没有立即占有物理内存,而是访问数据页的时候,先查页表,发现还没加载,发起缺页异常,然后在从磁盘将数据加载进内存,所以一些对实时性要求很高的中间件,例如rocketmq,消息存储在一个大小为1G的文件中,为了加快读写速度,会将这个文件映射到内存后,在每个页写一比特数据,这样就可以把整个1G文件都加载进内存,在实际读写的时候就不会发生缺页了,这个在rocketmq内部叫做文件预热。
下面我们贴一段 rocketmq 消息存储模块的代码,位于 MappedFile 类中,这个类是 rocketMq 消息存储的核心类感兴趣的可以自行研究,下面两个方法一个是创建文件映射,一个是预热文件,每预热 1000 个数据页,就让出 CPU 权限。
private void init(final String fileName, final int fileSize) throws IOExption {
this.fileName = fileName;
this.fileSize = fileSize;
this.file = new File(fileName);
this.fileFromOffset = Long.parseLong(this.file.getName());
boolean ok = false;
ensureDirOK(this.file.getParent());
try {
this.fileChannel = new RandomAcssFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
TOTAL_MAPPED_***crementAndGet();
ok = true;
} catch (FileNotFoundExption e) {
log.error("create file channel " + this.fileName + " Failed. ", e);
throw e;
} catch (IOExption e) {
log.error("map file " + this.fileName + " Failed. ", e);
throw e;
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}
//文件预热,OS_PAGE_SIZE = 4kb 相当于每 4kb 就写一个 byte 0 ,将所有的页都加载到内存,真正使用的时候就不会发生缺页异常了
public void warmMappedFile(FlushDiskType type, int pages) {
long beginTime = System.currentTimeMillis();
ByteBuffer byteBuffer = this.mappedByteBuffer.sli();
int flush = 0;
long time = System.currentTimeMillis();
for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
byteBuffer.put(i, (byte) 0);
// for flush when flush disk type is sync
if (type == FlushDiskType.SYNC_FLUSH) {
if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
flush = i;
mappedByteBuffer.for();
}
}
// prevent gc
if (j % 1000 == 0) {
lo***("j={}, costTime={}", j, System.currentTimeMillis() - time);
time = System.currentTimeMillis();
try {
// 这里sleep(0),让线程让出 CPU 权限,供其他更高优先级的线程执行,此线程从运行中转换为就绪
Thread.sleep(0);
} catch (InterruptedExption e) {
log.error("Interrupted", e);
}
}
}
// for flush when prepare load finished
if (type == FlushDiskType.SYNC_FLUSH) {
lo***("mapped file warm-up done, for to disk, mappedFile={}, costTime={}",
this.getFileName(), System.currentTimeMillis() - beginTime);
mappedByteBuffer.for();
}
lo***("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),
System.currentTimeMillis() - beginTime);
this.mlock();
}
JVM 中对象的内存布局
在linux中只要知道一个变量的起始地址就可以读出这个变量的值,因为从这个起始地址起前8位记录了变量的大小,也就是可以定 位到结束地址,在 Java 中我们可以通过 Field.get(object) 的方式获取变量的值,也就是反射,终是通过 UnSafe 类来实现的。我们可以分析下具体代码。
Field 对象的 getInt方法 先安全检查 ,然后调用 FieldAcssor
@CallerSensitive
public int getInt(Object obj)
throws IllegalArgumentExption, IllegalAcssExption
{
if (!override) {
if (!Reflection.quickCheckMemberAcss(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAcss(caller, clazz, obj, modifiers);
}
}
return getFieldAcssor(obj).getInt(obj);
}
获取field在所在对象中的地址的偏移量 fieldoffset
UnsafeFieldAcssorImpl(Field var1) {
this.field = var1;
if(Modifier.isStatic(var1.getModifiers())) {
this.fieldOffset = unsafe.staticFieldOffset(var1);
} else {
this.fieldOffset = unsafe.objectFieldOffset(var1);
}
this.isFinal = Modifier.isFinal(var1.getModifiers());
}
UnsafeStaticIntegerFieldAcssorImpl 调用unsafe中的方法
public int getInt(Object var1) throws IllegalArgumentExption {
return unsafe.getInt(this.base, this.fieldOffset);
}
通过上面的代码我们可以通过属性相对对象起始地址的偏移量,来读取和写入属性的值,这也是 Java 反射的原理,这种模式在jdk中很多场景都有用到,例如LockSupport.park中设置阻塞对象。 那么属性的偏移量具体根据什么规则来确定的呢? 下面我们借此机会分析下 Java 对象的内存布局。
在 Java 虚拟机中,每个 Java 对象都有一个对象头 (object header) ,由标记字段和类型指针构成,标记字段用来存储对象的哈希码, GC 信息, 持有的锁信息,而类型指针指向该对象的类 Class ,在 64 位操作系统中,标记字段占有 64 位,而类型指针也占 64 位,也就是说一个 Java 对象在什么属性都没有的情况下要占有 16 字节的空间,当前 JVM 中默认开启了压缩指针,这样类型指针可以只占 32 位,所以对象头占 12 字节, 压缩指针可以作用于对象头,以及引用类型的字段。
JVM 为了内存对齐,会对字段进行重排序,这里的对齐主要指 Java 虚拟机堆中的对象的起始地址为 8 的倍数,如果一个对象用不到 8N 个字节,那么剩下的就会被填充,另外子类继承的属性的偏移量和父类一致,以 Long 为例,他只有一个非 static 属性 value ,而尽管对象头只占有 12 字节,而属性 value 的偏移量只能是 16, 其中 4 字节只能浪费掉,所以字段重排就是为了避免内存浪费, 所以我们很难在 Java 字节码被加载之前分析出这个 Java 对象占有的实际空间有多大,我们只能通过递归父类的所有属性来预估对象大小,而真实占用的大小可以通过 Java agent 中的 Instrumentation获取。
当然内存对齐另外一个原因是为了让字段只出现在同一个 CPU 的缓存行中,如果字段不对齐,就有可能出现一个字段的一部分在缓存行 1 中,而剩下的一半在 缓存行 2 中,这样该字段的读取需要替换两个缓存行,而字段的写入会导致两个缓存行上缓存的其他数据都无效,这样会影响程序性能。
通过内存对齐可以避免一个字段同时存在两个缓存行里的情况,但还是无法完全规避缓存伪共享的问题,也就是一个缓存行中存了多个变量,而这几个变量在多核 CPU 并行的时候,会导致竞争缓存行的写权限,当其中一个 CPU 写入数据后,这个字段对应的缓存行将失效,导致这个缓存行的其他字段也失效。
程序设计的5个底层逻辑,决定你能走多快
在 Disruptor 中,通过填充几个的字段,让对象的大小刚好在 64 字节,一个缓存行的大小为64字节,这样这个缓存行就只会给这一个变量使用,从而避免缓存行伪共享,但是在 jdk7 中,由于无效字段被导致该方法失效,只能通过继承父类字段来避免填充字段被优化,而 jdk8 提供了注解@Contended 来标示这个变量或对象将独享一个缓存行,使用这个注解必须在 JVM 启动的时候加上 -XX:-RestrictContended 参数,其实也是用空间换取时间。
jdk6 --- 32 位系统下
public final static class VolatileLong
{
public volatile long value = 0L;
public long p1, p2, p3, p4, p5, p6; // 填充字段
}
jdk7 通过继承
public class VolatileLongPadding {
public volatile long p1, p2, p3, p4, p5, p6; // 填充字段
}
public class VolatileLong extends VolatileLongPadding {
public volatile long value = 0L;
}
jdk8 通过注解
@Contended
public class VolatileLong {
public volatile long value = 0L;
}
根据冯·诺依曼思想,计算机采用二进制作为数制基础,必须包含:运算器、控制器、存储设备,以及输入输出设备,如下图所示。
程序设计的5个底层逻辑,决定你能走多快
我们先来分析 CPU 的工作原理,现代 CPU 芯片中大都集成了,控制单元,运算单元,存储单元。控制单元是 CPU 的控制中心, CPU 需要通过它才知道下一步做什么,也就是执行什么指令,控制单元又包含:指令寄存器(IR ),指令译码器( ID )和操作控制器( OC )。
当程序被加载进内存后,指令就在内存中了,这个时候说的内存是独立于 CPU 外的主存设备,也就是 PC 机中的内存条,指令指针寄存器IP 指向内存中下一条待执行指令的地址,控制单元根据 IP寄存器的指向,将主存中的指令装载到指令寄存器。
这个指令寄存器也是一个存储设备,不过他集成在 CPU 内部,指令从主存到达 CPU 后只是一串 010101 的二进制串,还需要通过译码器解码,分析出操作码是什么,操作数在哪,之后就是具体的运算单元进行算术运算(加减乘除),逻辑运算(比较,位移)。而 CPU 指令执行过程大致为:取址(去主存获取指令放到寄存器),译码(从主存获取操作数放入高速缓存 L1 ),执行(运算)。
程序设计的5个底层逻辑,决定你能走多快
这里解释下上图中 CPU 内部集成的存储单元 SRAM ,正好和主存中的 DRAM 对应, RAM 是随机访问内存,就是给一个地址就能访问到数据,而磁盘这种存储媒介必须顺序访问,而 RAM 又分为动态和静态两种,静态 RAM 由于集成度较低,一般容量小,速度快,而动态 RAM 集成度较高,主要通过给电容充电和放电实现,速度没有静态 RAM 快,所以一般将动态 RAM 做为主存,而静态 RAM 作为 CPU 和主存之间的高速缓存 (cache),用来屏蔽 CPU 和主存速度上的差异,也就是我们经常看到的 L1 , L2 缓存。每一级别缓存速度变低,容量变大。
下图展示了存储器的层次化架构,以及 CPU 访问主存的过程,这里有两个知识点,一个是多级缓存之间为保证数据的一致性,而推出的缓存一致性协议,具体可以参考这篇文章,另外一个知识点是, cache 和主存的映射,首先要明确的是 cah 缓存的单位是缓存行,对应主存中的一个内存块,并不是一个变量,这个主要是因为 CPU 访问的空间局限性:被访问的某个存储单元,在一个较短时间内,很有可能再次被访问到,以及空间局限性:被访问的某个存储单元,在较短时间内,他的相邻存储单元也会被访问到。
而映射方式有很多种,类似于 cache 行号 = 主存块号 mod cache总行数 ,这样每次获取到一个主存地址,根据这个地址计算出在主存中的块号就可以计算出在 cache 中的行号。
程序设计的5个底层逻辑,决定你能走多快
下面我们接着聊 CPU 的指令执行。取址、译码、执行,这是一个指令的执行过程,所有指令都会严格按照这个顺序执行。但是多个指令之间其实是可以并行的,对于单核 CPU 来说,同一时刻只能有一条指令能够占有执行单元运行。这里说的执行是 CPU 指令处理 (取指,译码,执行) 三步骤中的第三步,也就是运算单元的计算任务。
所以为了提升 CPU 的指令处理速度,所以需要保证运算单元在执行前的准备工作都完成,这样运算单元就可以一直处于运算中,而刚刚的串行流程中,取指,解码的时候运算单元是空闲的,而且取指和解码如果没有命中高速缓存还需要从主存取,而主存的速度和 CPU 不在一个级别上,所以指令流水线 可以大大提高 CPU 的处理速度,下图是一个流水线的示例图,而现在的奔腾 CPU 都是32级流水线,具体做法就是将上面三个流程拆分的更细。
程序设计的5个底层逻辑,决定你能走多快
除了指令流水线, CPU 还有分支预测,乱序执行等优化速度的手段。好了,我们回到正题,一行 Java 代码是怎么执行的?
一行代码能够执行,必须要有可以执行的上下文环境,包括:指令寄存器、数据寄存器、栈空间等内存资源,然后这行代码必须作为一个执行流能够作系统的任务调度器识别,并给他分配 CPU 资源,当然这行代码所代表的指令必须是 CPU 可以解码识别的,所以一行 Java 代码必须被解释成对应的 CPU 指令才能执行。下面我们看下System.out.println("Hello world")这行代码的转译过程。
Java 是一门高级语言,这类语言不能直接运行在硬件上,必须运行在能够识别 Java 语言特性的虚拟机上,而 Java 代码必须通过 Java 编译器将其转换成虚拟机所能识别的指令序列,也称为 Java 字节码,之所以称为字节码是因为 Java 字节码的操作指令(OpCode)被固定为一个字节,以下为 System.out.println("Hello world") 编译后的字节码:
0x00: b2 00 02 getstatic Java .lang.System.out
0x03: 12 03 ldc "Hello, World!"
0x05: b6 00 04 invokevirtual Java .io.PrintStream.println
0x08: b1 return
左列是偏移;中间列是给虚拟机读的字节码;右列是高级语言的代码,下面是通过汇编语言转换成的机器指令,中间是机器码,第三列为对应的机器指令,后一列是对应的汇编代码:
0x00: 55 push rbp
0x01: 48 89 e5 mov rbp,rsp
0x04: 48 83 ec 10 sub rsp,0x10
0x08: 48 8d 3d 3b 00 00 00 lea rdi,[rip+0x3b]
; 加载 "Hello, World!\n"
0x0f: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
0x16: b0 00 mov al,0x0
0x18: e8 0d 00 00 00 call 0x12
; 调用 printf 方法
0x1d: 31 c9 xor ecx,ecx
0x1f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
0x22: 89 c8 mov eax,ecx
0x24: 48 83 c4 10 add rsp,0x10
0x28: 5d pop rbp
0x29: c3 ret
JVM 通过类加载器加载 class 文件里的字节码后,会通过解释器解释成汇编指令,终再转译成 CPU 可以识别的机器指令,解释器是软件来实现的,主要是为了实现同一份 Java 字节码可以在不同的硬件平台上运行,而将汇编指令转换成机器指令由硬件直接实现,这一步速度是很快的,当然 JVM 为了提高运行效率也可以将某些热点代码(一个方法内的代码)一次全部编译成机器指令后然后在执行,也就是和解释执行对应的即时编译(JIT), JVM 启动的时候可以通过 -Xint 和 -Xcomp 来控制执行模式。
从软件层面上, class 文件被加载进虚拟机后,类信息会存放在方法区,在实际运行的时候会执行方法区中的代码,在 JVM 中所有的线程共享堆内存和方法区,而每个线程有自己独立的 Java 方法栈,本地方法栈(面向 native 方法),PC寄存器(存放线程执行位置),当调用一个方法的时候, Java 虚拟机会在当前线程对应的方法栈中压入一个栈帧,用来存放 Java 字节码操作数以及局部变量,这个方法执行完会弹出栈帧,一个线程会连续执行多个方法,对应不同的栈帧的压入和弹出,压入栈帧后就是 JVM 解释执行的过程了。
程序设计的5个底层逻辑,决定你能走多快
中断
刚刚说到, CPU 只要一上电就像一个永动机, 不停的取指令,运算,周而复始,而中断便是操作系统的灵魂,故名思议,中断就是打断 CPU 的执行过程,转而去做点别的。
例如系统执行期间发生了致命错误,需要结束执行,例如用户程序调用了一个系统调用的方法,例如mmp等,就会通过中断让 CPU 切换上下文,转到内核空间,例如一个等待用户输入的程序正在阻塞,而当用户通过键盘完成输入,内核数据已经准备好后,就会发一个中断信号,唤醒用户程序把数据从内核取走,不然内核可能会数据溢出,当磁盘报了一个致命异常,也会通过中断通知 CPU ,定时器完成时钟滴答也会发时钟中断通知 CPU 。
中断的种类,我们这里就不做细分了,中断有点类似于我们经常说的驱动编程,而这个通知机制是怎么实现的呢,硬件中断的实现通过一个导线和 CPU 相连来传输中断信号,软件上会有特定的指令,例如执行系统调用创建线程的指令,而 CPU 每执行完一个指令,就会检查中断寄存器中是否有中断,如果有就取出然后执行该中断对应的处理程序。
陷入内核 : 我们在设计软件的时候,会考虑程序上下文切换的频率,频率太高肯定会影响程序执行性能,而陷入内核是针对 CPU 而言的, CPU 的执行从用户态转向内核态,以前是用户程序在使用 CPU ,现在是内核程序在使用 CPU ,这种切换是通过系统调用产生的。
系统调用是执行操作系统底层的程序,Linux的设计者,为了保护操作系统,将进程的执行状态用内核态和用户态分开,同一个进程中,内核和用户共享同一个地址空间,一般 4G 的虚拟地址,其中 1G 给内核态, 3G 给用户态。在程序设计的时候我们要尽量减少用户态到内核态的切换,例如创建线程是一个系统调用,所以我们有了线程池的实现。
发展快速,无法跟进。
玫瑰行业多会有自己行业恐惧症,计算机行业也不例外。曾经有一位资深的老编程员,熟悉掌握20多种程序设计语言,他曾经在sun、Google等大公司工作过,但是他也患上了一种叫时代恐惧症的时代病。他感觉自己跟不上时代了,看着新出现的程序设计语言,他无力适从。HTML5刚刚出现不就把HTML的地位给占领了;javaScript和Python等新语言发展快速,而对于他来说,去了解一门语言得要几个月的时间,掌握的话,得两三年的时间。倒是对于老编程员来说,没有那么多的时间去琢磨了,有家庭、有工作。来学习新语言的时间很紧凑,而且没有刚刚学习语言的那种冲劲,那你还能说,他不思上进吗?
程序员吃青春饭?是因为你真的老了,从心里自发的觉得自己老了,失去年纪时的活力和。在四十五十岁的年纪,被生活的琐碎磨掉了年纪时对新鲜事物的热情,而更多地是畏惧和不知所措。要是这要,你就只能吃青春饭了!
计算机程序设计员是做什么的成都哪里可以考费用是多少
成都其他教育培训相关信息
2024-12-17
2024-12-05
2024-12-03
2024-12-02
2024-12-02
2024-12-02 刷新
2024-12-02 刷新
2024-11-21
2024-11-21