<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    from:http://www.jianshu.com/p/2750c7c202ef

    上周有幸給部門的小伙伴分享了一些JVM相關的知識,在整個做PPT的過程中,也是對一個領域的碎片知識的整理,本文將針對虛擬機GC相關的一些內容進行整理,本文不會涉及到G1收集器。

    在Hotspot VM實現中,主要有兩大類GC

    1. Partial GC:并不會堆整個GC堆進行收集
      • young gc:只收集 young gen 的GC
      • old gc:只收集 old gen 的GC,只有CMS的 concurrent collection
      • mixed GC:收集整個 young gen 以及部分 old gen 的GC,只有G1
    2. Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等

    其實在各種文章或書上還可以看到Minor GC、Major GC的字眼,其中minor GC和young gc對應,而Major GC通常是和Full GC是等價的,由于HotSpot VM發展了這么多年,外界對各種名詞的解讀已經完全混亂了,所以Major GC有時也可能是指old gc,在下定論之前一定要先問清楚。

    單線程、并行、并發

    在GC收集器實現中,分為了單線程、并行和并發。
    單線程收集器:如 Serial GC,這個比較好理解,即垃圾收集過程中只有單一線程在進行收集工作,實現也最簡單。

    并行收集器:如Parallel GC,每次運行時,不管是YGC,還是FGC,會 stop-the-world,暫停所有的用戶線程,并采用多個線程同時進行垃圾收集。

    并發收集器:如CMS GC,在新生代進行垃圾收集時和并行收集器類似,都是并行收集(當然具體算法中,你也可以設置成采用單線程進行收集),而且都會stop-the-world,主要的區別在于老年代的收集上,CMS在老年代進行垃圾收集時,大部分時間可以和用戶線程并發執行的,只有小部分的時間stop-the-world,這就是它的優勢,可以大大降低應用的暫停時間,當然也是有劣勢的。

    算法組合

    Hotspot VM實現的幾種GC算法組合中,其中CMS GC使用最廣,因為現在都是大內存時代。

    1、Serial GC

    Serial generational collector (-XX:+UseSerialGC)
    是全局范圍的Full GC,這種算法組合是最早出現的,當年的Java堆內存大小都還不大,使用Serial GC進行單線程收集,還感覺不出來GC耗時導致應用暫停的問題

    2、Parallel GC

    Parallel for young space, serial for old space generational collector (-XX:+UseParallelGC).
    Parallel for young and old space generational collector (-XX:+UseParallelOldGC)
    當Java堆慢慢變大時,發現已經無法忍受GC耗時帶來的應用暫停了,出現了Parallel GC,采用多線程的方式進行垃圾收集,很明顯可以提升垃圾收集效率。

    3、CMS GC

    Concurrent mark sweep with serial young space collector (-XX:+UseConcMarkSweepGC
    –XX:-UseParNewGC)
    Concurrent mark sweep with parallel young space collector (-XX:+UseConcMarkSweepGC)
    當Java堆達到更大時,比如8G,使用Parallel GC帶來的應用暫停已經很明顯了,所有又出現了 CMS GC,這是目前我看到線上環境使用的比較多的GC策略,在參數中添加-XX:+UseConcMarkSweepGC,對于 young gen,會自動選用 ParNewGC,不需要額外添加 -XX:+UseParNewGC

    CMS雖然好,因為它的特殊算法,大部分的收集過程可以和用戶線程并發執行,大大降低應用的暫停時間,不過也會帶來負面影響,在收集完 old gen 之后,CMS并不會做整理過程,會產生空間碎片,如果這些碎片空間得不到利用,就會造成空間的浪費,整個過程中可能發生 concurrent mode failure,導致一次真正意義的 full gc,采用單線程對整個堆(young+old+perm) 使用MSC(Mark-Sweep-Compact)進行收集,這個過程意味著很慢很慢很慢,而且這個碎片問題是無法預測的.

    4、G1 GC

    G1 garbage collector (-XX:+UseG1GC),本文不對G1進行介紹

    觸發條件

    young gc

    對于 young gc,觸發條件似乎要簡單很多,當 eden 區的內存不夠時,就會觸發young gc,我們看看在 eden 區給對象分配一塊內存是怎樣一個過程,畫了一個簡單的流程圖,我一直覺得一個好的示意圖可以讓一個枯燥的過程變得更有意思。

    在 eden 區分配空間內存不足時有兩種情況,為對象分配內存、為TLAB分配內存,總之就是內存不夠,需要進行一次 young gc 為eden區騰出空間為后續的內存申請做準備,然后由一個用戶線程通知VM Thread,接下去要執行一次 young gc。

    full gc

    1、old gen 空間不足

    當創建一個大對象、大數組時,eden 區不足以分配這么大的空間,會嘗試在old gen 中分配,如果這時 old gen 空間也不足時,會觸發 full gc,為了避免上述導致的 full gc,調優時應盡量讓對象在 young gc 時就能夠被回收,還有不要創建過大的對象和數組。

    2、統計得到的 young gc 晉升到 old gen的對象平均總大小大于old gen 的剩余空間

    當準備觸發一次 young gc時,會判斷這次 young gc 是否安全,這里所謂的安全是當前老年代的剩余空間可以容納之前 young gc 晉升對象的平均大小,或者可以容納 young gen 的全部對象,如果結果是不安全的,就不會執行這次 young gc,轉而執行一次 full gc

    3、perm gen 空間不足

    如果有perm gen的話,當系統中要加載的類、反射的類和調用的方法較多,而且perm gen沒有足夠空間時,也會觸發一次 full gc

    4、ygc出現 promotion failure

    promotion failure 發生在 young gc 階段,即 cms 的 ParNewGC,當對象的gc年齡達到閾值時,或者 eden 的 to 區放不下時,會把該對象復制到 old gen,如果 old gen 空間不足時,會發生 promotion failure,并接下去觸發full gc

    在GC日志中,有時會看到 concurrent mode failure 關鍵字,這是因為什么原因導致的問題呢? 對這一塊的理解,很多文章都是說因為 concurrent mode failure 導致觸發full gc,其實應該反過來,是full gc 導致的 concurrent mode failure,在cms gc的算法實現中,通常說的cms是由一個后臺線程定時觸發的,默認每2秒檢查一次old gen的內存使用率,當 old gen 的內存使用率達到-XX:CMSInitiatingOccupancyFraction設置的值時,會觸發一次 cms gc,對 old gen 進行并發收集,而真正的 full gc 是通過 vm thread線程觸發的,而且在判斷當前ygc會失敗的情況下觸發full gc,如上一次ygc出現了promotion failure,如果執行 full gc 時,發現后臺線程正在執行 cms gc,就會導致 concurrent mode failure。

    對于以上這些情況,CMSInitiatingOccupancyFraction參數的設置就顯得尤為重要,設置的太大的話,發生CMS時的剩余空間太小,在ygc的時候容易發生promotion failure,導致 concurrent mode failure 發生的概率就增大,如果設置太小的話,會導致 cms gc 的頻率會增加,所以需要根據應用的需求對該參數進行調優。

    5、執行 System.gc()jmap -histo:live <pid>jmap -dump ...

    參考資料
    Major GC和Full GC的區別是什么?觸發條件呢

    個人公眾號

    posted @ 2017-07-27 14:33 小馬歌 閱讀(344) | 評論 (0)編輯 收藏
     

    from:http://www.cnblogs.com/foohack/p/5627163.html

    Cassandra note:

    依賴:需要java 8 (http://www.oracle.com/technetwork/java/javase/downloads/index.html)

    數據模型: 與Hbase同樣是屬于列式數據庫,Key-Value存儲系統。(http://www.ibm.com/developerworks/cn/opensource/os-cn-cassandra/)
    http://www.datastax.com/dev/blog/basic-rules-of-cassandra-data-modeling

    集群中的數據是靠partion key的hash code均勻映射到不同的節點上去的。partionkey是primary key的第一個元素,所以選一個好的主
    key才能使數據更好的均勻存儲在不同的節點上。


    Cassandra的節點實例叫Cluster,里面可以包含一個或多個鍵空間(KeysSpace).鍵空間是存放列族(Column Family)的容器,相當于
    關系數據中的database,schema。列族是存放列(column)的容器,類似與關系數據庫中的table。超級列(Super column)是一種
    特殊的列,它的value值可以包含多個column。 columns是cassandra的最基本單位,有name,value,timestamp構成。

     

    列式數據庫的優點: 
    1.適合存儲大量數據,而不是小量數據。因為數據是是基于列存儲的,所以可以忽略不需要的列的數據,提高查找效率。
    與之對應的是行數據庫。
    2.高壓縮比。節省存儲空間,也節省CPU和內存。
    3.高裝載速度。

    列式數據庫的缺點:
    1.不適合掃描小量數據。
    2.不適合隨機更新數據。
    3.不適合做含有刪除的更新的實時操作。


    查詢數據:
    Cassandra有自己的一套查詢語言CQL(類似SQL),在數據訪問方式上亦是如此。客戶端可以與集群中的任意節點相連,并訪問任意的數據。

    cassandra在寫入數據之前需要記錄日志(CommitLog),然后數據開始寫入到 Column Family 對應的 Memtable 中,
    Memtable 是一種按照 key 排序數據的內存結構,在滿足一定條件時,再把 Memtable 的數據批量的刷新到磁盤上,存儲為 SSTable 。

    存儲二進制大文件(不推薦存儲):http://wiki.apache.org/cassandra/FAQ#large_file_and_blob_storage

    Cassandra的GUI管理工具整理:http://wiki.apache.org/cassandra/FAQ#gui 也有自帶的CLI工具連接Cassandra

    Cassandra集群種子的概念(很重要):http://wiki.apache.org/cassandra/FAQ#seed
    類似與Cassandra集群的初始化節點(集線器),各個節點通過種子節點互相學習(交換)各自的數據(狀態),所以新加入的Cassandra節點都需要給它
    指定種子節點,下次啟動的時候就不需要了。


    Cassandra 的C++ 接口:
    Cassandra的各種編程語言的接口是有Thrift這個開源工具生成的,語言無關的Thrift輸入文件(cassandra.thrift)Cassandra已經自帶
    ,安裝thrift運行 thrift.exe -gen cpp cassandra.thrift生成就可以了。 cpp的接口依賴thrift的核心庫叫libthrift,libthrift依賴boost1.53.0
    版本和openssl


    Cassandra windows 安裝配置:
    解壓,配置好CASSANDRA_HOME環境變量的路徑(也就是你解壓的cassandra根目錄),然后運行bin下的cassandra.bat,如果發現logs
    目錄底下的system.log文件中有INFO - Starting up server gossip,那么恭喜你,Cassandra已經在你的本機啟動起來了。

     

     

    *****************************************Cassandra的基本操作************************************************

    數據模型:多維的hash表,每行可以有不同的列。每行都有個鍵. keyspace包含若干列族
    (列族和表是同一個概念:http://stackoverflow.com/questions/18824390/whats-the-difference-between-creating-a-table-and-creating-a-columnfamily-in-ca),
    keyspace在邏輯上是容納列族和某些配置屬性的命名空間。列族定義了相關的數據名字和它們的排序方式。

    入門必讀:http://wiki.apache.org/cassandra/GettingStarted

    CQLSH中運行外部創建的cql腳本文件: SOURCE '[file_path]'

    鍵空間的創建: 
    CREATE KEYSPACE [keyspace_name] WITH REPLICATION = {'class' : 'NetworkTopologyStrategy', 'datacenter1' : 3};

    ----鍵空間的創建要附帶副本屬性,class可以指定NetworkTopologyStrategy或SimpleStrategy。SimpleStrategy只用于測試評估Cassandra
    生產環境使用NetworkTopologyStrategy. 鍵空間類似關系型數據庫中的數據庫(database)

    鍵空間的修改:
    ALTER KEYSPACE [keyspace_name] WITH REPLICATION = {};

    鍵空間的刪除:
    DROP KEYSPACE [keyspace_name]

    鍵空間的使用:
    USE [keyspace_name]


    列出已存在的鍵空間:
    DESCRIBE keyspaces;

    列出某個鍵空間下的所有表:
    USE [keyspace_name];
    DESCRIBE tables;

    列出某個鍵空間下的所有列族:
    USE [keyspace_name];
    DESCRIBE columnfamilies;

    列出特定的表的基本信息:
    DESCRIBE TABLE [keyspace_name].[table_name];

    建表:
    http://docs.datastax.com/en/cql/3.1/cql/cql_using/create_table_t.html
    表的主鍵可以是復合的,就是多個列組成一個主鍵:
    CREATE TABLE emp (
    empID int,
    deptID int,
    first_name varchar,
    last_name varchar,
    PRIMARY KEY (empID, deptID)); //主鍵的第一個鍵就是分區鍵(empID),分區鍵的目的就是把表中的數據均分到集群中的
    各個節點中

    更改表:
    ALERT TABLE [table_name] [some change]
    https://docs.datastax.com/en/cql/3.0/cql/cql_reference/alter_table_r.html
    沒辦法更改主鍵,因為主鍵涉及到數據的物理儲存

    給表的某列建立索引:
    CREATE INDEX ON [table_name] (column_name);


    查表:
    SELCT [column_name] FROM [keyspace].[table_name] WHERE [column_name] = [value] //其中column_name必須是主鍵的其中部分,
    如果有多個條件必須其中有一個是分區鍵

    更新表中的值:
    UPDATE [keyspace].[table_name] SET [column_name] = [new_value] WHERE [column_name]=[value]

    刪除表中列或行:
    刪除列中值:DELETE [column_name] FROM [table_name] WHERE [column_name] = [value] # 同查表
    刪除一整行:DELETE FROM [table_name] WHERE [column_name] = [value] #同上

    自定義數據類型(http://docs.datastax.com/en/cql/3.1/cql/cql_using/cqlUseUDT.html):
    CREATE TYPE [keyspace_name].[type_name] (
    street text,
    city text,
    zip_code int,
    phones set<text>
    );
    自定義的數據類型的字面值是json-style的風格。

    內建的數據類型(http://docs.datastax.com/en/cql/3.1/cql/cql_reference/cql_data_types_c.html):
    ascii,bigint,blob,boolean,counter,double,float,inet,int,list,map,set,text,uuid,
    timestamp,tuple,varchar(UTF-8 encoded string) ,varint


    查看集群信息:
    SELECT * FROM system.peers;

    本地幫助文檔的查看:
    HELP [COMMAND]
    比如:查看創建鍵空間 HELP CREATE_KEYSPACE;


    CQL語句支持多語句提交(Batch):
    可以減少Node之間的流量交換,類似于事務,是原子的。
    http://docs.datastax.com/en/cql/3.1/cql/cql_reference/batch_r.html#reference_ds_djf_xdd_xj__batch-conditional
    http://docs.datastax.com/en/cql/3.1/cql/cql_using/use-batch-static.html


    給數據設置存活期:
    超過存活期的數據,將被銷毀。
    INSERT INTO [table_name]
    ([column_name1], [column_name2])
    VALUES ([column_value1], [column_value2]) USING TTL 86400; # 86400 sec 大概是一天的存活期
    是給column_name2設置的


    UPDATE [table_name] USING TTL 432000 SET [column_name] = [column_value]
    WHERE user_name = 'cbrown';

    posted @ 2017-07-18 09:57 小馬歌 閱讀(443) | 評論 (0)編輯 收藏
     
    from:http://blog.csdn.net/MoreWindows/article/category/859207 

    【白話經典算法系列之十七】 數組中只出現一次的數

    數組A中,除了某一個數字x之外,其他數字都出現了三次,而x出現了一次。請給出最快的方法找到x。 這個題目非常有意思,在本人博客中有《位操作基礎篇之位操作全面總結》這篇文章介紹了使用位操作的異或來解決——數組中其他數字出現二次,而x出現一次,找出x。有《【白話經典算法系列之十二】數組中只出現1次的兩個數字(百度面試題)》這邊文章介紹了分組異或的方法來解決——數組中其他數字出現二次,而x和y出現一次,找出x和y。而這個題目則是其他數字出現3次,x出現一次。...
    2013-10-21 11:49 閱讀(32100) 評論(34)
    首先看看題目要求: 給定一個無序的整數數組,怎么找到第一個大于0,并且不在此數組的整數。比如[1,2,0]返回3,[3,4,-1,1]返回2,[1, 5, 3, 4, 2]返回6,[100, 3, 2, 1, 6,8, 5]返回4。要求使用O(1)空間和O(n)時間。 這道題目初看沒有太好的思路,但是借鑒下《白話經典算法系列之十一道有趣的GOOGLE面試題》這篇文章,我們不發現使用“基數排序”正好可以用來解決這道題目...
    2013-10-15 10:17 閱讀(13580) 評論(11)
    【白話經典算法系列之十五】“一步千里”之數組找數 有這樣一個數組A,大小為n,相鄰元素差的絕對值都是1。如:A={4,5,6,5,6,7,8,9,10,9}。現在,給定A和目標整數t,請找到t在A中的位置。除了依次遍歷,還有更好的方法么?...
    2013-09-02 12:57 閱讀(25918) 評論(39)
    【白話經典算法系列之十三】隨機生成和為S的N個正整數——投影法      隨機生成和為S的N個正整數有很多種解法。下面講解一種比較高效且比較有趣味性的解法——投影法。    以生成和為20的4個數為例,可以先生成隨機生成0到20之間的三個數字再排序,假設得到了4,7,18。然后在X-Y數軸上畫出這三個數,如下圖:然后將這些數值投影到Y軸上,可得下圖:由圖很容易看出AB,BC,CD,DE這四段的長度...
    2013-01-04 13:46 閱讀(15710) 評論(46)
    微博http://weibo.com/MoreWindows已開通,歡迎關注。本系列文章地址:http://blog.csdn.net/MoreWindows/article/category/859207首先來看題目要求:在一個數組中除兩個數字只出現1次外,其它數字都出現了2次, 要求盡快找出這兩個數字。    考慮下這個題目的簡化版——數組中除一個數字只出現1次外,其它數字都成對出現,要求盡快...
    2012-11-27 09:17 閱讀(35498) 評論(51)
    微博http://weibo.com/MoreWindows已開通,歡迎關注。本系列文章地址:http://blog.csdn.net/MoreWindows/article/category/859207 上一篇《白話經典算法系列之十一道有趣的GOOGLE面試題》中對一道有趣的GOOGLE面試題進行了詳細的講解,使用了類似于基數排序的做法在O(N)的時間復雜度和O(1)的空間復雜度完成了題目的要...
    2012-11-23 07:57 閱讀(24806) 評論(52)
    微博http://weibo.com/MoreWindows已開通,歡迎關注。最近在微博上看到一道有趣的GOOGLE面試題,見下圖:文字版:一個大小為n的數組,里面的數都屬于范圍[0, n-1],有不確定的重復元素,找到至少一個重復元素,要求O(1)空間和O(n)時間。     這個題目要求用O(n)的時間復雜度,這意味著只能遍歷數組一次。同時還要尋找重復元素,很容易想到建立哈希表來完成,遍歷數組...
    2012-11-21 09:03 閱讀(47907) 評論(87)
    首先來看看原題 微軟2010年筆試題在一個排列中,如果一對數的前后位置與大小順序相反,即前面的數大于后面的數,那么它們就稱為一個逆序數對。一個排列中逆序的總數就稱為這個排列的逆序數。如{2,4,3,1}中,2和1,4和3,4和1,3和1是逆序數對,因此整個數組的逆序數對個數為4,現在給定一數組,要求統計出該數組的逆序數對個數。 計算數列的逆序數對個數最簡單的方便就最從前向后依次統計每個數字與它后面...
    2012-10-15 09:15 閱讀(30367) 評論(36)
    在我的博客對冒泡排序,直接插入排序,直接選擇排序,希爾排序,歸并排序,快速排序和堆排序這七種常用的排序方法進行了詳細的講解,并做成了電子書以供大家下載。下載地址為:http://download.csdn.net/detail/morewindows/4443208。       有網友提議到這本《MoreWindows白話經典算法之七大排序》電子書講解細致用來平時學習是非常好的,但是頁數有22頁...
    2012-09-10 10:08 閱讀(42997) 評論(26)
    堆排序與快速排序,歸并排序一樣都是時間復雜度為O(N*logN)的幾種常見排序方法。學習堆排序前,先講解下什么是數據結構中的二叉堆。二叉堆的定義二叉堆是完全二叉樹或者是近似完全二叉樹。二叉堆滿足二個特性:1.父結點的鍵值總是大于或等于(小于或等于)任何一個子節點的鍵值。2.每個結點的左子樹和右子樹都是一個二叉堆(都是最大堆或最小堆)。當父結點的鍵值總是大于或等于任何一個子節點的鍵值時為最大堆。當父...
    2011-08-22 20:04 閱讀(338481) 評論(188)
    快速排序由于排序效率在同為O(N*logN)的幾種排序方法中效率較高,因此經常被采用,再加上快速排序思想----分治法也確實實用,因此很多軟件公司的筆試面試,包括像騰訊,微軟等知名IT公司都喜歡考這個,還有大大小的程序方面的考試如軟考,考研中也常常出現快速排序的身影。總的說來,要直接默寫出快速排序還是有一定難度的,因為本人就自己的理解對快速排序作了下白話解釋,希望對大家理解有幫助,達到快速排序,快...
    2011-08-13 17:19 閱讀(418202) 評論(284)
    歸并排序是建立在歸并操作上的一種有效的排序算法。該算法是采用分治法(Divide and Conquer)的一個非常典型的應用。首先考慮下如何將將二個有序數列合并。這個非常簡單,只要從比較二個數列的第一個數,誰小就先取誰,取了后就在對應數列中刪除這個數。然后再進行比較,如果有數列為空,那直接將另一個數列的數據依次取出即可。//將有序數組a[]和b[]合并到c[]中 void MemeryArra...
    2011-08-11 11:01 閱讀(275147) 評論(154)
    直接選擇排序和直接插入排序類似,都將數據分為有序區和無序區,所不同的是直接播放排序是將無序區的第一個元素直接插入到有序區以形成一個更大的有序區,而直接選擇排序是從無序區選一個最小的元素直接放到有序區的最后。   設數組為a[0…n-1]。 1.      初始時,數組全為無...
    2011-08-09 11:15 閱讀(29055) 評論(38)
    希爾排序的實質就是分組插入排序,該方法又稱縮小增量排序,因DL.Shell于1959年提出而得名。   該方法的基本思想是:先將整個待排元素序列分割成若干個子序列(由相隔某個“增量”的元素組成的)分別進行直接插入排序,然后依次縮減增量再進行排序,待整個序列中的元素基本有序(增...
    2011-08-08 11:41 閱讀(148913) 評論(82)
    直接插入排序(Insertion Sort)的基本思想是:每次將一個待排序的記錄,按其關鍵字大小插入到前面已經排好序的子序列中的適當位置,直到全部記錄插入完成為止。   設數組為a[0…n-1]。 1.      初始時,a[0]自成1個有序區,無序區為a[1..n-1]。...
    2011-08-06 19:27 閱讀(118661) 評論(81)
    冒泡排序是非常容易理解和實現,,以從小到大排序舉例: 設數組長度為N。 1.比較相鄰的前后二個數據,如果前面數據大于后面的數據,就將二個數據交換。 2.這樣對數組的第0個數據到N-1個數據進行一次遍歷后,最大的一個數據就“沉”到數組第N-1個位置。 3.N=N-1,如果N...
    2011-08-06 19:20 閱讀(166505) 評論(94)
    posted @ 2017-06-23 11:17 小馬歌 閱讀(329) | 評論 (0)編輯 收藏
     
    from:http://blog.csdn.net/liaodehong/article/details/51605457

    本文將深入淺出地講解 JIT 編譯器在 JVM 中的運作原理,使讀者能夠更好的理解 Java 底層機制并且為讀者在 Java 性能優化領域打開更廣的視野。

    JIT 簡介

    JIT 是 just in time 的縮寫, 也就是即時編譯編譯器。使用即時編譯器技術,能夠加速 Java 程序的執行速度。下面,就對該編譯器技術做個簡單的講解。

    首先,我們大家都知道,通常通過 javac 將程序源代碼編譯,轉換成 java 字節碼,JVM 通過解釋字節碼將其翻譯成對應的機器指令,逐條讀入,逐條解釋翻譯。很顯然,經過解釋執行,其執行速度必然會比可執行的二進制字節碼程序慢很多。為了提高執行速度,引入了 JIT 技術。

    在運行時 JIT 會把翻譯過的機器碼保存起來,以備下次使用,因此從理論上來說,采用該 JIT 技術可以接近以前純編譯技術。下面我們看看,JIT 的工作過程。

    JIT 編譯過程

    當 JIT 編譯啟用時(默認是啟用的),JVM 讀入.class 文件解釋后,將其發給 JIT 編譯器。JIT 編譯器將字節碼編譯成本機機器代碼,下圖展示了該過程。

    圖 1. JIT 工作原理圖
    圖 1. JIT 工作原理圖

    回頁首

    Hot Spot 編譯

    當 JVM 執行代碼時,它并不立即開始編譯代碼。這主要有兩個原因:

    首先,如果這段代碼本身在將來只會被執行一次,那么從本質上看,編譯就是在浪費精力。因為將代碼翻譯成 java 字節碼相對于編譯這段代碼并執行代碼來說,要快很多。

    當然,如果一段代碼頻繁的調用方法,或是一個循環,也就是這段代碼被多次執行,那么編譯就非常值得了。因此,編譯器具有的這種權衡能力會首先執行解釋后的代碼,然后再去分辨哪些方法會被頻繁調用來保證其本身的編譯。其實說簡單點,就是 JIT 在起作用,我們知道,對于 Java 代碼,剛開始都是被編譯器編譯成字節碼文件,然后字節碼文件會被交由 JVM 解釋執行,所以可以說 Java 本身是一種半編譯半解釋執行的語言。Hot Spot VM 采用了 JIT compile 技術,將運行頻率很高的字節碼直接編譯為機器指令執行以提高性能,所以當字節碼被 JIT 編譯為機器碼的時候,要說它是編譯執行的也可以。也就是說,運行時,部分代碼可能由 JIT 翻譯為目標機器指令(以 method 為翻譯單位,還會保存起來,第二次執行就不用翻譯了)直接執行。

    第二個原因是最優化,當 JVM 執行某一方法或遍歷循環的次數越多,就會更加了解代碼結構,那么 JVM 在編譯代碼的時候就做出相應的優化。

    我們將在后面講解這些優化策略,這里,先舉一個簡單的例子:我們知道 equals() 這個方法存在于每一個 Java Object 中(因為是從 Object class 繼承而來)而且經常被覆寫。當解釋器遇到 b = obj1.equals(obj2) 這樣一句代碼,它則會查詢 obj1 的類型從而得知到底運行哪一個 equals() 方法。而這個動態查詢的過程從某種程度上說是很耗時的。

    寄存器和主存

    其中一個最重要的優化策略是編譯器可以決定何時從主存取值,何時向寄存器存值。考慮下面這段代碼:

    清單 1. 主存 or 寄存器測試代碼
    public class RegisterTest {  private int sum;   public void calculateSum(int n) {  for (int i = 0; i < n; ++i) {  sum += i;  }  } }

    在某些時刻,sum 變量居于主存之中,但是從主存中檢索值是開銷很大的操作,需要多次循環才可以完成操作。正如上面的例子,如果循環的每一次都是從主存取值,性能是非常低的。相反,編譯器加載一個寄存器給 sum 并賦予其初始值,利用寄存器里的值來執行循環,并將最終的結果從寄存器返回給主存。這樣的優化策略則是非常高效的。但是線程的同步對于這種操作來說是至關重要的,因為一個線程無法得知另一個線程所使用的寄存器里變量的值,線程同步可以很好的解決這一問題,有關于線程同步的知識,我們將在后續文章中進行講解。

    寄存器的使用是編譯器的一個非常普遍的優化。

    回到之前的例子,JVM 注意到每次運行代碼時,obj1 都是 java.lang.String 這種類型,那么 JVM 生成的被編譯后的代碼則是直接調用 String.equals() 方法。這樣代碼的執行將變得非常快,因為不僅它是被編譯過的,而且它會跳過查找該調用哪個方法的步驟。

    當然過程并不是上面所述這樣簡單,如果下次執行代碼時,obj1 不再是 String 類型了,JVM 將不得不再生成新的字節碼。盡管如此,之后執行的過程中,還是會變的更快,因為同樣會跳過查找該調用哪個方法的步驟。這種優化只會在代碼被運行和觀察一段時間之后發生。這也就是為什么 JIT 編譯器不會理解編譯代碼而是選擇等待然后再去編譯某些代碼片段的第二個原因。

    回頁首

    初級調優:客戶模式或服務器模式

    JIT 編譯器在運行程序時有兩種編譯模式可以選擇,并且其會在運行時決定使用哪一種以達到最優性能。這兩種編譯模式的命名源自于命令行參數(eg: -client 或者 -server)。JVM Server 模式與 client 模式啟動,最主要的差別在于:-server 模式啟動時,速度較慢,但是一旦運行起來后,性能將會有很大的提升。原因是:當虛擬機運行在-client 模式的時候,使用的是一個代號為 C1 的輕量級編譯器,而-server 模式啟動的虛擬機采用相對重量級代號為 C2 的編譯器。C2 比 C1 編譯器編譯的相對徹底,服務起來之后,性能更高。

    通過 java -version 命令行可以直接查看當前系統使用的是 client 還是 server 模式。例如:

    圖 2. 查看編譯模式
    圖 2. 查看編譯模式

    回頁首

    中級編譯器調優

    大多數情況下,優化編譯器其實只是選擇合適的 JVM 以及為目標主機選擇合適的編譯器(-cient,-server 或是-xx:+TieredCompilation)。多層編譯經常是長時運行應用程序的最佳選擇,短暫應用程序則選擇毫秒級性能的 client 編譯器。

    優化代碼緩存

    當 JVM 編譯代碼時,它會將匯編指令集保存在代碼緩存。代碼緩存具有固定的大小,并且一旦它被填滿,JVM 則不能再編譯更多的代碼。

    我們可以很容易地看到如果代碼緩存很小所具有的潛在問題。有些熱點代碼將會被編譯,而其他的則不會被編譯,這個應用程序將會以運行大量的解釋代碼來結束。

    這是當使用 client 編譯器模式或分層編譯時很頻繁的一個問題。當使用普通 server 編譯器模式時,編譯合格的類的數量將被填入代碼緩存,通常只有少量的類會被編譯。但是當使用 client 編譯器模式時,編譯合格的類的數量將會高很多。

    在 Java 7 版本,分層編譯默認的代碼緩存大小經常是不夠的,需要經常提高代碼緩存大小。大型項目若使用 client 編譯器模式,則也需要提高代碼緩存大小。

    現在并沒有一個好的機制可以確定一個特定的應用到底需要多大的代碼緩存。因此,當需要提高代碼緩存時,這將是一種湊巧的操作,一個通常的做法是將代碼緩存變成默認大小的兩倍或四倍。

    可以通過 –XX:ReservedCodeCacheSize=Nflag(N 就是之前提到的默認大小)來最大化代碼緩存大小。代碼緩存的管理類似于 JVM 中的內存管理:有一個初始大小(用-XX:InitialCodeCacheSize=N 來聲明)。代碼緩存的大小從初始大小開始,隨著緩存被填滿而逐漸擴大。代碼緩存的初始大小是基于芯片架構(例如 Intel 系列機器,client 編譯器模式下代碼緩存大小起始于 160KB,server 編譯器模式下代碼緩存大小則起始于 2496KB)以及使用的編譯器的。重定義代碼緩存的大小并不會真正影響性能,所以設置 ReservedCodeCacheSize 的大小一般是必要的。

    再者,如果 JVM 是 32 位的,那么運行過程大小不能超過 4GB。這包括了 Java 堆,JVM 自身所有的代碼空間(包括其本身的庫和線程棧),應用程序分配的任何的本地內存,當然還有代碼緩存。

    所以說代碼緩存并不是無限的,很多時候需要為大型應用程序來調優(或者甚至是使用分層編譯的中型應用程序)。比如 64 位機器,為代碼緩存設置一個很大的值并不會對應用程序本身造成影響,應用程序并不會內存溢出,這些額外的內存預定一般都是被操作系統所接受的。

    編譯閾值

    在 JVM 中,編譯是基于兩個計數器的:一個是方法被調用的次數,另一個是方法中循環被回彈執行的次數。回彈可以有效的被認為是循環被執行完成的次數,不僅因為它是循環的結尾,也可能是因為它執行到了一個分支語句,例如 continue。

    當 JVM 執行一個 Java 方法,它會檢查這兩個計數器的總和以決定這個方法是否有資格被編譯。如果有,則這個方法將排隊等待編譯。這種編譯形式并沒有一個官方的名字,但是一般被叫做標準編譯。

    但是如果方法里有一個很長的循環或者是一個永遠都不會退出并提供了所有邏輯的程序會怎么樣呢?這種情況下,JVM 需要編譯循環而并不等待方法被調用。所以每執行完一次循環,分支計數器都會自增和自檢。如果分支計數器計數超出其自身閾值,那么這個循環(并不是整個方法)將具有被編譯資格。

    這種編譯叫做棧上替換(OSR),因為即使循環被編譯了,這也是不夠的:JVM 必須有能力當循環正在運行時,開始執行此循環已被編譯的版本。換句話說,當循環的代碼被編譯完成,若 JVM 替換了代碼(前棧),那么循環的下個迭代執行最新的被編譯版本則會更加快。

    標準編譯是被-XX:CompileThreshold=Nflag 的值所觸發。Client 編譯器模式下,N 默認的值 1500,而 Server 編譯器模式下,N 默認的值則是 10000。改變 CompileThreshold 標志的值將會使編譯器相對正常情況下提前(或推遲)編譯代碼。在性能領域,改變 CompileThreshold 標志是很被推薦且流行的方法。事實上,您可能知道 Java 基準經常使用此標志(比如:對于很多 server 編譯器來說,經常在經過 8000 次迭代后改變次標志)。

    我們已經知道 client 編譯器和 server 編譯器在最終的性能上有很大的差別,很大程度上是因為編譯器在編譯一個特定的方法時,對于兩種編譯器可用的信息并不一樣。降低編譯閾值,尤其是對于 server 編譯器,承擔著不能使應用程序運行達到最佳性能的風險,但是經過測試應用程序我們也發現,將閾值從 8000 變成 10000,其實有著非常小的區別和影響。

    檢查編譯過程

    中級優化的最后一點其實并不是優化本身,而是它們并不能提高應用程序的性能。它們是 JVM(以及其他工具)的各個標志,并可以給出編譯工作的可見性。它們中最重要的就是--XX:+PrintCompilation(默認狀態下是 false)。

    如果 PrintCompilation 被啟用,每次一個方法(或循環)被編譯,JVM 都會打印出剛剛編譯過的相關信息。不同的 Java 版本輸出形式不一樣,我們這里所說的是基于 Java 7 版本的。

    編譯日志中大部分的行信息都是下面的形式:

    清單 2. 日志形式
    timestamp compilation_id attributes (tiered_level) method_name size depot

    這里 timestamp 是編譯完成時的時間戳,compilation_id 是一個內部的任務 ID,且通常情況下這個數字是單調遞增的,但有時候對于 server 編譯器(或任何增加編譯閾值的時候),您可能會看到失序的編譯 ID。這表明編譯線程之間有些快有些慢,但請不要隨意推斷認為是某個編譯器任務莫名其妙的非常慢。

    用 jstat 命令檢查編譯

    要想看到編譯日志,則需要程序以-XX:+PrintCompilation flag 啟動。如果程序啟動時沒有 flag,您可以通過 jstat 命令得到有限的可見性信息。

    Jstat 有兩個選項可以提供編譯器信息。其中,-compile 選項提供總共有多少方法被編譯的總結信息(下面 6006 是要被檢查的程序的進程 ID):

    清單 3 進程詳情
    % jstat -compiler 6006 CompiledFailedInvalid TimeFailedTypeFailedMethod 206 0 0 1.97 0

    注意,這里也列出了編譯失敗的方法的個數信息,以及編譯失敗的最后一個方法的名稱。

    另一種選擇,您可以使用-printcompilation 選項得到最后一個被編譯的方法的編譯信息。因為 jstat 命令有一個參數選項用來重復其操作,您可以觀察每一次方法被編譯的情況。舉個例子:

    Jstat 對 6006 號 ID 進程每 1000 毫秒執行一次: %jstat –printcompilation 6006 1000,具體的輸出信息在此不再描述。

    回頁首

    高級編譯器調優

    這一節我們將介紹編譯工作剩下的細節,并且過程中我們會探討一些額外的調優策略。調優的存在很大程度上幫助了 JVM 工程師診斷 JVM 自身的行為。如果您對編譯器的工作原理很感興趣,這一節您一定會喜歡。

    編譯線程

    從前文中我們知道,當一個方法(或循環)擁有編譯資格時,它就會排隊并等待編譯。這個隊列是由一個或很多個后臺線程組成。這也就是說編譯是一個異步的過程。它允許程序在代碼正在編譯時被繼續執行。如果一個方法被標準編譯方式所編譯,那么下一個方法調用則會執行已編譯的方法。如果一個循環被棧上替換方式所編譯,那么下一次循環迭代則會執行新編譯的代碼。

    這些隊列并不會嚴格的遵守先進先出原則:哪一個方法的調用計數器計數更高,哪一個就擁有優先權。所以即使當一個程序開始執行,并且有大量的代碼需要編譯,這個優先權順序將幫助并保證最重要的代碼被優先編譯(這也是為什么編譯 ID 在 PrintComilation 的輸出結果中有時會失序的另一個原因)。

    當使用 client 編譯器時,JVM 啟動一個編譯線程,而 server 編譯器有兩個這樣的線程。當分層編譯生效時,JVM 會基于某些復雜方程式默認啟動多個 client 和 server 線程,涉及雙日志在目標平臺上的 CPU 數量。如下圖所示:

    分層編譯下 C1 和 C2 編譯器線程默認數量:

    圖 3. C1 和 C2 編譯器默認數量
    圖 3. C1 C2 編譯器默認數量

    編譯器線程的數量可以通過-XX:CICompilerCount=N flag 進行調節設置。這個數量是 JVM 將要執行隊列所用的線程總數。對于分層編譯,三分之一的(至少一個)線程被用于執行 client 編譯器隊列,剩下的(也是至少一個)被用來執行 server 編譯器隊列。

    在何時我們應該考慮調整這個值呢?如果一個程序被運行在單 CPU 機器上,那么只有一個編譯線程會更好一些:因為對于某個線程來說,其對 CPU 的使用是有限的,并且在很多情況下越少的線程競爭資源會使其運行性能更高。然而,這個優勢僅僅局限于初始預熱階段,之后,這些具有編譯資格的方法并不會真的引起 CPU 爭用。當一個股票批處理應用程序運行在單 CPU 機器上并且編譯器線程被限制成只有一個,那么最初的計算過程將比一般情況下快 10%(因為它沒有被其他線程進行 CPU 爭用)。迭代運行的次數越多,最初的性能收益就相對越少,直到所有的熱點方法被編譯完性能收益也隨之終止。

    回頁首

    結束語

    本文詳細介紹了 JIT 編譯器的工作原理。從優化的角度講,最簡單的選擇就是使用 server 編譯器的分層編譯技術,這將解決大約 90%左右的與編譯器直接相關的性能問題。最后,請保證代碼緩存的大小設置的足夠大,這樣編譯器將會提供最高的編譯性能。

    轉載自點擊打開鏈接

    posted @ 2017-06-08 17:27 小馬歌 閱讀(268) | 評論 (0)編輯 收藏
     

    from:http://www.cnblogs.com/insistence/p/5901457.html

    1. 什么是Just In Time編譯器?

    Hot Spot 編譯

    當 JVM 執行代碼時,它并不立即開始編譯代碼。這主要有兩個原因:

    首先,如果這段代碼本身在將來只會被執行一次,那么從本質上看,編譯就是在浪費精力。因為將代碼翻譯成 java 字節碼相對于編譯這段代碼并執行代碼來說,要快很多。

    當 然,如果一段代碼頻繁的調用方法,或是一個循環,也就是這段代碼被多次執行,那么編譯就非常值得了。因此,編譯器具有的這種權衡能力會首先執行解釋后的代 碼,然后再去分辨哪些方法會被頻繁調用來保證其本身的編譯。其實說簡單點,就是 JIT 在起作用,我們知道,對于 Java 代碼,剛開始都是被編譯器編譯成字節碼文件,然后字節碼文件會被交由 JVM 解釋執行,所以可以說 Java 本身是一種半編譯半解釋執行的語言。Hot Spot VM 采用了 JIT compile 技術,將運行頻率很高的字節碼直接編譯為機器指令執行以提高性能,所以當字節碼被 JIT 編譯為機器碼的時候,要說它是編譯執行的也可以。也就是說,運行時,部分代碼可能由 JIT 翻譯為目標機器指令(以 method 為翻譯單位,還會保存起來,第二次執行就不用翻譯了)直接執行。

    第二個原因是最優化,當 JVM 執行某一方法或遍歷循環的次數越多,就會更加了解代碼結構,那么 JVM 在編譯代碼的時候就做出相應的優化。

    我 們將在后面講解這些優化策略,這里,先舉一個簡單的例子:我們知道 equals() 這個方法存在于每一個 Java Object 中(因為是從 Object class 繼承而來)而且經常被覆寫。當解釋器遇到 b = obj1.equals(obj2) 這樣一句代碼,它則會查詢 obj1 的類型從而得知到底運行哪一個 equals() 方法。而這個動態查詢的過程從某種程度上說是很耗時的。

    在主流商用JVM(HotSpot、J9)中,Java程序一開始是通過解釋器(Interpreter)進行解釋執行的。當JVM發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定為“熱點代碼(Hot Spot Code)”,然后JVM會把這些代碼編譯成與本地平臺相關的機器碼,并進行各種層次的優化,完成這個任務的編譯器稱為:即時編譯器(Just In Time Compiler,JIT)

    JIT編譯器是“動態編譯器”的一種,相對的“靜態編譯器”則是指的比如:C/C++的編譯器

    JIT并不是JVM的必須部分,JVM規范并沒有規定JIT必須存在,更沒有限定和指導JIT。但是,JIT性能的好壞、代碼優化程度的高低卻是衡量一款JVM是否優秀的最關鍵指標之一,也是虛擬機中最核心且最能體現虛擬機技術水平的部分。


    2. 編譯器與解釋器

    首先,不是所有JVM都采用編譯器和解釋器并存的架構,但主流商用虛擬機,都同時包含這兩部分。

    2.1 配合過程

    1. 當程序需要迅速啟動然后執行的時候,解釋器可以首先發揮作用,編譯器不運行從而省去編譯時間,立即執行程序

    2. 在程序運行后,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲得更高的執行效率

    3. 當程序運行環境中內存資源限制較大(如部分嵌入式系統中),可以使用解釋執行來節約內存;反之,則可以使用編譯執行來提升效率。

    4. 同時,解釋器還可以作為編譯器(C2才會激進優化)激進優化時的一個“逃生門”,讓編譯器根據概率選擇一些大多數時候都能提升運行速度的優化手段,當激進優化假設不成立。如:加載了新類后,類型繼承結構出現變化,出現“罕見陷阱(Uncommon Trap)”時,可以通過逆優化(Deoptimization)退回到解釋狀態繼續執行 
      (部分沒有解釋器的虛擬機,也會采用不進行激進優化的C1編譯器擔任“逃生門”的角色)

      這里寫圖片描述

    2.2 解釋器 - Interpreter

    Interpreter解釋執行class文件,好像JavaScript執行引擎一樣

    特殊的例子:

    • 最早的Sun Classic VM只有Interpreter
    • BEA JRockit VM則只有Compiler,但它主要面向服務端應用,部署在其上的應用不重點關注啟動時間

    2.3 編譯器 - Compiler

    只說HotSpot JVM

    1. C1和C2:

    HotSpot虛擬機內置了兩個即時編譯器,分別稱為Client Compiler和Server Compiler,習慣上將前者稱為C1,后者稱為C2

    2. 使用C1還是C2?

    HotSpot默認采用解釋器和其中一個編譯器直接配合的方式工作,使用那個編譯器取決于虛擬機運行的模式,HotSpot會根據自身版本和宿主機器硬件性能自動選擇模式,用戶也可以使用“-client”或”-server”參數去指定

    1. 混合模式(Mixed Mode) 
      默認的模式,如上面描述的這種方式就是mixed mode

    2. 解釋模式(Interpreted Mode) 
      可以使用參數“-Xint”,在此模式下全部代碼解釋執行

    3. 編譯模式(Compiled Mode) 
      參數“-Xcomp”,此模式優先采用編譯,但是無法編譯時也會解釋(在最新的HotSpot中此參數被取消)

      可以看到,我的JVM現在是mixed mode 
      這里寫圖片描述

    重要:↓

    在JDK1.7(1.7僅包括Server模式)之后,HotSpot就不是默認“采用解釋器和其中一個編譯器”配合的方式了,而是采用了分層編譯,分層編譯時C1和C2有可能同時工作


    3. 分層編譯

    3.1 為什么要分層編譯?

    由于編譯器compile本地代碼需要占用程序時間,要編譯出優化程度更高的代碼所花費的時間可能更長,且此時解釋器還要替編譯器收集性能監控信息,這對解釋執行的速度也有影響

    所以,為了在程序啟動響應時間與運行效率之間達到最佳平衡,HotSpot在JDK1.6中出現了分層編譯(Tiered Compilation)的概念并在JDK1.7的Server模式JVM中作為默認策略被開啟

    3.2 編譯層 tier(或者叫級別)

    分層編譯根據編譯器編譯、優化的規模與耗時,劃分了不同的編譯層次(不只以下3種),包括:

    • 第0層,程序解釋執行(沒有編譯),解釋器不開啟性能監控功能,可觸發第1層編譯。

    • 第1層,也稱C1編譯,將字節碼編譯為本地代碼,進行簡單、可靠的優化,如有必要將加入性能監控的邏輯

    • 第2層(或2層以上),也稱為C2編譯,也是將字節碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優化,甚至會根據性能監控信息進行一些不可靠的激進優化

    實施分層編譯后,C1和C2將會同時工作,許多代碼會被多次編譯,用C1獲取更高的編譯速度,用C2來獲取更好的編譯質量,且在解釋執行的時候解釋器也無須再承擔收集性能監控信息的任務


    4. 編譯對象與觸發條件

    1. 誰被編譯了?

    編譯對象就是之前說的“熱點代碼”,它有兩類:

    1. 被多次調用的方法 
      • 一個方法被多次調用,理應稱為熱點代碼,這種編譯也是虛擬機中標準的JIT編譯方式
    2. 被多次執行的循環體 
      • 編譯動作由循環體出發,但編譯對象依然會以整個方法為對象
      • 這種編譯方式由于編譯發生在方法執行過程中,因此形象的稱為:棧上替換(On Stack Replacement- OSR編譯,即方法棧幀還在棧上,方法就被替換了)

    2. 觸發條件

    1. 綜述

    上面的方法和循環體都說“多次”,那么多少算多?換個說法就是編譯的觸發條件。

    判斷一段代碼是不是熱點代碼,是不是需要觸發JIT編譯,這樣的行為稱為:熱點探測(Hot Spot Detection),有幾種主流的探測方式:

    1. 基于計數器的熱點探測(Counter Based Hot Spot Detection) 
      虛擬機會為每個方法(或每個代碼塊)建立計數器,統計執行次數,如果超過閥值那么就是熱點代碼。缺點是維護計數器開銷。

    2. 基于采樣的熱點探測(Sample Based Hot Spot Detection) 
      虛擬機會周期性檢查各個線程的棧頂,如果某個方法經常出現在棧頂,那么就是熱點代碼。缺點是不精確。

    3. 基于蹤跡的熱點探測(Trace Based Hot Spot Detection) 
      Dalvik中的JIT編譯器使用這種方式

    2. HotSpot

    HotSpot使用的是第1種,因此它為每個方法準備了兩類計數器:方法調用計數器(Invocation Counter)和回邊計數器(Back Edge Counter)

    1. 方法計數器

      • 默認閥值,在Client模式下是1500次,Server是10000次,可以通過參數“-XX:CompileThreshold”來設定

      • 當一個方法被調用時會首先檢查是否存在被JIT編譯過得版本,如果存在則使用此本地代碼來執行;如果不存在,則將方法計數器+1,然后判斷“方法計數器和回邊計數器之和”是否超過閥值,如果是則會向編譯器提交一個方法編譯請求

      • 默認情況下,執行引擎并不會同步等待上面的編譯完成,而是會繼續解釋執行。當編譯完成后,此方法的調用入口地址會被系統自動改寫為新的本地代碼地址

      • 還有一點,熱度是會衰減的,也就是說不是僅僅+,也會-,熱度衰減動作是在虛擬機的GC執行時順便進行的

    2. 回邊計數器

      • 回邊,顧名思義,只有執行到大括號”}”時才算+1

      • 默認閥值,Client下13995,Server下10700

      • 它的調用邏輯和方法計數器差不多,只不過遇到回邊指令時+1、超過閥值時會提交OSR編譯請求以及這里沒有熱度衰減


    5. 編譯過程

    編譯過程是在后臺線程(daemon)中完成的,可以通過參數“-XX:-BackgroundCompilation”來禁止后臺編譯,但此時執行線程就會同步等待編譯完成才會執行程序

    1. Client Compiler 
      C1編譯器是一個簡單快速的三段式編譯器,主要關注“局部性能優化”,放棄許多耗時較長的全局優化手段 
      過程:class -> 1. 高級中間代碼 -> 2. 低級中間代碼 -> 3. 機器代碼
    2. Server Compiler 
      C2是專門面向服務器應用的編譯器,是一個充分優化過的高級編譯器,幾乎能達到GNU C++編譯器使用-O2參數時的優化強度。

    使用參數“-XX:+PrintCompilation”會讓虛擬機在JIT時把方法名稱打印出來,如圖: 
    這里寫圖片描述


    6. Java和C/C++的編譯器對比

    這里不是比Java和C/C++誰快這種大坑問題,只是比較編譯器(我認為開發效率上Java快,執行效率上C/C++快)

    這種對比代表了經典的即時編譯器與靜態編譯期的對比,其實總體來說Java編譯器有優有劣。主要就是動態編譯時間壓力大能做的優化少,還要做一些動態校驗。而靜態編譯器無法實現一些開發上很有用的動態特性

    posted @ 2017-06-08 17:26 小馬歌 閱讀(277) | 評論 (0)編輯 收藏
     
    from:http://www.tuicool.com/articles/r6Z7vaj

    JVM自動監控這所有方法的執行,如果某個方法是熱點方法,JVM就計劃把該方法的字節碼代碼編譯成本地機器代碼,編譯成機器代碼的過程是在獨立線程中執行的,不會影響程序的執行,這個過程就是JIT(just in time)。

    JIT針對下面的幾種方式進行優化

    • 把bytecode編譯成本地代碼
    • 單態調度(monomorphic dispatch),當個對象的類和其父類間有方法重寫時,JVM調用對象的方法可以通過對象的類型路徑來判斷應該調用父類的方法還是子類的方法,對此JIT進行優化,這種優化是C++所不具備的,C++中需要查找虛函數表。
    • 循環展開(loop unrolling)
    • 類型銳化
    • 逃逸分析(escape analysis)
    • 移除無用代碼(這個現在IDE會提示我們的,比如:intellij idea)
    • Intrinsics
    • 分支預測
    • 方法內聯(inlining,對性能的提升很大),默認情況,<= 35字節碼的方法可以進行內聯,通過這個來修改內聯方法的最大值:-XX:MaxInlineSize=,通過-XX:FreqInlineSize=來設置頻繁調用方法的臨界值

    這些優化方法通常是層層依賴的,所以當JIT優化后的代碼被JVM應用,就會開始嘗試進行更上一層次的優化。因此我們寫代碼的時候,應該盡量往這些優化方式上面靠。

    輸出JIT編譯過的方法

    在JVM啟動參數中添加如下的啟動參數:

    -XX:+PrintCompilation 

    輸出內容類似這樣:

    31    23 s!    sun.misc.URLClassPath::getLoader (136 bytes) 
    • 第1列  31:為JVM啟動后到該方法被編譯相隔的時間,單位為毫秒
    • 第2列  23:編譯ID,用來跟蹤一個方法的編譯、優化、深度優化
    • 第3列  s!:s是指該方法是synchronized,感嘆號是指該方法有對異常的處理
    • 第4列  sun.misc.URLClassPath::getLoader:被編譯的方法
    • 第5列  (136 bytes):方法的字節大小

    輸出JIT編譯的細節信息

    通過添加參數-XX:+PrintCompilation,可以看到的信息其實并不具體,比如:那些方法進行了內聯,內聯后的二進制代碼是怎么樣的都沒有。而要輸出JIT編譯的細節信息,就需要在JVM啟動參數中添加這個參數:

    -XX:+LogCompilation -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading -XX:+PrintAssembly 

    輸出的編譯信息,默認情況是在啟動JVM的目錄下一個名為:hotspot_pid<PID>.log的文件

    如果想指定文件路徑和文件名的話,可以再添加一個啟動參數:

    -XX:LogFile=<pathto file> 

    輸出的是一個很大的xml文件,可能有幾百兆,內容大致如下:

    <nmethodcompile_id='2' compiler='C1' level='3'  entry='0x00000001023fe240' size='1224'  address='0x00000001023fe0d0' relocation_offset='288'  insts_offset='368' stub_offset='880' scopes_data_offset='1032'  scopes_pcs_offset='1104' dependencies_offset='1200'  nul_chk_table_offset='1208'  method='java/lang/String hashCode ()I' bytes='55' count='512'  backedge_count='8218' iicount='512' stamp='0.350'/> 

    而且內容很難讀懂,建議使用JITWatch( https://github.com/AdoptOpenJDK/jitwatch/ )的可視化界面來查看JIT編譯的細節信息。同時JITWatch還可以給出很多優化建議,給我們有效的優化代碼提供參考,詳見下文。

    JIT編譯模式

    C1: 通常用于那種快速啟動的GUI應用,對應啟動參數:-client

    C2: 通常用于長時間允許的服務端應用,對應啟動參數:-server

    分層編譯模式(tiered compilation):這是自從Java SE 7以后的新特性,可通過添加啟動參數來開啟:

    -XX:+TieredCompilation 

    這個特性在應用啟動階段使用C1模式以達到快速啟動的效果,一旦應用程序運行起來以后,C2模式將取代C1模式,以進行更深度的優化。在Java SE 8中,這個特性是默認的。

    JITWatch

    前面也提到了,JITWatch可以通過可視化界面來幫助我們分析JVM輸出的JIT編譯輸出日志,還可以幫助我們靜態分析jar中的代碼是否符合JIT編譯優化的條件,還可以以曲線圖形的方式展示JIT編譯的整個過程中的一些指標,非常好用的工具。

    下載

    JITWatch需要在github上把代碼clone下來,然后用maven來運行,地址為: https://github.com/AdoptOpenJDK/jitwatch/

    運行JITWwatch

    在代碼根目錄下執行 launchUI.sh( Linux/Mac)或則 launchUI.bat(windows)

    如果你使用maven,也可以在代碼根目錄下這樣運行(其他運行方式,請參考JITWatch的github首頁)

    mvncleancompileexec:java 

    如果你使用的是mac,而且idk版本是jdk7,且運行mvn clean compile exec:java時出現下面的錯誤和異常時: 

    Causedby: java.lang.NullPointerException  atcom.sun.t2k.MacFontFinder.initPSFontNameToPathMap(MacFontFinder.java:339)  atcom.sun.t2k.MacFontFinder.getFontNamesOfFontFamily(MacFontFinder.java:390)  atcom.sun.t2k.T2KFontFactory.getFontResource(T2KFontFactory.java:233)  atcom.sun.t2k.LogicalFont.getSlot0Resource(LogicalFont.java:184)  atcom.sun.t2k.LogicalFont.getSlotResource(LogicalFont.java:228)  atcom.sun.t2k.CompositeStrike.getStrikeSlot(CompositeStrike.java:86)  atcom.sun.t2k.CompositeStrike.getMetrics(CompositeStrike.java:132)  atcom.sun.javafx.font.PrismFontUtils.getFontMetrics(PrismFontUtils.java:31)  atcom.sun.javafx.font.PrismFontLoader.getFontMetrics(PrismFontLoader.java:466)  atjavafx.scene.text.Text.<init>(Text.java:153)  atcom.sun.javafx.scene.control.skin.Utils.<clinit>(Utils.java:52)  ... 13 more   [ERROR] Failedto executegoalorg.codehaus.mojo:exec-maven-plugin:1.5.0:java (default-cli) onprojectjitwatch-ui: Anexceptionoccuredwhile executingtheJavaclass. null: InvocationTargetException: Exceptionin Applicationstartmethod: ExceptionInInitializerError: NullPointerException -> [Help 1] 

    請在org.adoptopenjdk.jitwatch.launch.LaunchUI類的main函數開頭處添加下面的代碼(或者直接使用我fork修改好的 JITWatch ):

    final Class<?> macFontFinderClass = Class.forName("com.sun.t2k.MacFontFinder"); final java.lang.reflect.FieldpsNameToPathMap = macFontFinderClass.getDeclaredField("psNameToPathMap"); psNameToPathMap.setAccessible(true); if (psNameToPathMap.get(null) == null) {     psNameToPathMap.set(         null, new java.util.HashMap<String, String>()); } final java.lang.reflect.FieldallAvailableFontFamilies = macFontFinderClass.getDeclaredField("allAvailableFontFamilies"); allAvailableFontFamilies.setAccessible(true); if (allAvailableFontFamilies.get(null) == null) {     allAvailableFontFamilies.set(         null, new String[] {}); } 

    然后重新運行即可看到JITWatch的界面。

    Reference

    http://www.oracle.com/technetwork/articles/java/architect-evans-pt1-2266278.html

    https://www.chrisnewland.com/images/jitwatch/HotSpot_Profiling_Using_JITWatch.pdf

    posted @ 2017-06-08 16:48 小馬歌 閱讀(848) | 評論 (0)編輯 收藏
     
         摘要: from:http://yemengying.com/2016/05/07/threadsafe-hashmap/ 2016-05-07 JAVAHASHMAP HASHMAP, JAVA文章目錄1. 為什么HashMap是線程不安全的1.1. HashMap的內部存儲結構1.2. HashMap的自動擴容機制1.3. ...  閱讀全文
    posted @ 2017-06-02 17:57 小馬歌 閱讀(244) | 評論 (0)編輯 收藏
     
    from:http://tech.meituan.com/java-hashmap.html

    摘要

    HashMap是Java程序員使用頻率最高的用于映射(鍵值對)處理的數據類型。隨著JDK(Java Developmet Kit)版本的更新,JDK1.8對HashMap底層的實現進行了優化,例如引入紅黑樹的數據結構和擴容的優化等。本文結合JDK1.7和JDK1.8的區別,深入探討HashMap的結構實現和功能原理。

    簡介

    Java為數據結構中的映射定義了一個接口java.util.Map,此接口主要有四個常用的實現類,分別是HashMap、Hashtable、LinkedHashMap和TreeMap,類繼承關系如下圖所示:

    java.util.map類圖

    下面針對各個實現類的特點做一些說明:

    (1) HashMap:它根據鍵的hashCode值存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多只允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非線程安全,即任一時刻可以有多個線程同時寫HashMap,可能會導致數據的不一致。如果需要滿足線程安全,可以用 Collections的synchronizedMap方法使HashMap具有線程安全的能力,或者使用ConcurrentHashMap。

    (2) Hashtable:Hashtable是遺留類,很多映射的常用功能與HashMap類似,不同的是它承自Dictionary類,并且是線程安全的,任一時間只有一個線程能寫Hashtable,并發性不如ConcurrentHashMap,因為ConcurrentHashMap引入了分段鎖。Hashtable不建議在新代碼中使用,不需要線程安全的場合可以用HashMap替換,需要線程安全的場合可以用ConcurrentHashMap替換。

    (3) LinkedHashMap:LinkedHashMap是HashMap的一個子類,保存了記錄的插入順序,在用Iterator遍歷LinkedHashMap時,先得到的記錄肯定是先插入的,也可以在構造時帶參數,按照訪問次序排序。

    (4) TreeMap:TreeMap實現SortedMap接口,能夠把它保存的記錄根據鍵排序,默認是按鍵值的升序排序,也可以指定排序的比較器,當用Iterator遍歷TreeMap時,得到的記錄是排過序的。如果使用排序的映射,建議使用TreeMap。在使用TreeMap時,key必須實現Comparable接口或者在構造TreeMap傳入自定義的Comparator,否則會在運行時拋出java.lang.ClassCastException類型的異常。

    對于上述四種Map類型的類,要求映射中的key是不可變對象。不可變對象是該對象在創建后它的哈希值不會被改變。如果對象的哈希值發生變化,Map對象很可能就定位不到映射的位置了。

    通過上面的比較,我們知道了HashMap是Java的Map家族中一個普通成員,鑒于它可以滿足大多數場景的使用條件,所以是使用頻度最高的一個。下文我們主要結合源碼,從存儲結構、常用方法分析、擴容以及安全性等方面深入講解HashMap的工作原理。

    內部實現

    搞清楚HashMap,首先需要知道HashMap是什么,即它的存儲結構-字段;其次弄明白它能干什么,即它的功能實現-方法。下面我們針對這兩個方面詳細展開講解。

    存儲結構-字段

    從結構實現來講,HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的,如下如所示。

    hashMap內存結構圖

    這里需要講明白兩個問題:數據底層具體存儲的是什么?這樣的存儲方式有什么優點呢?

    (1) 從源碼可知,HashMap類中有一個非常重要的字段,就是 Node[] table,即哈希桶數組,明顯它是一個Node的數組。我們來看Node[JDK1.8]是何物。

    static class Node<K,V> implements Map.Entry<K,V> {         final int hash;    //用來定位數組索引位置         final K key;         V value;         Node<K,V> next;   //鏈表的下一個node          Node(int hash, K key, V value, Node<K,V> next) { ... }         public final K getKey(){ ... }         public final V getValue() { ... }         public final String toString() { ... }         public final int hashCode() { ... }         public final V setValue(V newValue) { ... }         public final boolean equals(Object o) { ... } } 

    Node是HashMap的一個內部類,實現了Map.Entry接口,本質是就是一個映射(鍵值對)。上圖中的每個黑色圓點就是一個Node對象。

    (2) HashMap就是使用哈希表來存儲的。哈希表為解決沖突,可以采用開放地址法和鏈地址法等來解決問題,Java中HashMap采用了鏈地址法。鏈地址法,簡單來說,就是數組加鏈表的結合。在每個數組元素上都一個鏈表結構,當數據被Hash后,得到數組下標,把數據放在對應下標元素的鏈表上。例如程序執行下面代碼:

        map.put("美團","小美"); 

    系統將調用"美團"這個key的hashCode()方法得到其hashCode 值(該方法適用于每個Java對象),然后再通過Hash算法的后兩步運算(高位運算和取模運算,下文有介紹)來定位該鍵值對的存儲位置,有時兩個key會定位到相同的位置,表示發生了Hash碰撞。當然Hash算法計算結果越分散均勻,Hash碰撞的概率就越小,map的存取效率就會越高。

    如果哈希桶數組很大,即使較差的Hash算法也會比較分散,如果哈希桶數組數組很小,即使好的Hash算法也會出現較多碰撞,所以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況確定哈希桶數組的大小,并在此基礎上設計好的hash算法減少Hash碰撞。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數組(Node[] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。

    在理解Hash和擴容流程之前,我們得先了解下HashMap的幾個字段。從HashMap的默認構造函數源碼可知,構造函數就是對下面幾個字段進行初始化,源碼如下:

         int threshold;             // 所能容納的key-value對極限       final float loadFactor;    // 負載因子      int modCount;        int size; 

    首先,Node[] table的初始化長度length(默認值是16),Load factor為負載因子(默認值是0.75),threshold是HashMap所能容納的最大數據量的Node(鍵值對)個數。threshold = length * Load factor。也就是說,在數組定義好長度之后,負載因子越大,所能容納的鍵值對個數越多。

    結合負載因子的定義公式可知,threshold就是在此Load factor和length(數組長度)對應下允許的最大元素數目,超過這個數目就重新resize(擴容),擴容后的HashMap容量是之前容量的兩倍。默認的負載因子0.75是對空間和時間效率的一個平衡選擇,建議大家不要修改,除非在時間和空間比較特殊的情況下,如果內存空間很多而又對時間效率要求很高,可以降低負載因子Load factor的值;相反,如果內存空間緊張而對時間效率要求不高,可以增加負載因子loadFactor的值,這個值可以大于1。

    size這個字段其實很好理解,就是HashMap中實際存在的鍵值對數量。注意和table的長度length、容納最大鍵值對數量threshold的區別。而modCount字段主要用來記錄HashMap內部結構發生變化的次數,主要用于迭代的快速失敗。強調一點,內部結構發生變化指的是結構發生變化,例如put新鍵值對,但是某個key對應的value值被覆蓋不屬于結構變化。

    在HashMap中,哈希桶數組table的長度length大小必須為2的n次方(一定是合數),這是一種非常規的設計,常規的設計是把桶的大小設計為素數。相對來說素數導致沖突的概率要小于合數,具體證明可以參考http://blog.csdn.net/liuqiyao_01/article/details/14475159,Hashtable初始化桶大小為11,就是桶大小設計為素數的應用(Hashtable擴容后不能保證還是素數)。HashMap采用這種非常規設計,主要是為了在取模和擴容時做優化,同時為了減少沖突,HashMap定位哈希桶索引位置時,也加入了高位參與運算的過程。

    這里存在一個問題,即使負載因子和Hash算法設計的再合理,也免不了會出現拉鏈過長的情況,一旦出現拉鏈過長,則會嚴重影響HashMap的性能。于是,在JDK1.8版本中,對數據結構做了進一步的優化,引入了紅黑樹。而當鏈表長度太長(默認超過8)時,鏈表就轉換為紅黑樹,利用紅黑樹快速增刪改查的特點提高HashMap的性能,其中會用到紅黑樹的插入、刪除、查找等算法。本文不再對紅黑樹展開討論,想了解更多紅黑樹數據結構的工作原理可以參考http://blog.csdn.net/v_july_v/article/details/6105630

    功能實現-方法

    HashMap的內部功能實現很多,本文主要從根據key獲取哈希桶數組索引位置、put方法的詳細執行、擴容過程三個具有代表性的點深入展開講解。

    1. 確定哈希桶數組索引位置

    不管增加、刪除、查找鍵值對,定位到哈希桶數組的位置都是很關鍵的第一步。前面說過HashMap的數據結構是數組和鏈表的結合,所以我們當然希望這個HashMap里面的元素位置盡量分布均勻些,盡量使得每個位置上的元素數量只有一個,那么當我們用hash算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,不用遍歷鏈表,大大優化了查詢的效率。HashMap定位數組索引位置,直接決定了hash方法的離散性能。先看看源碼的實現(方法一+方法二):

    方法一: static final int hash(Object key) {   //jdk1.8 & jdk1.7      int h;      // h = key.hashCode() 為第一步 取hashCode值      // h ^ (h >>> 16)  為第二步 高位參與運算      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } 方法二: static int indexFor(int h, int length) {  //jdk1.7的源碼,jdk1.8沒有這個方法,但是實現原理一樣的      return h & (length-1);  //第三步 取模運算 } 

    這里的Hash算法本質上就是三步:取key的hashCode值、高位運算、取模運算

    對于任意給定的對象,只要它的hashCode()返回值相同,那么程序調用方法一所計算得到的Hash碼值總是相同的。我們首先想到的就是把hash值對數組長度取模運算,這樣一來,元素的分布相對來說是比較均勻的。但是,模運算的消耗還是比較大的,在HashMap中是這樣做的:調用方法二來計算該對象應該保存在table數組的哪個索引處。

    這個方法非常巧妙,它通過h & (table.length -1)來得到該對象的保存位,而HashMap底層數組的長度總是2的n次方,這是HashMap在速度上的優化。當length總是2的n次方時,h& (length-1)運算等價于對length取模,也就是h%length,但是&比%具有更高的效率。

    在JDK1.8的實現中,優化了高位運算的算法,通過hashCode()的高16位異或低16位實現的:(h = k.hashCode()) ^ (h >>> 16),主要是從速度、功效、質量來考慮的,這么做可以在數組table的length比較小的時候,也能保證考慮到高低Bit都參與到Hash的計算中,同時不會有太大的開銷。

    下面舉例說明下,n為table的長度。

    hashMap哈希算法例圖

    2. 分析HashMap的put方法

    HashMap的put方法執行過程可以通過下圖來理解,自己有興趣可以去對比源碼更清楚地研究學習。

    hashMap put方法執行流程圖

    ①.判斷鍵值對數組table[i]是否為空或為null,否則執行resize()進行擴容;

    ②.根據鍵值key計算hash值得到插入的數組索引i,如果table[i]==null,直接新建節點添加,轉向⑥,如果table[i]不為空,轉向③;

    ③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這里的相同指的是hashCode以及equals;

    ④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;

    ⑤.遍歷table[i],判斷鏈表長度是否大于8,大于8的話把鏈表轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行鏈表的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;

    ⑥.插入成功后,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。

    JDK1.8HashMap的put方法源碼如下:

     1 public V put(K key, V value) {  2     // 對key的hashCode()做hash  3     return putVal(hash(key), key, value, false, true);  4 }  5   6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  7                boolean evict) {  8     Node<K,V>[] tab; Node<K,V> p; int n, i;  9     // 步驟①:tab為空則創建 10     if ((tab = table) == null || (n = tab.length) == 0) 11         n = (tab = resize()).length; 12     // 步驟②:計算index,并對null做處理  13     if ((p = tab[i = (n - 1) & hash]) == null)  14         tab[i] = newNode(hash, key, value, null); 15     else { 16         Node<K,V> e; K k; 17         // 步驟③:節點key存在,直接覆蓋value 18         if (p.hash == hash && 19             ((k = p.key) == key || (key != null && key.equals(k)))) 20             e = p; 21         // 步驟④:判斷該鏈為紅黑樹 22         else if (p instanceof TreeNode) 23             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24         // 步驟⑤:該鏈為鏈表 25         else { 26             for (int binCount = 0; ; ++binCount) { 27                 if ((e = p.next) == null) { 28                     p.next = newNode(hash, key,value,null);                         //鏈表長度大于8轉換為紅黑樹進行處理 29                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   30                         treeifyBin(tab, hash); 31                     break; 32                 }                     // key已經存在直接覆蓋value 33                 if (e.hash == hash && 34                     ((k = e.key) == key || (key != null && key.equals(k))))  35                            break; 36                 p = e; 37             } 38         } 39          40         if (e != null) { // existing mapping for key 41             V oldValue = e.value; 42             if (!onlyIfAbsent || oldValue == null) 43                 e.value = value; 44             afterNodeAccess(e); 45             return oldValue; 46         } 47     }  48     ++modCount; 49     // 步驟⑥:超過最大容量 就擴容 50     if (++size > threshold) 51         resize(); 52     afterNodeInsertion(evict); 53     return null; 54 } 

    3. 擴容機制

    擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,而HashMap對象內部的數組無法裝載更多的元素時,對象就需要擴大數組的長度,以便能裝入更多的元素。當然Java里的數組是無法自動擴容的,方法是使用一個新的數組代替已有的容量小的數組,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。

    我們分析下resize的源碼,鑒于JDK1.8融入了紅黑樹,較復雜,為了便于理解我們仍然使用JDK1.7的代碼,好理解一些,本質上區別不大,具體區別后文再說。

     1 void resize(int newCapacity) {   //傳入新的容量  2     Entry[] oldTable = table;    //引用擴容前的Entry數組  3     int oldCapacity = oldTable.length;           4     if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的數組大小如果已經達到最大(2^30)了  5         threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以后就不會擴容了  6         return;  7     }  8    9     Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry數組 10     transfer(newTable);                         //!!將數據轉移到新的Entry數組里 11     table = newTable;                           //HashMap的table屬性引用新的Entry數組 12     threshold = (int)(newCapacity * loadFactor);//修改閾值 13 } 

    這里就是使用一個容量更大的數組來代替已有的容量小的數組,transfer()方法將原有Entry數組的元素拷貝到新的Entry數組里。

     1 void transfer(Entry[] newTable) {  2     Entry[] src = table;                   //src引用了舊的Entry數組  3     int newCapacity = newTable.length;  4     for (int j = 0; j < src.length; j++) { //遍歷舊的Entry數組  5         Entry<K,V> e = src[j];             //取得舊Entry數組的每個元素  6         if (e != null) {  7             src[j] = null;//釋放舊Entry數組的對象引用(for循環后,舊的Entry數組不再引用任何對象)  8             do {  9                 Entry<K,V> next = e.next; 10                 int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在數組中的位置 11                 e.next = newTable[i]; //標記[1] 12                 newTable[i] = e;      //將元素放在數組上 13                 e = next;             //訪問下一個Entry鏈上的元素 14             } while (e != null); 15         } 16     } 17 } 

    newTable[i]的引用賦給了e.next,也就是使用了單鏈表的頭插入方式,同一位置上新元素總會被放在鏈表的頭部位置;這樣先放在一個索引上的元素終會被放到Entry鏈的尾部(如果發生了hash沖突的話),這一點和Jdk1.8有區別,下文詳解。在舊數組中同一條Entry鏈上的元素,通過重新計算索引位置后,有可能被放到了新數組的不同位置上。

    下面舉個例子說明下擴容過程。假設了我們的hash算法就是簡單的用key mod 一下表的大小(也就是數組的長度)。其中的哈希桶數組table的size=2, 所以key = 3、7、5,put順序依次為 5、7、3。在mod 2以后都沖突在table[1]這里了。這里假設負載因子 loadFactor=1,即當鍵值對的實際大小size 大于 table的實際大小時進行擴容。接下來的三個步驟是哈希桶數組 resize成4,然后所有的Node重新rehash的過程。

    jdk1.7擴容例圖

    下面我們講解下JDK1.8做了哪些優化。經過觀測可以發現,我們使用的是2次冪的擴展(指長度擴為原來2倍),所以,元素的位置要么是在原位置,要么是在原位置再移動2次冪的位置。看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容后key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的哈希與高位運算結果。

    hashMap 1.8 哈希算法例圖1

    元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

    hashMap 1.8 哈希算法例圖2

    因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:

    jdk1.8 hashMap擴容例圖

    這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由于新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的沖突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。有一點注意區別,JDK1.7中rehash的時候,舊鏈表遷移新鏈表的時候,如果在新表的數組索引位置相同,則鏈表元素會倒置,但是從上圖可以看出,JDK1.8不會倒置。有興趣的同學可以研究下JDK1.8的resize源碼,寫的很贊,如下:

     1 final Node<K,V>[] resize() {  2     Node<K,V>[] oldTab = table;  3     int oldCap = (oldTab == null) ? 0 : oldTab.length;  4     int oldThr = threshold;  5     int newCap, newThr = 0;  6     if (oldCap > 0) {  7         // 超過最大值就不再擴充了,就只好隨你碰撞去吧  8         if (oldCap >= MAXIMUM_CAPACITY) {  9             threshold = Integer.MAX_VALUE; 10             return oldTab; 11         } 12         // 沒超過最大值,就擴充為原來的2倍 13         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 14                  oldCap >= DEFAULT_INITIAL_CAPACITY) 15             newThr = oldThr << 1; // double threshold 16     } 17     else if (oldThr > 0) // initial capacity was placed in threshold 18         newCap = oldThr; 19     else {               // zero initial threshold signifies using defaults 20         newCap = DEFAULT_INITIAL_CAPACITY; 21         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 22     } 23     // 計算新的resize上限 24     if (newThr == 0) { 25  26         float ft = (float)newCap * loadFactor; 27         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 28                   (int)ft : Integer.MAX_VALUE); 29     } 30     threshold = newThr; 31     @SuppressWarnings({"rawtypes","unchecked"}) 32         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 33     table = newTab; 34     if (oldTab != null) { 35         // 把每個bucket都移動到新的buckets中 36         for (int j = 0; j < oldCap; ++j) { 37             Node<K,V> e; 38             if ((e = oldTab[j]) != null) { 39                 oldTab[j] = null; 40                 if (e.next == null) 41                     newTab[e.hash & (newCap - 1)] = e; 42                 else if (e instanceof TreeNode) 43                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 44                 else { // 鏈表優化重hash的代碼塊 45                     Node<K,V> loHead = null, loTail = null; 46                     Node<K,V> hiHead = null, hiTail = null; 47                     Node<K,V> next; 48                     do { 49                         next = e.next; 50                         // 原索引 51                         if ((e.hash & oldCap) == 0) { 52                             if (loTail == null) 53                                 loHead = e; 54                             else 55                                 loTail.next = e; 56                             loTail = e; 57                         } 58                         // 原索引+oldCap 59                         else { 60                             if (hiTail == null) 61                                 hiHead = e; 62                             else 63                                 hiTail.next = e; 64                             hiTail = e; 65                         } 66                     } while ((e = next) != null); 67                     // 原索引放到bucket里 68                     if (loTail != null) { 69                         loTail.next = null; 70                         newTab[j] = loHead; 71                     } 72                     // 原索引+oldCap放到bucket里 73                     if (hiTail != null) { 74                         hiTail.next = null; 75                         newTab[j + oldCap] = hiHead; 76                     } 77                 } 78             } 79         } 80     } 81     return newTab; 82 } 

    線程安全性

    在多線程使用場景中,應該盡量避免使用線程不安全的HashMap,而使用線程安全的ConcurrentHashMap。那么為什么說HashMap是線程不安全的,下面舉例子說明在并發的多線程使用場景中使用HashMap可能造成死循環。代碼例子如下(便于理解,仍然使用JDK1.7的環境):

    public class HashMapInfiniteLoop {        private static HashMap<Integer,String> map = new HashMap<Integer,String>(2,0.75f);       public static void main(String[] args) {           map.put(5, "C");            new Thread("Thread1") {               public void run() {                   map.put(7, "B");                   System.out.println(map);               };           }.start();           new Thread("Thread2") {               public void run() {                   map.put(3, "A);                   System.out.println(map);               };           }.start();             }   } 

    其中,map初始化為一個長度為2的數組,loadFactor=0.75,threshold=2*0.75=1,也就是說當put第二個key的時候,map就需要進行resize。

    通過設置斷點讓線程1和線程2同時debug到transfer方法(3.3小節代碼塊)的首行。注意此時兩個線程已經成功添加數據。放開thread1的斷點至transfer方法的“Entry next = e.next;” 這一行;然后放開線程2的的斷點,讓線程2進行resize。結果如下圖。

    jdk1.7 hashMap死循環例圖1

    注意,Thread1的 e 指向了key(3),而next指向了key(7),其在線程二rehash后,指向了線程二重組后的鏈表。

    線程一被調度回來執行,先是執行 newTalbe[i] = e, 然后是e = next,導致了e指向了key(7),而下一次循環的next = e.next導致了next指向了key(3)。

    jdk1.7 hashMap死循環例圖2

    jdk1.7 hashMap死循環例圖3

    e.next = newTable[i] 導致 key(3).next 指向了 key(7)。注意:此時的key(7).next 已經指向了key(3), 環形鏈表就這樣出現了。

    jdk1.7 hashMap死循環例圖4

    于是,當我們用線程一調用map.get(11)時,悲劇就出現了——Infinite Loop。

    JDK1.8與JDK1.7的性能對比

    HashMap中,如果key經過hash算法得出的數組索引位置全部不相同,即Hash算法非常好,那樣的話,getKey方法的時間復雜度就是O(1),如果Hash算法技術的結果碰撞非常多,假如Hash算極其差,所有的Hash算法結果得出的索引位置一樣,那樣所有的鍵值對都集中到一個桶中,或者在一個鏈表中,或者在一個紅黑樹中,時間復雜度分別為O(n)和O(lgn)。 鑒于JDK1.8做了多方面的優化,總體性能優于JDK1.7,下面我們從兩個方面用例子證明這一點。

    Hash較均勻的情況

    為了便于測試,我們先寫一個類Key,如下:

    class Key implements Comparable<Key> {      private final int value;      Key(int value) {         this.value = value;     }      @Override     public int compareTo(Key o) {         return Integer.compare(this.value, o.value);     }      @Override     public boolean equals(Object o) {         if (this == o) return true;         if (o == null || getClass() != o.getClass())             return false;         Key key = (Key) o;         return value == key.value;     }      @Override     public int hashCode() {         return value;     } } 

    這個類復寫了equals方法,并且提供了相當好的hashCode函數,任何一個值的hashCode都不會相同,因為直接使用value當做hashcode。為了避免頻繁的GC,我將不變的Key實例緩存了起來,而不是一遍一遍的創建它們。代碼如下:

    public class Keys {      public static final int MAX_KEY = 10_000_000;     private static final Key[] KEYS_CACHE = new Key[MAX_KEY];      static {         for (int i = 0; i < MAX_KEY; ++i) {             KEYS_CACHE[i] = new Key(i);         }     }      public static Key of(int value) {         return KEYS_CACHE[value];     } } 

    現在開始我們的試驗,測試需要做的僅僅是,創建不同size的HashMap(1、10、100、......10000000),屏蔽了擴容的情況,代碼如下:

       static void test(int mapSize) {          HashMap<Key, Integer> map = new HashMap<Key,Integer>(mapSize);         for (int i = 0; i < mapSize; ++i) {             map.put(Keys.of(i), i);         }          long beginTime = System.nanoTime(); //獲取納秒         for (int i = 0; i < mapSize; i++) {             map.get(Keys.of(i));         }         long endTime = System.nanoTime();         System.out.println(endTime - beginTime);     }      public static void main(String[] args) {         for(int i=10;i<= 1000 0000;i*= 10){             test(i);         }     } 

    在測試中會查找不同的值,然后度量花費的時間,為了計算getKey的平均時間,我們遍歷所有的get方法,計算總的時間,除以key的數量,計算一個平均值,主要用來比較,絕對值可能會受很多環境因素的影響。結果如下:

    性能比較表1.png

    通過觀測測試結果可知,JDK1.8的性能要高于JDK1.7 15%以上,在某些size的區域上,甚至高于100%。由于Hash算法較均勻,JDK1.8引入的紅黑樹效果不明顯,下面我們看看Hash不均勻的的情況。

    Hash極不均勻的情況

    假設我們又一個非常差的Key,它們所有的實例都返回相同的hashCode值。這是使用HashMap最壞的情況。代碼修改如下:

    class Key implements Comparable<Key> {      //...      @Override     public int hashCode() {         return 1;     } } 

    仍然執行main方法,得出的結果如下表所示:

    性能比較表2.png

    從表中結果中可知,隨著size的變大,JDK1.7的花費時間是增長的趨勢,而JDK1.8是明顯的降低趨勢,并且呈現對數增長穩定。當一個鏈表太長的時候,HashMap會動態的將它替換成一個紅黑樹,這話的話會將時間復雜度從O(n)降為O(logn)。hash算法均勻和不均勻所花費的時間明顯也不相同,這兩種情況的相對比較,可以說明一個好的hash算法的重要性。

          測試環境:處理器為2.2 GHz Intel Core i7,內存為16 GB 1600 MHz DDR3,SSD硬盤,使用默認的JVM參數,運行在64位的OS X 10.10.1上。

    小結

    (1) 擴容是一個特別耗性能的操作,所以當程序員在使用HashMap的時候,估算map的大小,初始化的時候給一個大致的數值,避免map進行頻繁的擴容。

    (2) 負載因子是可以修改的,也可以大于1,但是建議不要輕易修改,除非情況非常特殊。

    (3) HashMap是線程不安全的,不要在并發的環境中同時操作HashMap,建議使用ConcurrentHashMap。

    (4) JDK1.8引入紅黑樹大程度優化了HashMap的性能。

    (5) 還沒升級JDK1.8的,現在開始升級吧。HashMap的性能提升僅僅是JDK1.8的冰山一角。

    參考

    1. JDK1.7&JDK1.8 源碼。
    2. CSDN博客頻道,HashMap多線程死循環問題,2014。
    3. 紅黑聯盟,Java類集框架之HashMap(JDK1.8)源碼剖析,2015。
    4. CSDN博客頻道, 教你初步了解紅黑樹,2010。
    5. Java Code Geeks,HashMap performance improvements in Java 8,2014。
    6. Importnew,危險!在HashMap中將可變對象用作Key,2014。
    7. CSDN博客頻道,為什么一般hashtable的桶數會取一個素數,2013。


    posted @ 2017-06-02 17:56 小馬歌 閱讀(156) | 評論 (0)編輯 收藏
     
    from:http://blog.csdn.net/hemeinvyiqiluoben/article/details/62439861

    課程安排
    第一天 
    上午
    一、機器學習基礎1.線性代數
    (1).矩陣運算  (2).向量運算
    (3).SVD      (4).PCA
    2.概率信息論
    (1).概率分布  (2).期望、方差、協方差
    (3). 貝葉斯   (4).結構概率模型
    3.數值優化
    二、深度學習基礎1.深度學習介紹 
    (1).發展歷史  (2).主要應用
    2.感知器      
    3.人工神經網絡
    4.前饋神經網絡  
    5.BP算法      
    6.Hessian矩陣
    第一天
      下午
    三、深度學習進階---卷積神經網絡1.CNN卷積神經網絡
    (1).卷積層(一維卷積、二維卷積)
    (2).池化層(均值池化、最大池化)
    (3). 全連接層
    (4).激活函數層
    (5).Softmax層
    2.CNN卷積神經網絡改進
    (1).R-CNN (SPPNET)
    (2).Fast-R-CNN
    (3).Faster-R-CNN (YOLO、SSD)
    3.深度學習的模型訓練技巧
    4.梯度下降的優化方法詳解
    第二天
    上午
    四、深度學習軟件1.深度學習相關軟件的安裝配置與使用介紹
    (1).Caffe
    (2).Tensorflow
    (3).Torch
    (4).MXNet
    第二天
    下午
    五、 CNN應用案例(1).CNN與手寫數字集分類
    (2).YOLO實現目標檢測
    (3).PixelNet原理與實現
    (4).利用卷積神經網絡做圖像風格結合
    第三天
    上午
    六、深度學習——循環神經網絡1.RNN循環神經網絡
    (1).梯度計算
    (2).BPTT
    2.RNN循環神經網絡改進
    (1).LSTM
    (2).GRU
    (3).Bi-RNN
    (4).Attention based RNN
    3.RNN實際應用
    (1).Seq2Seq的原理與實現   
    第三天
    下午
    、強化學習1.強化學習的理論知識
    2.經典模型DQN講解
    3.AlphaGo原理講解
    4.RL實際應用
    (1).實現一個AlphaGo     
    第四天
    上午
    八、對抗性生成網絡1.GAN的理論知識
    2.GAN經典模型
    (1).GAN,CGAN,LAPGAN,DCGAN,
    3.GAN經典模型
    (1). INFOGAN,WGAN,S2-GAN
    4.GAN實際應用
    (1).DCGAN提高模糊圖片分辨率
    5.GAN實際應用
    (1).InfoGAN做特定的樣本生成
    第四天
    下午
    九、遷移學習1.遷移學習的理論概述
    2.遷移學習的常見方法
    (1).基于特征的遷移
    (2).基于實例的遷移
    (3).基于數據的遷移
    (4).深度遷移學習
    (5).強化遷移學習
    (6).遷移學習的研究案例
    (7).遷移學習的應用
    (8).2017年AAAI最佳論文講解:利用物理定理的知識遷移到視頻理解
    posted @ 2017-05-17 11:28 小馬歌 閱讀(418) | 評論 (0)編輯 收藏
     
    from:http://mobile.51cto.com/hot-439693.htm

    背景:除去大名鼎鼎的QQ這款即時聊天工具,還有許多細分行業的IM,比如淘寶阿里旺旺、網易泡泡、YY語音......。恰巧公司產品也要開發一款基于我 們自己行業的類IM系統,很有幸我擔當了這個產品的架構師,核心代碼編寫、實現者。下面把我近年來從技術上我對IM系統(即時消息的傳輸,不包括語音,視頻,文件的傳輸)的理解和設計分享出來,淺薄之見,望大家別見笑,歡迎給出批評意見。

    一.網絡傳輸協議的選擇

    目前我知曉的所有IM系統傳輸即時消息無外乎使用UDP、TCP、基于TCP的http這幾種協議中的一種或幾種。比如QQ主要采用UDP協議,MSN主要采用TCP協議,而且他們也都支持HTTP協議的代理模式。更多資料,請參加這篇文章《一些常用軟件的網絡端口協議分類介紹》

    我們該如何選擇呢?

    • UDP協議實時性更好,但是如何處理安全可靠的傳輸并且處理不同客戶端之間的消息交互是個難題,實現起來過于復雜;

    • HTTP協議屬于擴展支持,我們在產品的初始階段可以不用支持;

    • 那就非TCP協議莫屬了,要考慮的同樣也有很多,特別是如果有海量用戶的需求。如何保證單機服務器高并發量,如何做到靈活,擴展的架構。

    Tips: QQ 為什么采用 UDP 協議,而不采用 TCP 協議實現?

    二.應該選擇什么格式的數據協議

    二進制格式?文本格式?這個話題轉到我的這篇文章《網絡傳輸數據格式的選擇》,從我們當前的需求和產品周期上我覺得選擇JSON形式的數據協議是最好的。

    三.架構設計

    首先我們來提煉一下一個IM系統的主要需求,包括賬號,關系鏈,在線狀態顯示,消息交互......。

    架構考量:

    • 由于采用可靠傳輸協議TCP,考慮到負載問題(短連接實現賬號、關系鏈相關業務,長連接實現上線、信息推送);

    • 后臺架構的靈活性、可擴展性,支持分布式部署——把網絡層、業務邏輯層、數據層分離,網絡層和業務層支持負載均衡策略、數據層支持分布式存儲;

    • 客戶端SDK的易用性:把網絡層、數據層分離、業務邏輯層分離;

    后臺架構簡化圖

    架構示意圖

    架構細化圖

    說明

    • 從< 架構細化圖>中可以看出對于上線服務由于建立的是TCP長連接,對于單臺服務器往往由于硬件資源、系統資源、網絡資源的限制無法做到海量用戶的同時 在線,所以設計為根據服務器負載支持多服務器上線,同時由于多服務器上線造成了對整個系統交互(不同的客戶端的交互,協作部門應用服務和客戶的交互)的分 割,引入消息轉發服務器作為粘合點。另外對于多服務器上線造成的統一賬戶信息(在線狀態,消息)數據的分割,引入統一的數據層(內存存儲 層:session、狀態信息存儲、消息隊列存儲;數據庫:賬號信息存儲)做到業務和數據的分離,也就做到了支持分布式部署。參見我的這篇文章《構建高性能服務的考量》

    • 對于部分業務服務:做到網絡層、業務層、數據層的完全分離。首先對于TCP短連接來說不會如長連接那般消耗資源,即使后期遇到海量的并發訪問請求依然可以從容的通過負載均衡策略和數據分布式部署策略進行解決。參見我的這篇文章《服務端架構中的“網關服務器”》

    服務端平臺及技術選型

    • 系統開發平臺: CentOS——Linux發行版的一種,穩定可靠、可定制優化、支持豐富;

    • 網絡支撐層: libevent——減小開發成本,增強穩定性;

    • 緩存存儲層: Redis——支持豐富的存儲結構,支持分布式存儲;

    • 數據庫: MySQL——最適合互聯網的數據庫,免授權、高效穩定、可控性高;

    • 開發語言: C/C++;

    部分熱點問題考量

    • 系統性能考量:

      • 編碼角度:采用高效的網絡模型,線程模型,I/O處理模型,合理的數據庫設計和操作語句的優化;

      • 垂直擴展:通過提高單服務器的硬件資源或者網絡資源來提高性能;

      • 水平擴展:通過合理的架構設計和運維方面的負載均衡策略將負載分擔,有效提高性能;后期甚至可以考慮加入數據緩存層,突破IO瓶頸;

    • 系統的高可用性:(防止單點故障)

      • 在架構設計時做到業務處理和數據的分離,從而依賴分布式的部署使得在單點故障時能保證系統可用。

      • 對于關鍵獨立節點可以采用雙機熱備技術進行切換。

      • 數據庫數據的安全性可以通過磁盤陣列的冗余配置和主備數據庫來解決。

    主要學習資料: 請自行google。

    • 《1.4億在線背后的故事》;

    • 《BasicDB的架構演變》;

    • 《微信之道-至簡》;

    本文出自51博客 “永遠的朋友” ,轉載請務必保留此出處http://yaocoder.blog.51cto.com/2668309/1412029

    posted @ 2017-05-15 14:22 小馬歌 閱讀(233) | 評論 (0)編輯 收藏
    僅列出標題
    共95頁: 上一頁 1 2 3 4 5 6 7 8 9 下一頁 Last 
     
    主站蜘蛛池模板: 99视频在线观看免费| 可以免费观看的国产视频| 亚洲午夜久久久久久久久久| 黄色网站软件app在线观看免费| 91亚洲国产成人精品下载| 日本免费的一级v一片| 中文字幕a∨在线乱码免费看 | 中字幕视频在线永久在线观看免费 | 国产精品免费久久久久电影网| 亚洲视频在线观看免费| 国产成人3p视频免费观看| 久久午夜夜伦鲁鲁片免费无码| 亚洲AV无码一区二区三区电影| 亚洲成a人片在线观看中文动漫| 好爽…又高潮了毛片免费看| 久久99久久成人免费播放| 亚洲成年网站在线观看| 亚洲精品无码不卡在线播放HE| 最近2019中文字幕mv免费看| a毛片免费观看完整| 国产区图片区小说区亚洲区| 亚洲福利一区二区精品秒拍| 久久久久亚洲精品无码网址| 成人黄软件网18免费下载成人黄18免费视频 | 亚洲ts人妖网站| 亚洲精品国产精品乱码在线观看| 永久免费看mv网站入口| 亚洲视频在线观看免费视频| 中文在线免费视频| 色婷婷亚洲一区二区三区| 亚洲精品伊人久久久久| 亚洲日本在线观看| 亚洲精品无码AV人在线播放| 日产国产精品亚洲系列| 成人a视频片在线观看免费| 51精品视频免费国产专区| 中国一级毛片免费看视频| 免费一区二区三区在线视频| 亚洲欧美国产国产一区二区三区 | 亚洲成aⅴ人片久青草影院| 亚洲人成网站免费播放|