程序设计的5个底层逻辑,决定你能走多快
阿里妹导读:肉眼看计算机是由CPU、内存、显示器这些硬件设备组成,但大部分人从事的是软件开发工作。计算机底层原理就是连通硬件和软件的桥梁,理解计算机底层原理才能在程序设计这条路上越走越快,越走越轻松。从操作系统层面去理解高级编程语言的执行过程,会发现好多软件设计都是同一种套路,很多语言特性都依赖于底层机制,今天董鹏为你一一揭秘。
结合 CPU 理解一行 Java 代码是怎么执行的
当程序被加载进内存后,指令就在内存中了,这个时候说的内存是独立于 CPU 外的主存设备,也就是 PC 机中的内存条,指令指针寄存器IP 指向内存中下一条待执行指令的地址,控制单元根据 IP寄存器的指向,将主存中的指令装载到指令寄存器。
enter image description here
这里解释下上图中 CPU 内部集成的存储单元 SRAM ,正好和主存中的 DRAM 对应, RAM 是随机访问内存,就是给一个地址就能访问到数据,而磁盘这种存储媒介必须顺序访问,而 RAM 又分为动态和静态两种,静态 RAM 由于集成度较低,一般容量小,速度快,而动态 RAM 集成度较高,主要通过给电容充电和放电实现,速度没有静态 RAM 快,所以一般将动态 RAM 做为主存,而静态 RAM 作为 CPU 和主存之间的高速缓存 (cache),用来屏蔽 CPU 和主存速度上的差异,也就是我们经常看到的 L1 , L2 缓存。每一级别缓存速度变低,容量变大。
下图展示了存储器的层次化架构,以及 CPU 访问主存的过程,这里有两个知识点,一个是多级缓存之间为保证数据的一致性,而推出的缓存一致性协议,具体可以参考这篇文章,另外一个知识点是, cache 和主存的映射,首先要明确的是 cahce 缓存的单位是缓存行,对应主存中的一个内存块,并不是一个变量,这个主要是因为 CPU 访问的空间局限性:被访问的某个存储单元,在一个较短时间内,很有可能再次被访问到,以及空间局限性:被访问的某个存储单元,在较短时间内,他的相邻存储单元也会被访问到。
而映射方式有很多种,类似于 cache 行号 = 主存块号 mod cache总行数 ,这样每次获取到一个主存地址,根据这个地址计算出在主存中的块号就可以计算出在 cache 中的行号。
0x00: b2 00 02 getstatic Java .lang.System.out0x03: 12 03 ldc "Hello, World!"0x05: b6 00 04 invokevirtual Java .io.PrintStream.println0x08: b1 return 0x00: 55 push rbp0x01: 48 89 e5 mov rbp,rsp0x04: 48 83 ec 10 sub rsp,0x100x08: 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],0x00x16: b0 00 mov al,0x00x18: e8 0d 00 00 00 call 0x12 ; 调用 printf 方法0x1d: 31 c9 xor ecx,ecx0x1f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax0x22: 89 c8 mov eax,ecx0x24: 48 83 c4 10 add rsp,0x100x28: 5d pop rbp0x29: c3 ret 中断
从 Linux 内存管理角度理解 JVM 内存模型
进程上下文
虚拟存储
-
装入位 表示对于页是否在主存,如果地址页每页表示,数据还在磁盘 -
存放位置 建立虚拟页和物理页的映射,用于地址转换,如果为null表示是一个未分配页 -
修改位 用来存储数据是否修改过 -
权限位 用来控制是否有读写权限 -
禁止缓存位 主要用来保证 cache 主存 磁盘的数据一致性
内存映射
private void init(final String fileName, final int fileSize) throws IOException { 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 RandomAccessFile(this.file, "rw").getChannel(); this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize); TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize); TOTAL_MAPPED_FILES.incrementAndGet(); ok = true; } catch (FileNotFoundException e) { log.error("create file channel " + this.fileName + " Failed. ", e); throw e; } catch (IOException 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.slice(); 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); // force 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.force(); } }
// prevent gc if (j % 1000 == 0) { log.info("j={}, costTime={}", j, System.currentTimeMillis() - time); time = System.currentTimeMillis(); try { // 这里sleep(0),让线程让出 CPU 权限,供其他更高优先级的线程执行,此线程从运行中转换为就绪 Thread.sleep(0); } catch (InterruptedException e) { log.error("Interrupted", e); } } }
// force flush when prepare load finished if (type == FlushDiskType.SYNC_FLUSH) { log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}", this.getFileName(), System.currentTimeMillis() - beginTime); mappedByteBuffer.force(); } log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(), System.currentTimeMillis() - beginTime);
this.mlock(); } JVM 中对象的内存布局
Field 对象的 getInt方法 先安全检查 ,然后调用 FieldAccessor @CallerSensitive public int getInt(Object obj) throws IllegalArgumentException, IllegalAccessException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } return getFieldAccessor(obj).getInt(obj); }
获取field在所在对象中的地址的偏移量 fieldoffset UnsafeFieldAccessorImpl(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()); }
UnsafeStaticIntegerFieldAccessorImpl 调用unsafe中的方法 public int getInt(Object var1) throws IllegalArgumentException { return unsafe.getInt(this.base, this.fieldOffset); } 当然内存对齐另外一个原因是为了让字段只出现在同一个 CPU 的缓存行中,如果字段不对齐,就有可能出现一个字段的一部分在缓存行 1 中,而剩下的一半在 缓存行 2 中,这样该字段的读取需要替换两个缓存行,而字段的写入会导致两个缓存行上缓存的其他数据都无效,这样会影响程序性能。
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; } NPTL和 Java 的线程模型
线程的状态
-
BLOCKED (on object monitor) 通过 synchronized(obj) 同步块获取锁的时候,等待其他线程释放对象锁,dump 文件会显示 waiting to lock <0x00000000e1c9f108> -
TIMED WAITING (on object monitor) 和 WAITING (on object monitor) 在获取锁后,调用了 object.wait() 等待其他线程调用 object.notify(),两者区别是是否带超时时间 -
TIMED WAITING (sleeping) 程序调用了 thread.sleep(),这里如果 sleep(0) 不会进入阻塞状态,会直接从运行转换为就绪 -
TIMED WAITING (parking) 和 WAITING (parking) 程序调用了 Unsafe.park(),线程被挂起,等待某个条件发生,waiting on condition
线程的同步
Java 中如何实现定时任务
public static void sleep(long millis, int nanos) throws InterruptedException { if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (nanos < 0 || nanos > 999999) { throw new IllegalArgumentException( "nanosecond timeout value out of range"); } if (nanos >= 500000 || (nanos != 0 && millis == 0)) { millis++; } sleep(millis); }
LockSupport.park(long nans) Condition.await()调用的该方法, ScheduledExecutorService 用的 condition.await() 来实现阻塞一定的超时时间,其他带超时参数的方法也都通过他来实现,目前大多定时器都是通过这个方法来实现的,该方法也提供了一个布尔值来确定时间的精度。
System.currentTimeMillis() 以及 System.nanoTime() 这两种方式都依赖于底层操作系统,前者是毫秒级,经测试 windows 平台的频率可能超过 10ms ,而后者是纳秒级别,频率在 100ns 左右,所以如果要获取更精准的时间建议用后者 好了,api 了解完了,我们来看下定时器的底层是怎么实现的,现代PC机中有三种硬件时钟的实现,他们都是通过晶体振动产生的方波信号输入来完成时钟信号同步的。
-
实时时钟 RTC ,用于长时间存放系统时间的设备,即使关机也可以依靠主板中的电池继续计时。Linux 启动的时候会从 RTC 中读取时间和日期作为初始值,之后在运行期间通过其他计时器去维护系统时间。 -
可编程间隔定时器 PIT ,该计数器会有一个初始值,每过一个时钟周期,该初始值会减1,当该初始值被减到0时,就通过导线向 CPU 发送一个时钟中断, CPU 就可以执行对应的中断程序,也就是回调对应的任务 -
时间戳计数器 TSC , 所有的 Intel8086 CPU 中都包含一个时间戳计数器对应的寄存器,该寄存器的值会在每次 CPU 收到一个时钟周期的中断信号后就会加 1 。他比 PIT 精度高,但是不能编程,只能读取。
时钟周期:硬件计时器在多长时间内产生时钟脉冲,而时钟周期频率为1秒内产生时钟脉冲的个数。目前通常为1193180。
时钟滴答:当PIT中的初始值减到0的时候,就会产生一次时钟中断,这个初始值由编程的时候指定。

