
2.1 关联代码
预加载关联类
public class MainClass {
static {
// 预加载MyClass,其实现了相关功能
Class.forName("com.example.MyClass");
}
// 运行相关功能的代码
// ...
}
使用线程池
使用静态变量
可以使用静态变量来缓存与关联代码有关的对象和数据。在程序启动时,可以预先加载关联代码,并将对象或数据存储在静态变量中。然后在程序运行时使用静态变量中缓存的对象或数据,以避免重复加载和生成。这种方式可以有效地提高程序的性能,但需要注意静态变量的使用,确保它们在多线程环境中的安全性。
2.2 缓存对齐

-
缓存行(Cache line):CPU读取内存数据时并非一次只读一个字节,一般是会读一段64字节(硬件决定)长度的连续的内存块(chunks of memory),这些块我们称之为缓存行。 -
伪共享(False Sharing):当运行在两个不同CPU上的两个线程写入两个不同的变量时,如果这两个变量恰好存储在同一个 CPU 缓存行中,就会发生伪共享(False Sharing)。即当第一个线程修改缓存行中其中一个变量时,其他引用此缓存行变量的线程的缓存行将会无效。如果CPU需要读取失效的缓存行,它必须等待缓存行刷新,这会导致性能下降。 -
CPU停止运转(stall):当一个核心需要等待另一个核心重新加载缓存行时(出现伪共享时),它无法继续执行下一条指令,只能停止运转等待,这被称之为stall。减少伪共享也就意味着减少了stall的发生。 -
IPC(instructions per cycle):它表示平均每个 CPU 周期执行的指令数量,很显然该数值越大性能越好。可以基于IPC指标(比如:阈值1.0)来简单判断程序是属于访问密集型还是计算密集型。Linux系统中可以通过tiptop命令来查看每个进程的CPU硬件数据:

-
如果 IPC < 1.0, 很可能是 Memory stall 占主导,多半意味着访存密集型。 -
如果IPC > 1.0, 很可能是计算密集型的程序。
-
CPU利用率:是指系统中CPU处于忙碌状态的时间与总时间的比例。忙碌状态时间又可以进一步拆分为指令(instruction)执行消耗周期cycle(%INS) 和 stalled 的周期cycle(%STL)。perf 采集了10秒内全部 CPU 的运行状态:

