java 性能 优化_Java十大简单性能优化
java 性能 優化
關于“ web scale ”這個流行詞有很多炒作,人們花了很多時間來重新組織他們的應用程序體系結構,以使其系統“規?;薄?
但是什么是擴展,我們如何確??梢詳U展?
縮放的不同方面
上面提到的炒作主要是關于擴展負載 ,即確保適用于1個用戶的系統也能很好地適用于10個用戶,100個用戶或數百萬個用戶。 理想情況下,您的系統應盡可能“無狀態”,以便可以在網絡中的任何處理單元上轉移和轉換真正保留的少量狀態。 當負載是您的問題時,延遲可能就沒有了,因此如果單個請求花費50-100ms沒關系。 這通常也稱為橫向擴展
擴展的一個完全不同的方面是擴展性能 ,即,確保適用于1條信息的算法也適用于10條或100條或數百萬條。 Big O Notation最好地描述了這種縮放是否可行。 延遲是擴展性能的殺手。 您想盡一切可能將所有計算保持在一臺計算機上。 這通常也稱為放大
如果有免費午餐之類的東西( 沒有 ),我們可以無限地組合擴大規模和擴大規模。 無論如何,今天,我們將研究一些非常簡單的方法來改善性能。
大O符號
Java 7的ForkJoinPool以及Java 8的并行Stream有助于并行化內容,這在將Java程序部署到多核處理器計算機上時非常有用。 與在網絡上的不同計算機上進行擴展相比,這種并行性的優勢在于您幾乎可以完全消除延遲影響,因為所有內核都可以訪問同一內存。
但是,不要被并行性的效果所迷惑! 請記住以下兩件事:
- 并行主義吞噬了您的核心。 這對于批處理非常有用,但是對于異步服務器(例如HTTP)來說卻是一場噩夢。 在過去的幾十年中,我們使用單線程servlet模型是有充分的理由的。 因此,并行性僅在擴大規模時有用。
- 并行性對算法的Big O表示法沒有影響。 如果您的算法是O(n log n) ,并且讓該算法在c核上運行,那么您仍將擁有O(n log n / c)算法,因為c在算法復雜度上是微不足道的常數。 您將節省時鐘時間,但不會降低復雜性!
當然,提高性能的最佳方法是降低算法復雜度。 當然,殺手是實現O(1)或準O(1) ,例如HashMap查找。 但這并不總是可能的,更不用說輕松了。
如果您無法降低復雜性,只要找到合適的位置,只要對算法真正重要的地方進行調整,您仍然可以獲得很多性能。 假定算法的以下直觀表示形式:
如果要處理單個數量級,該算法的總體復雜度為O(N 3 )或O(N x O x P) 。 但是,在分析此代碼時,您可能會發現一個有趣的場景:
- 在開發框中,左分支( N -> M -> Heavy operation )是您可以在分析器中看到的唯一分支,因為O和P的值在開發樣本數據中很小。
- 但是,在生產中,右分支( N -> O -> P -> Easy operation或NOPE )確實會造成麻煩。 您的運營團隊可能已經使用AppDynamics或DynaTrace或某些類似的軟件解決了這一問題。
沒有生產數據,您可能會Swift得出結論并優化“繁重的操作”。 您將產品運送到生產中,并且修復無效。
除了以下事實外,沒有最佳的黃金法則:
- 精心設計的應用程序更容易優化
- 過早的優化不會解決任何性能問題,但會使您的應用程序設計欠佳,從而使優化變得更加困難
足夠的理論。 假設您已找到問題所在的正確分支。 很容易在生產中吹起一個非常簡單的操作,因為它被稱為很多次(如果N , O和P大)。 請在不可避免的O(N 3 )算法的葉節點存在問題的情況下閱讀本文。 這些優化不會幫助您擴展規模。 他們將幫助您暫時節省客戶的時間,將整個算法的困難改進推遲到以后!
以下是Java中最容易進行的10個性能優化:
1.使用StringBuilder
這幾乎是所有Java代碼中的默認設置。 盡量避免使用+運算符。 當然,您可能會爭辯說它仍然只是StringBuilder語法糖,例如:
String x = "a" + args.length + "b";…編譯成
0 new java.lang.StringBuilder [16]3 dup4 ldc <String "a"> [18]6 invokespecial java.lang.StringBuilder(java.lang.String) [20]9 aload_0 [args] 10 arraylength 11 invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23] 14 ldc <String "b"> [27] 16 invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29] 19 invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32] 22 astore_1 [x]但是會發生什么,如果以后需要用可選部分修改String呢?
String x = "a" + args.length + "b";if (args.length == 1)x = x + args[0];現在,您將擁有第二個StringBuilder ,它不必要地消耗了堆內存,從而給GC帶來了壓力。 改寫這個:
StringBuilder x = new StringBuilder("a"); x.append(args.length); x.append("b");if (args.length == 1);x.append(args[0]);帶走
在上面的示例中,如果您使用顯式StringBuilder實例,或者依賴Java編譯器為您創建隱式實例,則可能完全不相關。 但是請記住,我們在NOPE分支中 。 每個CPU周期我們都在浪費像GC這樣愚蠢的東西,或者分配StringBuilder的默認容量,我們浪費的時間是N x O x P次。
根據經驗,請始終使用StringBuilder而不是+運算符。 并且如果可以的話,如果您的String構建起來比較復雜,則可以跨多個方法保留StringBuilder引用。 這是jOOQ在生成復雜SQL語句時所做的。 只有一個StringBuilder可以“遍歷”您的整個SQL AST(抽象語法樹)
為了大聲喊叫,如果仍然有StringBuffer引用,請用StringBuilder替換它們。 您實際上幾乎不需要同步正在創建的字符串。
2.避免使用正則表達式
正則表達式相對便宜且方便。 但是,如果您位于NOPE分支中 ,那么它們將是您最糟糕的事情。 如果您絕對必須在計算密集型代碼節中使用正則表達式,則至少要緩存Pattern引用,而不要一直重新編譯它:
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");但是如果你的正則表達式真的很傻
String[] parts = ipAddress.split("\\.");…那么您真的最好使用普通的char[]或基于索引的操作。 例如,這個完全不可讀的循環執行相同的操作:
int length = ipAddress.length(); int offset = 0; int part = 0; for (int i = 0; i < length; i++) {if (i == length - 1 || ipAddress.charAt(i + 1) == '.') {parts[part] = ipAddress.substring(offset, i + 1);part++;offset = i + 2;} }……這也說明了為什么您不應該進行任何過早的優化。 與split()版本相比,這是無法維護的。
挑戰:讀者中的聰明人可能會找到更快的算法。
帶走
正則表達式很有用,但要付出一定的代價。 如果您深入了解NOPE分支 ,則必須不惜一切代價避免使用正則表達式。 提防使用正則表達式的各種JDK String方法,例如String.replaceAll()或String.split() 。
請改用諸如Apache Commons Lang之類的流行庫來進行String操作。
3.不要使用iterator()
現在,此建議實際上并不適用于一般用例,而僅適用于NOPE分支的深處。 但是,您應該考慮一下。 編寫Java-5樣式的foreach循環很方便。 您可以完全忘記循環內部,然后編寫:
for (String value : strings) {// Do something useful here }但是,每次遇到此循環時,如果strings是Iterable ,則將創建一個新的Iterator實例。 如果您使用ArrayList ,這將在堆上分配一個3 ints的對象:
private class Itr implements Iterator<E> {int cursor;int lastRet = -1;int expectedModCount = modCount;// ...相反,您可以編寫以下等效循環,并僅在堆棧上“浪費”一個int值,這非常便宜:
int size = strings.size(); for (int i = 0; i < size; i++) {String value : strings.get(i);// Do something useful here }…或者,如果您的列表沒有真正改變,您甚至可以對其數組版本進行操作:
for (String value : stringArray) {// Do something useful here }帶走
從可寫性和可讀性以及從API設計的角度來看,迭代器,Iterable和foreach循環都非常有用。 但是,它們為每次迭代在堆上創建一個小的新實例。 如果您多次運行此迭代,則要確保避免創建此無用的實例,而改為編寫基于索引的迭代。
討論區
關于上述部分的一些有趣的分歧(特別是用索引訪問代替Iterator用法) 已經在Reddit上進行了討論 。
4.不要調用該方法
一些方法簡單昂貴。 在我們的NOPE分支示例中,葉子上沒有這樣的方法,但是您很可能有一個。 讓我們假設您的JDBC驅動程序需要經歷難以置信的麻煩才能計算ResultSet.wasNull()的值。 您自己SQL框架代碼可能如下所示:
if (type == Integer.class) {result = (T) wasNull(rs, Integer.valueOf(rs.getInt(index))); }// And then... static final <T> T wasNull(ResultSet rs, T value) throws SQLException {return rs.wasNull() ? null : value; }每次您從結果集中獲取一個int ,此邏輯將立即調用ResultSet.wasNull() 。 但是getInt()合同的內容為:
返回:列值; 如果值為SQL NULL,則返回值為0
因此,對上述內容進行簡單但可能極大的改進將是:
static final <T extends Number> T wasNull(ResultSet rs, T value ) throws SQLException {return (value == null || (value.intValue() == 0 && rs.wasNull())) ? null : value; }因此,這很容易:
帶走
不要在算法的“葉子節點”中調用昂貴的方法,而要緩存調用,或者在方法合同允許的情況下避免調用。
5.使用原語和堆棧
上面的示例來自jOOQ ,它使用了許多泛型,因此被迫對byte , short , int和long使用包裝器類型-至少在泛型將在Java 10和項目Valhalla中實現特殊化之前。 但是您的代碼中可能沒有此約束,因此應采取所有措施替換:
// Goes to the heap Integer i = 817598;… 這樣:
// Stays on the stack int i = 817598;使用數組時,情況會變得更糟:
// Three heap objects! Integer[] i = { 1337, 424242 };… 這樣:
// One heap object. int[] i = { 1337, 424242 };帶走
當您深入了解NOPE分支時 ,應該非常警惕使用包裝器類型。 可能會給GC帶來很大壓力,必須時刻加油清理垃圾。
一種特別有用的優化可能是使用某種原始類型并為其創建大型的一維數組,以及幾個定界符變量以指示編碼對象在數組上的確切位置。
trove4j是一個出色的原始集合庫,它比您的平均int[]要復雜一些 ,它隨LGPL一起提供。
例外
此規則有一個例外: boolean和byte值很少,因此無法完全由JDK緩存。 你可以寫:
Boolean a1 = true; // ... syntax sugar for: Boolean a2 = Boolean.valueOf(true);Byte b1 = (byte) 123; // ... syntax sugar for: Byte b2 = Byte.valueOf((byte) 123);對于其他整數基本類型的低值(包括char , short , int , long 。
但是僅當您將它們自動裝箱或調用TheType.valueOf() ,才調用構造函數!
除非確實需要新實例,否則切勿在包裝器類型上調用構造函數。
這個事實還可以幫助您為同事寫一個復雜的,愚蠢的愚人節玩笑
堆外
當然,您可能還想嘗試堆外數據庫,盡管它們更多是一個戰略決策,而不是本地優化。
彼得·勞瑞(Peter Lawrey)和本·科頓(Ben Cotton)撰寫的有關該主題的有趣文章是: OpenJDK和HashMap…安全地教老狗新手(超堆!)技巧
6.避免遞歸
像Scala這樣的現代函數式編程語言鼓勵使用遞歸,因為它們提供了將尾遞歸算法優化回到迭代算法的方法 。 如果您的語言支持這種優化,則可能會很好。 但是即使這樣,算法的最細微改動也可能會產生一個分支,從而阻止您的遞歸成為尾遞歸。 希望編譯器能夠檢測到這一點! 否則,您可能會浪費大量的堆??蚣?#xff0c;而這些內容可能只使用了幾個局部變量就已經實現了。
帶走
除了以下內容外,沒有什么要說的:當您深入NOPE分支時,始終喜歡迭代而不是遞歸。
7.使用entrySet()
如果要遍歷Map ,并且既需要鍵又需要值,則必須有一個很好的理由來編寫以下內容:
for (K key : map.keySet()) {V value : map.get(key); }…而不是以下內容:
for (Entry<K, V> entry : map.entrySet()) {K key = entry.getKey();V value = entry.getValue(); }當您位于NOPE分支時 ,無論如何,您應該警惕地圖,因為很多O(1)地圖訪問操作仍然很多。 而且訪問也不是免費的。 但是至少,如果您不能沒有地圖,請使用entrySet()進行迭代! 無論如何,都有Map.Entry實例,您只需要訪問它即可。
帶走
在地圖迭代期間同時需要鍵和值時,請始終使用entrySet() 。
8.使用EnumSet或EnumMap
在某些情況下,映射中可能的鍵數是預先已知的,例如在使用配置映射時。 如果該數字相對較小,則應真正考慮使用EnumSet或EnumMap ,而不是常規的HashSet或HashMap 。 通過查看EnumMap.put()可以很容易地解釋這一點:
private transient Object[] vals;public V put(K key, V value) {// ...int index = key.ordinal();vals[index] = maskNull(value);// ... }此實現的本質是這樣一個事實,即我們擁有一個索引值數組而不是哈希表。 當插入一個新值時,我們要查找映射項的所有工作就是向枚舉詢問其常量序數,該序數由Java編譯器在每種枚舉類型上生成。 如果這是一個全局配置映射(即,僅一個實例),則提高的訪問速度將幫助EnumMap大大優于HashMap ,后者可能使用較少的堆內存,但必須在每個鍵上運行hashCode()和equals() 。
帶走
Enum和EnumMap是非常好的朋友。 每當您將類似枚舉的結構用作鍵時,請考慮實際上使這些結構成為枚舉并將其用作EnumMap鍵。
9.優化您的hashCode()和equals()方法
如果您不能使用EnumMap ,至少要優化您的hashCode()和equals()方法。 一個好的hashCode()方法是必不可少的,因為它將阻止對更昂貴的equals()進一步調用,因為它將為每個實例集生成更多不同的哈希存儲桶。
在每個類層次結構中,您可能都有流行和簡單的對象。 讓我們看一下jOOQ的org.jooq.Table實現。
hashCode()的最簡單,最快的實現是:
// AbstractTable, a common Table base implementation:@Override public int hashCode() {// [#1938] This is a much more efficient hashCode()// implementation compared to that of standard// QueryPartsreturn name.hashCode(); }…其中name只是表名。 我們甚至不考慮表的模式或任何其他屬性,因為表名通常在數據庫中足夠不同。 另外,該name是一個字符串,因此它內部已經有一個緩存的hashCode()值。
該注釋很重要,因為AbstractTable擴展了AbstractQueryPart ,它是任何AST(抽象語法樹)元素的通用基本實現。 通用AST元素沒有任何屬性,因此它不能做任何假設來優化hashCode()實現。 因此,重寫的方法如下所示:
// AbstractQueryPart, a common AST element // base implementation:@Override public int hashCode() {// This is a working default implementation. // It should be overridden by concrete subclasses,// to improve performancereturn create().renderInlined(this).hashCode(); }換句話說,必須觸發整個SQL呈現工作流以計算公共AST元素的哈希碼。
equals()事情變得更加有趣
// AbstractTable, a common Table base implementation:@Override public boolean equals(Object that) {if (this == that) {return true;}// [#2144] Non-equality can be decided early, // without executing the rather expensive// implementation of AbstractQueryPart.equals()if (that instanceof AbstractTable) {if (StringUtils.equals(name, (((AbstractTable<?>) that).name))) {return super.equals(that);}return false;}return false; }第一件事: 始終 (不僅在NOPE分支中 )提前中止每個equals()方法,如果:
- this == argument
- this "incompatible type" argument
請注意,如果您使用instanceof檢查兼容類型,則后一個條件包括argument == null 。 之前我們在“編碼Java的10個微妙的最佳實踐”中已經對此進行過博客撰寫。
現在,在明顯的情況下盡早中止比較之后,您可能還想在可以做出部分決策時就中止比較。 例如,jOOQ的Table.equals()的Table.equals()是要使兩個表相等,無論具體的實現類型如何,它們都必須具有相同的名稱。 例如,這兩個項目不可能相等:
- com.example.generated.Tables.MY_TABLE
- DSL.tableByName("MY_OTHER_TABLE")
如果argument 不能等于this ,并且如果我們可以輕松地進行檢查,那么我們可以這樣做,如果檢查失敗,則中止。 如果檢查成功,我們仍然可以從super進行更昂貴的實現。 鑒于Universe中的大多數對象不相等,我們將通過簡化此方法來節省大量CPU時間。
有些對象比其他對象更平等
在jOOQ的情況下,大多數實例實際上是由jOOQ源代碼生成器生成的表,該表的equals()實現甚至得到了進一步優化。 其他數十種表類型(派生表,表值函數,數組表,聯接表,數據透視表,公用表表達式等)可以保持其“簡單”實現。
10.集合思考,而不是個別思考
最后但并非最不重要的一點是,它與Java不相關,但適用于任何語言。 此外,我們將離開NOPE分支,因為此建議可能只是幫助您從O(N 3 )遷移到O(n log n)或類似的東西。
不幸的是,許多程序員認為是簡單的本地算法。 他們一步一步地解決問題,逐分支,逐循環,逐方法。 那就是命令式和/或函數式編程風格。 從純粹的命令式到面向對象(仍然是命令式)再到函數式編程時,為“更大的畫面”建模變得越來越容易,但是所有這些樣式都缺少只有SQL和R和類似語言才能具備的功能:
聲明式編程。
在SQL中( 并且我們很喜歡,因為它是jOOQ博客 ),您可以聲明要從數據庫中獲取的結果,而不會產生任何算法含義。 然后,數據庫可以考慮所有可用的元數據( 例如約束,鍵,索引等 ),以找出可能的最佳算法。
從理論上講,從一開始,這就是SQL和關系演算背后的主要思想。 實際上,SQL供應商僅在最近十年才實施了高效的CBO(基于成本的優化工具) ,因此請與我們保持在一起,直到2010年SQL最終釋放出其全部潛力(大約是時間!)。
但是,您不必執行SQL即可進行集合思考。 集合/收藏/袋子/清單可提供所有語言和庫。 使用集合的主要優點是您的算法將變得更加簡潔。 編寫起來非常容易:
SomeSet INTERSECT SomeOtherSet而不是:
// Pre-Java 8 Set result = new HashSet(); for (Object candidate : someSet)if (someOtherSet.contains(candidate))result.add(candidate);// Even Java 8 doesn't really help someSet.stream().filter(someOtherSet::contains).collect(Collectors.toSet());有人可能會認為函數式編程和Java 8將幫助您編寫更簡單,更簡潔的算法。 不一定是真的。 您可以將命令性的Java-7循環轉換為功能性的Java-8 Stream集合,但是您仍在編寫完全相同的算法。 編寫類似SQL的表達式是不同的。 這個…
SomeSet INTERSECT SomeOtherSet…可以由實施引擎以1000種方式實施。 正如我們今天所了解的那樣,在運行INTERSECT操作之前將兩個集合自動轉換為EnumSet也許是明智的。 也許我們可以并行化此INTERSECT而無需對Stream.parallel()進行低級調用。
結論
在本文中,我們討論了在NOPE分支上進行的優化,即深入到高復雜度算法中。 在我們的案例中,作為jOOQ開發人員,我們有興趣優化我們SQL生成:
- 每個查詢僅在單個StringBuilder上生成
- 我們的模板引擎實際上是解析字符,而不是使用正則表達式
- 我們會盡可能使用數組,尤其是在偵聽器上進行迭代時
- 我們避免了不必調用的JDBC方法
- 等等…
jOOQ處于“食物鏈的底部”,因為它是(次)API,在調用離開JVM進入DBMS之前,我們的客戶應用程序正在調用它。 位于食物鏈的底部意味著在jOOQ中執行的每一行代碼都可能被稱為N x O x P倍,因此我們必須熱切地進行優化。
您的業??務邏輯不在NOPE分支中 。 但是您自己的本地基礎結構邏輯可能是(自定義SQL框架,自定義庫等),應該根據我們今天所看到的規則進行審查。 例如,使用Java Mission Control或任何其他探查器。
翻譯自: https://www.javacodegeeks.com/2015/02/top-10-easy-performance-optimisations-java.html
java 性能 優化
總結
以上是生活随笔為你收集整理的java 性能 优化_Java十大简单性能优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 苹果备忘录删除的内容如何恢复
- 下一篇: java 编程工具_Java开发工具可以