應用層面
- 反射操作記得緩存method和field,最好能用方法句柄或者字節碼增強替換掉
public class PerformanceOptimizationDemo {
private static final Method METHOD;
static {
METHOD = "獲取method";
}
}
更多細節見 Java反射性能詳解
- 原生String的split和replaceAll請謹慎使用,StringTokenizer是個更好的選擇,或者這裏推薦org.apache.commons.lang3.StringUtils#replace(String, String, String)這個工具類
- 慎用String.intern,當字符串數量非常多時使用hashmap做緩存性能要好得多
- 放棄Random隨機數請用ThreadLocalRandom
public static int getRandomNum() {
// 性能略差
return new Random().nextInt();
// 性能更好的平替
return ThreadLocalRandom.current().nextInt();
}
- 正確應用單例模式,邏輯類儘量使用單例,需注意處理線程安全問題,這裏推薦靜態工廠方式,其唯一的問題是較多的冗餘代碼,可使用插件解決
public class PerformanceOptimizationDemo {
private PerformanceOptimizationDemo(){
}
private static class LazyHolder {
private static final PerformanceOptimizationDemo INSTANCE = new PerformanceOptimizationDemo();
}
protected static PerformanceOptimizationDemo getInstance() {
return PerformanceOptimizationDemo.LazyHolder.INSTANCE;
}
}
- 使用三方庫如果是實例級的方法而非static的使用方法,查看api查看實例是否線程安全,線程安全時創建一個單例實例,而非在方法中每次都創建一個實例如ObjectMapper,此外對應各種Factory和Context要更嚴格地檢查,頻繁地創建Factory和Context可能會對內存造成較大的壓力甚至導致Fullgc
private static final ObjectMapper DEFAULT_OBJECT_MAPPER = getObjectMapper();
- 遞歸調用層次比較深時但可以預見棧底時優化為循環(相當於手動實現尾遞歸轉為循環)
- 合理使用多級緩存, 本地緩存->遠程緩存(redis、etcd) ,做緩存時考慮好超時時間、淘汰策略、容量規劃、最終一致性,這裏推薦jetcache
@Override
@Cached(name = "user.", key = "#id", expire = 3600, cacheType = CacheType.REMOTE, postCondition = "result != null")
public UserInfo getData(Long id) {
return Optional.ofNullable(userMapper.selectById(id))
.map(user -> convert(user, UserInfo.class))
.orElse(null);
}
9.接上一條,業務配置數據在初始化時直接加載到緩存和內存,優先從內存和緩存讀取,沒有再去讀數據庫並加載到內存和緩存中
- Mq消息批處理
- 數據庫批處理(儘量不要在for循環裏面進行數據庫操作),注意jdbc參數rewriteBatchedStatements和allowMultiQueries要為true
private final UserMapper userMapper;
public void batchInsert(List<UserDo> userList) {
// 此種操作會佔用較多的數據庫連接且多次訪問數據庫rt會慢很多
userList.forEach(user -> userMapper.insert(user));
}
- 數據庫部分場景下有事務比沒有事務要執行的更快,比如一些查詢雖然是本身是無事務性的,但是多個小的查詢放在一個事務中有利於複用連接避免了從連接池中獲取連接的開銷(沒有連接池的話這個應用是存在一定問題的)
@Transactional(readOnly = true)
public void doSomething() {
doQuery0();
doQuery1();
doQuery2();
}
- 巨型對象使用完手動賦值null(請一定確認是個巨型對象也不會再次複用)
- 業務允許的情況下使用cas+重試替換悲觀鎖
- 涉及大文件操作,使用nio技術優化寫入(小文件可能適得其反),簡單示例:
public static void writeToFile(String filePath, String content) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filePath);
FileChannel channel = fos.getChannel()) {
// 將內容轉換為字節數組
byte[] bytes = content.getBytes();
// 創建ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
buffer.put(bytes);
// 將Buffer切換為讀取模式
buffer.flip();
// 將數據寫入到文件通道
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
}
- 開啓多線程多個獨立任務併發進行(謹慎使用, 多線程調試困難且使用不當會引發全局性的問題),一般而言多線程主要是實現rt的降低並不會實現資源利用率的提升
- 接上一條線程應當在線程池中運行,順便貼一些使用線程池的注意點:
-
線程池使用的注意點
- ThreadLocal(登錄信息上下文或其它的業務信息)丟失;
- 合適的任務隊列及其大小,過大會造成 oom ;
- 全鏈路 id 丟失;
- 合適的線程池策略和線程數(固定數目和不定數目);
- 任務重啓丟失(優雅退出);
- 大循環裏面可以使用Thread.sleep(0) 讓線程重新競爭,可以更充分的利用cpu資源(上下文切換也有開銷),native方法執行以後就會插入一個safepoint,可以幫助gc
- 嘗試r2dbc這種異步數據庫連接框架(謹慎,需搭配一整套異步框架使用)
- 日誌異步化, 異步輸出日誌不阻塞業務線程,資源允許的情況下引入類似kafka+ elk的日誌系統,把日誌的開銷直接抽離業務系統
- 數據庫連接預熱,緩存預熱及其它可預熱的資源,在預熱和資源空閒率之間做權衡
- 連接數和線程數調整(數據庫連接池、redis連接池、http連接池、tomcat線程數配置, dubbo處理線程數配置)等
- 使用aop切面功能時將aspectj相關jar包版本升級到1.9.0及以上,如果是根據註解切面RetentionPolicy應設置為RUNTIME
- 使用org.apache.commons.io.output.ByteArrayOutputStream替換jdk的java.io.ByteArrayOutputStream
數據庫層面
- 合理設計表結構:冗餘等要適當,要對數據量做合理的評估
-
分庫分表:需考慮分庫事務、讀擴散等問題,對原有的join方式可能不兼容
-
分庫分表方式
- 客户端分表
- 使用數據庫代理層
- 使用分佈式數據庫
-
分庫分表策略
- 按分片鍵水平分
- 按時間(年/月/周/日)分
- 其它自定義策略
-
- 合理加索引:是否有足夠區分度?聯合索引還是獨立索引?是否唯一?索引容量。
- 索引性能和預期時刻想差較大時刻重建索引(該項慎重,小心鎖表時間過長影響業務)
- 定期優化表消除空間碎片,可考慮業務低谷期重做一些表(需考慮表的大小預估時間),最好配合合理的數據歸檔
機器層面
- redis綁核
- jvm參數調優(該項做不好就是反向優化需慎重)
- 內核參數調整(文件打開數、線程限制、頁大小等需慎重)
網絡層面
- cdn優化尋址
- nginx配置優化(工作線程數等,nginx是一個多進程架構)
- tcp參數調優
性能優化沒有銀彈,所有的優化要以業務的訴求和業務量為基石,實踐是檢驗真理的唯一標準。
原文地址: https://pebble-skateboard-d46.notion.site/Java-9e8e05c6bdcf4b...