Linux启动的时候,先通过 RTC 获取初始时间,之后内核通过 PIT 中的定时器的时钟滴答来维护日期,并且会定时将该日期写入 RTC,而应用程序的定时器主要是通过设置 PIT 的初始值设置的,当初始值减到0的时候,就表示要执行回调函数了,这里大家会不会有疑问,这样同一时刻只能有一个定时器程序了,而我们在应用程序中,以及多个应用程序之间, 肯定有好多定时器任务,其实我们可以参考 ScheduledExecutorService 的实现。
只需要将这些定时任务按照时间做一个排序,越靠前待执行的任务放在前面,第一个任务到了在设置第二个任务相对当前时间的值,毕竟 CPU 同一时刻也只能运行一个任务,关于时间的精度问题,我们无法在软件层面做的完全精准,毕竟 CPU 的调度不完全受用户程序控制,当然更大的依赖是硬件的时钟周期频率,目前 TSC 可以提高更高的精度。
现在我们知道了,Java 中的超时时间,是通过可编程间隔定时器设置一个初始值然后等待中断信号实现的,精度上受硬件时钟周期的影响,一般为毫秒级别,毕竟1纳秒光速也只有3米,所以 JDK 中带纳秒参数的实现都是粗暴做法,预留着等待精度更高的定时器出现,而获取当前时间 System.currentTimeMillis() 效率会更高,但他是毫秒级精度,他读取的 Linux 内核维护的日期,而 System.nanoTime() 会优先使用 TSC ,性能稍微低一点,但他是纳秒级,Random 类为了防止冲突就用nanoTime生成种子。
Java 如何和外部设备通信
计算机的外部设备有鼠标、键盘、打印机、网卡等,通常我们将外部设备和和主存之间的信息传递称为 I/O 操作 , 按操作特性可以分为,输出型设备,输入型设备,存储设备。现代设备都采用通道方式和主存进行交互,通道是一个专门用来处理IO任务的设备, CPU 在处理主程序时遇到I/O请求,启动指定通道上选址的设备,一旦启动成功,通道开始控制设备进行操作,而 CPU 可以继续执行其他任务,I/O 操作完成后,通道发出 I/O 操作结束的中断,处理器转而处理 IO 结束后的事件。其他处理 IO 的方式,例如轮询、中断、DMA,在性能上都不见通道,这里就不介绍了。当然 Java 程序和外部设备通信也是通过系统调用完成,这里也不在继续深入了。 阿里巴巴Java方向面试题汇总
要拿阿里Java研发岗 Offer吗?先来搞定这几道题!识别下方二维码或点击文末“阅读原文”立刻看题。

你可能还喜欢
点击下方图片即可阅读
为什么大部分人做不了架构师?这2点是关键
如何回答“性能优化”,才能打动阿里面试官?
关注「阿里技术」 把握前沿技术脉搏
戳这里,进阿里。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
