写出高性能代码的几个小技巧

936 阅读5分钟

CPU缓存

cpu缓存结构

图片.png

局部性原理

是指CPU访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。

  • 时间局部性:如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。程序循环、堆栈等是产生时间局部性的原因。
  • 空间局部性:在最近的将来将用到的信息很可能与正在使用的信息在空间地址上是临近的。
  • 顺序局部性:在典型程序中,除转移类指令外,大部分指令是顺序进行的。顺序执行和非顺序执行的比例大致是5:1。此外,对大型数组访问也是顺序的。指令的顺序执行、数组的连续存放等是产生顺序局部性的原因。

cpu缓存应用场景

  • 数组遍历遵从低维到高维顺序遍历
  • 共享变量独占cpu缓存行,避免伪共享

内存分配

我们写的每一行代码都占用了内存(方法区),代码运行时会占据栈或堆,不恰当的类加载机制和逻辑会引起内存申请和回收问题。如何优雅的进行内存分配和回收是写出高效代码的关键。java中又有哪些内存分配策略呢?

撞针分配

撞针分配也叫线性分配,就是顺序使用内存。但是内存在使用一段时间后,经过垃圾回收会出现内存碎片,变的不太整齐,针对这种情况又引入freeList。

FreeList

freelist用来记录那些曾被使用后来又被释放了的内存地址,下次分配对象的时候,优先从 FreeList 中寻找合适的内存大小进行分配,之后再在主内存中撞针分配。撞针分配和freelist一般搭配使用,看似解决了内存碎片的问题,但针对并发申请内存的时候出现CAS自旋导致效率地下的问题并没有解决。

TLAB

JVM为了解决多线程并发申请内存冲突,提出了内存预分配策略,就是提前为线程在堆上分配一定大小的内存,如果线程内申请内存则优先从TLAB进行分配。

类分配过程

图片.png

对象分配流程

图片.png

针对new对象的分配,优先在栈上进行逃逸分析,通过标量替换的方式尝试分配

对象在内存的引用方式

图片.png

对象在内存中的结构

图片.png

markword存储了hashcode,gc分代年龄,锁状态标识等;对齐填充是为了保证对象是8个字节的整数倍。

java查看对象大小: ObjectSizeCalculator.getObjectSize(obj)

内存分配运用

图片.png

看这个

应用级缓存

hash表

应用级缓存就缓存到hash表,直接用不要再有计算,累,时间复杂度O(1)。

hash冲突解决方法:链表法和开发寻址法

缓存更新策略

  • 如果数据量不大,每次都全量更新,利用引用替换;

  • 如果数据量很大,则增量更新,利用写实复制原理

零拷贝

  • 我们一般这样做:4次拷贝,4次切换

图片.png

如果内核在读取文件后,直接把 PageCache 中的内容拷贝到 Socket 缓冲区,待到网卡发送完毕后,再通知进程。

  • 我们可以这样做:2次切换,3次拷贝

图片.png

如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术,还可以再去除 Socket 缓冲区的拷贝,这样一共只有 2 次内存拷贝。

图片.png

PageCache预读功能

pagecache是磁盘高速缓存,利用了时间局部性,会将一部分数据预先从磁盘读入到内存中。但是如果在进行大文件读取时,pagecache每次都会被刷新,导致命中率非常低,那么预读反而会降低性能。

异步IO

高并发场景处理大文件时,应当使用异步 IO 和直接 IO 来替换零拷贝技术。

图片.png

异步 IO(异步 IO 既可以处理网络 IO,也可以处理磁盘 IO,这里我们只关注磁盘 IO)可以解决阻塞问题。它把读操作分为两部分,前半部分向内核发起读请求,但不等待数据就位就立刻返回,此时进程可以并发地处理其他任务。当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据,这是异步 IO 的后半部分。如下图所示:

图片.png 从图中可以看到,异步 IO 并没有拷贝到 PageCache 中。经过 PageCache 的 IO 我们称为缓存 IO。

IO总结:BIO,NIO,AIO

同步与异步

  • 同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
  • 异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

简而言之,同步异步说的是消息怎么发给调用者的,比如食堂点菜是自己拿还是服务员送过来;阻塞非阻塞说的是调用者有没有被挂起,比如食堂点菜是在柜台上干等着还是可以坐下来玩手机聊天。

看这里

BIO:死等 图片.png

NIO:定时轮训数据是否就绪,然后阻塞读。(轮询有上线文切换)

图片.png

IO多路复用(NIO):不用轮训,系统通知数据继续,然后阻塞读。(一次切换,内核轮询)

图片.png

AIO:直接给数据

图片.png