2.1 Código asociado
Precargar clases asociadas
public class MainClass {
static {
// 预加载MyClass,其实现了相关功能
Class.forName("com.example.MyClass");
}
// 运行相关功能的代码
// ...
}
usar grupo de subprocesos
usar variables estáticas
Puede usar variables estáticas para almacenar en caché objetos y datos relacionados con el código asociado. Al inicio del programa, el código asociado se puede precargar y los objetos o datos se pueden almacenar en variables estáticas. Los objetos o datos almacenados en caché en variables estáticas se utilizan mientras el programa se ejecuta para evitar la carga y generación repetidas. Este método puede mejorar efectivamente el rendimiento del programa, pero debe prestar atención al uso de variables estáticas para garantizar su seguridad en un entorno de subprocesos múltiples.
2.2 Alineación de caché
-
Línea de caché: cuando la CPU lee datos de la memoria, no solo lee un byte a la vez, sino que generalmente lee un bloque continuo de memoria (trozos de memoria) con una longitud de 64 bytes (determinada por el hardware), que llamamos para la línea de caché. -
Uso compartido falso (uso compartido falso): cuando dos subprocesos que se ejecutan en dos CPU diferentes escriben en dos variables diferentes, si las dos variables están almacenadas en la misma línea de caché de la CPU, se produce un uso compartido falso (uso compartido falso). Es decir, cuando el primer subproceso modifica una de las variables en la línea de caché, las líneas de caché de otros subprocesos que hacen referencia a esta variable de línea de caché no serán válidas. Si la CPU necesita leer una línea de caché obsoleta, debe esperar a que se vacíe la línea de caché, lo que da como resultado un rendimiento deficiente. -
Bloqueo de la CPU: cuando un núcleo necesita esperar a que otro núcleo vuelva a cargar la línea de caché (cuando se produce un intercambio falso), no puede continuar ejecutando la siguiente instrucción, solo puede detenerse y esperar, lo que se denomina bloqueo. Reducir el uso compartido falso también significa reducir la ocurrencia de estancamientos. -
IPC (instrucciones por ciclo): Representa el número promedio de instrucciones ejecutadas por ciclo de CPU, obviamente, cuanto mayor sea el valor, mejor será el rendimiento. Basado en el índice IPC (por ejemplo: umbral 1.0), se puede juzgar simplemente si el programa es intensivo en acceso o computación. En el sistema Linux, puede usar el comando tiptop para ver los datos del hardware de la CPU de cada proceso:
-
Si IPC < 1.0, es probable que la parada de memoria domine, lo que probablemente signifique un uso intensivo de memoria. -
Si IPC > 1.0, es probable que sea un programa computacionalmente intensivo.
-
Utilización de la CPU: se refiere a la relación entre el tiempo que la CPU está ocupada en el sistema y el tiempo total. El tiempo de estado ocupado se puede dividir en ciclo de ciclo de consumo de ejecución de instrucción (instrucción) (%INS) y ciclo de ciclo detenido (%STL). perf recopila el estado de ejecución de todas las CPU en 10 segundos:
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)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
{{o.name}}
{{m.name}}