IPC计算
IPC = instructions/cycles
上图中,可以计算出结果为:0.79
现代处理器一般有多条流水线(比如:4核心),运行 perf 的那台机器,IPC的理论值可达到4.0。
如果我们从 IPC的角度来看,这台机器只运行到其处理器最高速度的 19.7%(0.79 / 4.0)。
-
缓存对齐:是通过调整数据在内存中的分布,让数据在被缓存时,更有利于CPU从缓存中读取,从而避免了频繁的内存读取,提高了数据访问的速度。
缓存填充(Padding)
/**
* 缓存行填充测试
*
* @author liuhuiqing
* @date 2023年04月28日
*/
public class FalseSharingTest {
private static final int LOOP_NUM = 1000000000;
public static void main(String[] args) throws InterruptedException {
Struct struct = new Struct();
long start = System.currentTimeMillis();
Thread t1 = new Thread(() -> {
for (int i = 0; i < LOOP_NUM; i++) {
struct.x++;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < LOOP_NUM; i++) {
struct.y++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("cost time [" + (System.currentTimeMillis() - start) + "] ms");
}
static class Struct {
// 共享变量
volatile long x;
// 一个long占用8个字节,此处定义7个填充数据,来保证业务数据x和y分布在不同的缓存行中
long p1, p2, p3, p4, p5, p6, p7;
// long[] paddings = new long[7];// 使用数组代替不会生效,思考一下,为什么?
// 共享变量
volatile long y;
}
}
@Contended注解
import sun.misc.Contended;
public class ContendedTest {
@Contended
volatile long a;
@Contended
volatile long b;
public static void main(String[] args) throws InterruptedException {
ContendedTest c = new ContendedTest();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000_0000L; i++) {
c.a = i;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000_0000L; i++) {
c.b = i;
}
});
final long start = System.nanoTime();
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println((System.nanoTime() - start) / 100_0000);
}
}
对齐内存与本地变量
-
对齐内存:内存行的大小一般为64个字节,这个大小是硬件决定的,但大多数编译器默认情况下都以4字节的边界对齐,通过将变量按照内存行的大小对齐,可以避免伪共享问题; -
本地变量:在不同线程之间使用不同的变量存储数据,避免不同的线程之间共享同一块内存,Java中的ThreadLocal就是一种典型的实现方式;
2.3 分支预测
-
关注圈复杂度
-
优先处理常用路径
2.4 写时复制
// 初始化数组
private List<String> list = new CopyOnWriteArrayList<>();
// 向数组中添加元素
list.add("value");
需要注意的是,Copy-On-Write机制适用于读操作比写操作多的情况,因为它假定写操作的频率较低,从而可以通过牺牲复制的开销来减少锁的操作和内存分配的消耗。
2.5 内联优化
final修饰符
限制方法长度
JVM参数 | 默认值 (JDK 8, Linux x86_64) | 参数说明 |
-XX:MaxInlineSize=<n> | 35 字节码 | 内联方法大小上限 |
-XX:FreqInlineSize=<n> | 325 字节码 | 内联热方法的最大值 |
-XX:InlineSmallCode=<n> | 1000字节的原生代码(非分层) 2000字节的原生代码(分层编译) | 如果最后一层的的分层编译代码量已经超过这个值,就不进行内联编译 |
-XX:MaxInlineLevel=<n> | 9 |
调用层级比这个值深的话,就不进行内联 |
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+JVMCICompiler
@ForceInline
public static int add(int a, int b) {
return a + b;
}
2.6 编码优化
反射机制
-
尽可能使用原生方法调用,而不是通过反射调用; -
尽可能缓存反射调用结果,避免重复调用。例如,可以将反射结果缓存到静态变量中,以便下次使用时直接获取,而不必再次使用反射; -
使用字节码增强技术;
-
反射结果缓存可以大幅减少反射过程中的类型检查,类型转换和方法查找等动作,是降低反射对程序执行效率影响的一种优化策略。
/**
* 反射工具类
*
* @author liuhuiqing
* @date 2023年5月7日
*/
public abstract class BeanUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(BeanUtils.class);
private static final Field[] NO_FIELDS = {};
private static final Map<Class<?>, Field[]> DECLARED_FIELDS_CACHE = new ConcurrentReferenceHashMap<Class<?>, Field[]>(256);
private static final Map<Class<?>, Field[]> FIELDS_CACHE = new ConcurrentReferenceHashMap<Class<?>, Field[]>(256);
/**
* 获取当前类及其父类的属性数组
*
* @param clazz
* @return
*/
public static Field[] getFields(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("Class must not be null");
}
Field[] result = FIELDS_CACHE.get(clazz);
if (result == null) {
Field[] fields = NO_FIELDS;
Class<?> searchType = clazz;
while (Object.class != searchType && searchType != null) {
Field[] tempFields = getDeclaredFields(searchType);
fields = mergeArray(fields, tempFields);
searchType = searchType.getSuperclass();
}
result = fields;
FIELDS_CACHE.put(clazz, (result.length == 0 ? NO_FIELDS : result));
}
return result;
}
/**
* 获取当前类属性数组(不包含父类的属性)
*
* @param clazz
* @return
*/
public static Field[] getDeclaredFields(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("Class must not be null");
}
Field[] result = DECLARED_FIELDS_CACHE.get(clazz);
if (result == null) {
result = clazz.getDeclaredFields();
DECLARED_FIELDS_CACHE.put(clazz, (result.length == 0 ? NO_FIELDS : result));
}
return result;
}
/**
* 数组合并
*
* @param array1
* @param array2
* @param <T>
* @return
*/
public static <T> T[] mergeArray(final T[] array1, final T... array2) {
if (array1 == null || array1.length < 1) {
return array2;
}
if (array2 == null || array2.length < 1) {
return array1;
}
Class<?> compType = array1.getClass().getComponentType();
int newArrLength = array1.length + array2.length;
T[] newArr = (T[]) Array.newInstance(compType, newArrLength);
int firstArrayLen = array1.length;
System.arraycopy(array1, 0, newArr, 0, firstArrayLen);
try {
System.arraycopy(array2, 0, newArr, firstArrayLen, array2.length);
} catch (ArrayStoreException ase) {
final Class<?> type2 = array2.getClass().getComponentType();
if (!compType.isAssignableFrom(type2)) {
throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of "
+ compType.getName(), ase);
}
throw ase;
}
return newArr;
}
}
-
字节码增强技术,一般使用第三方库来实现,例如Javassist或Byte Buddy,在运行时生成字节码,从而避免使用反射。
-
动态字节码生成的方式在编译期就已经将类型信息确定下来,无需进行类型检查和转换; -
动态字节码生成的方式可以直接调用方法,无需查找,提高了执行效率; -
动态字节码生成的方式只需要在生成字节码时获取一次Method对象,多次调用时可以直接使用,避免了重复获取Method对象的开销;
异常处理
-
响应延迟:当异常被抛出时,Java虚拟机需要查找并执行相应的异常处理程序,这会导致一定的延迟。如果程序中存在大量的异常处理,这些延迟可能会累积,导致程序的整体性能下降。 -
内存占用:异常处理需要在堆栈中创建异常对象,这些对象需要占用内存。如果程序中存在大量的异常处理,这些异常对象可能会占用大量的内存,导致程序的整体内存占用量增加。 -
CPU占用:异常处理需要执行额外的代码,这会导致CPU占用率增加。如果程序中存在大量的异常处理,这些额外的代码可能会导致CPU占用率过高,导致程序的整体性能下降。
日志处理
LOGGER.info("result:" + JsonUtil.write2JsonStr(contextAdContains) + ", logid = " + DigitThreadLocal.getLogId());
以上示例代码中,类似的日志打印方式很常见,难道有什么问题吗?
-
性能问题:每次使用+进行字符串拼接时,都会创建一个新的字符串对象,这可能会导致内存分配和垃圾回收的开销增加; -
可读性问题:使用+进行字符串拼接时,代码可能会变得难以阅读和理解,特别是在需要连接多个字符串时; -
如果日志级别调整到ERROR模式,我们希望日志的字符串内容不需要进行加工计算,但这种写法,即使日志处于不需要打印的模式,日志内容也进行了无效计算;
临时对象
-
字符串拼接中,使用StringBuilder或StringBuffer进行字符串拼接,避免使用连接符,每次都创建新的字符串对象; -
在集合操作中,尽量使用批量操作,如addAll、removeAll等,避免频繁的add、remove操作,触发数组的扩容或者缩容; -
在正则表达式中,可以使用Pattern.compile()方法预编译正则表达式,避免每次都创建新的Matcher对象; -
尽量使用基本数据类型,避免使用包装类,因为包装类的创建和销毁都会产生临时对象; -
尽量使用对象池的方式创建和管理对象,比如使用静态工厂方法创建对象,避免使用new关键字创建对象,因为静态工厂方法可以重用对象,避免创建新的临时对象;
-
对象未被正确地释放:如果在方法执行完毕后,临时对象没有被正确地释放,就会导致内存泄漏风险; -
对象过度共享:如果临时对象被过度共享,就可能会导致多个线程同时访问同一个对象,从而导致线程安全问题和性能问题; -
对象创建过于频繁:如果在方法内部频繁地创建临时对象,就会导致内存开销过大,可能会引起性能甚至内存溢出问题;
-
及时释放对象:在方法执行完毕后,应该及时释放临时对象(比如主动将对象设置为null),以便回收内存资源; -
避免过度共享:在多线程环境下,应该避免过度共享临时对象,可以使用局部变量或ThreadLocal等方式来避免共享问题; -
对象池技术:使用对象池技术可以避免频繁创建临时对象,从而降低内存开销。对象池可以预先创建一定数量的对象,并在需要时从池中获取对象,使用完毕后再将对象放回池中;
小结
3.1 缓存
/**
* Least recently used 内存缓存过期策略:最近最少使用
* Title: 带容量的<b>线程不安全的</b>最近访问排序的Hashmap
* Description: 最后访问的元素在最后面。<br>
* 如果要线程安全,请使用<pre>Collections.synchronizedMap(new LRUHashMap(123));</pre> <br>
*
* @author: liuhuiqing
* @date: 20123/4/27
*/
public class LRUHashMap<K, V> extends LinkedHashMap<K, V> {
/**
* The Size.
*/
private final int maxSize;
/**
* 初始化一个最大值, 按访问顺序排序
*
* @param maxSize the max size
*/
public LRUHashMap(int maxSize) {
//0.75是默认值,true表示按访问顺序排序
super(maxSize, 0.75f, true);
this.maxSize = maxSize;
}
/**
* 初始化一个最大值, 按指定顺序排序
*
* @param maxSize 最大值
* @param accessOrder true表示按访问顺序排序,false为插入顺序
*/
public LRUHashMap(int maxSize, boolean accessOrder) {
//0.75是默认值,true表示按访问顺序排序,false为插入顺序
super(maxSize, 0.75f, accessOrder);
this.maxSize = maxSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > maxSize;
}
}
3.2 异步
非阻塞IO
"/async/callable") (
public WebAsyncTask<String> asyncCallable() {
Callable<String> callable = () -> {
// 执行异步操作
return "异步任务已完成";
};
return new WebAsyncTask<>(10000, callable);
}
"/async/deferredresult") (
public DeferredResult<String> asyncDeferredResult() {
DeferredResult<String> deferredResult = new DeferredResult<>(10000L);
// 异步处理完成后设置结果
deferredResult.setResult("DeferredResult异步任务已完成");
return deferredResult;
}
协程
Thread thread = Thread.ofVirtual()
.name("Virtual Threads")
.unstarted(runnable);
ThreadFactory factory = Thread.ofVirtual().factory();
3.3 并行
-
分布式计算框架中的MapReduce就是采用一种分而治之的思想设计出来的,将复杂或计算量大的任务,切分成一个个小的任务,小任务分别在不同的线程或服务器上并行的执行,最终再汇总每个小任务的结果。 -
边缘计算(Edge Computing)是一种分布式计算范式,它将计算、存储和网络服务的部分功能从云数据中心延伸至离数据源更近的地方,即网络的边缘。这种计算方式能够实现低延迟、节省带宽、提高数据安全性以及实时处理与分析等优势。
-
多个请求可以通过多线程并行处理,每个请求的不同处理阶段; -
如查询阶段,可以采用协程并行执行; -
存储阶段,可以采用消息订阅发布的方式进行处理; -
监控统计阶段,就可以采用NIO异步的方式进行指标数据文件的写入; -
请求/响应采用非阻塞IO模式;
3.4 池化
池化就是初始预设资源,降低每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。典型的场景就是线程池,数据库连接池,业务处理结果缓存池等。
-
建立TCP连接,通过三次握手实现; -
服务器发送给客户端「握手信息」 ,客户端响应该握手消息; -
客户端「发送认证包」 ,用于用户验证,验证成功后,服务器返回OK响应,之后开始执行命令;
-
公用的数据可以全局只定义一份,比如使用枚举,static修饰的容器对象等; -
根据实际情况,提前设置List,Map等容器对象的初始化容量大小,防止后面的扩容,对性能的影响; -
亨元设计模式的应用等;
3.5 预处理
-
为了提高响应性能,将部分业务数据提前预加载到内存中; -
为了减轻CPU压力,将计算逻辑提前执行,直接将计算后的结果数据保存下来,直接供调用方使用; -
为了降低网络带宽成本,将传输数据通过压缩算法进行压缩处理,到了目标服务,再进行解压,获得原始数据; -
Myibatis为了提高SQL语句的安全性和执行效率,也引入了预处理的概念;
性能优化是程序开发过程中绕不过去一个课题,本文聚焦代码和设计两个方面,从CPU硬件到JVM容器,从缓存设计到数据预处理,全面的展现了性能优化的实施方向和落地细节。阐述的过程没有追求各个方向的面面俱到,但都给到了一些场景化案例,来辅助理解和思考,起到抛砖引玉的效果。最后,希望本文能够为你带来思考和帮助。
-end-
本文分享自微信公众号 - 京东云开发者(JDT_Developers)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。