熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

java內存洩漏的定位與分析

2022-06-13   來源: Java核心技術 

  ArrayList就是傳說中的動態數組就是Array的復雜版本它提供了如下一些好處動態的增加和減少元素靈活的設置數組的大小……

  認真閱讀本文我相信一定會對你有幫助比如為什麼ArrayList裡面提供了一個受保護的removeRange方法?提供了其他沒有被調用過的私有方法?

  首先看到對ArrayList的定義

  [java]

  public class ArrayList<E> extends AbstractList<E>  implements List<E> RandomAccess Cloneable javaioSerializable

  從ArrayList<E>可以看出它是支持泛型的它繼承自AbstractList實現了ListRandomAccessCloneablejavaioSerializable接口

  AbstractList提供了List接口的默認實現(個別方法為抽象方法)

  List接口定義了列表必須實現的方法

  RandomAccess是一個標記接口接口內沒有定義任何內容

  實現了Cloneable接口的類可以調用Objectclone方法返回該對象的淺拷貝

  通過實現 javaioSerializable 接口以啟用其序列化功能未實現此接口的類將無法使其任何狀態序列化或反序列化序列化接口沒有方法或字段僅用於標識可序列化的語義

  ArrayList的屬性

  ArrayList定義只定義類兩個私有屬性

  [java]

  /**

  * The array buffer into which the elements of the ArrayList are stored

  * The capacity of the ArrayList is the length of this array buffer

  */

  private transient Object[] elementData;

  /**

  * The size of the ArrayList (the number of elements it contains)

  *

  * @serial

  */

  private int size;

  [java]

  很容易理解elementData存儲ArrayList內的元素size表示它包含的元素的數量

  有個關鍵字需要解釋transient

  Java的serialization提供了一種持久化對象實例的機制當持久化對象時可能有一個特殊的對象數據成員我們不想用serialization機制來保存它為了在一個特定對象的一個域上關閉serialization可以在這個域前加上關鍵字transient

  ansient是Java語言的關鍵字用來表示一個域不是該對象串行化的一部分當一個對象被串行化的時候transient型變量的值不包括在串行化的表示中然而非transient型的變量是被包括進去的

  有點抽象看個例子應該能明白

  [java]

  public class UserInfo implements Serializable {

  private static final long serialVersionUID = L;

  private String name;

  private transient String psw;

  public UserInfo(String name String psw) {

  thisname = name;

  thispsw = psw;

  }

  public String toString() {

  return name= + name + psw= + psw;

  }

  }

  public class TestTransient {

  public static void main(String[] args) {

  UserInfo userInfo = new UserInfo(張三

  Systemoutprintln(userInfo)

  try {

  // 序列化被設置為transient的屬性沒有被序列化

  ObjectOutputStream o = new ObjectOutputStream(new FileOutputStream(

  UserInfoout))

  owriteObject(userInfo)

  oclose()

  } catch (Exception e) {

  // TODO: handle exception

  eprintStackTrace()

  }

  try {

  // 重新讀取內容

  ObjectInputStream in = new ObjectInputStream(new FileInputStream(

  UserInfoout))

  UserInfo readUserInfo = (UserInfo) inreadObject()

  //讀取後psw的內容為null

  Systemoutprintln(readUserInfotoString())

  } catch (Exception e) {

  // TODO: handle exception

  eprintStackTrace()

  }

  }

  }

  被標記為transient的屬性在對象被序列化的時候不會被保存

  

  接著回到ArrayList的分析中……

  ArrayList的構造方法

  看完屬性看構造方法ArrayList提供了三個構造方法

  [java]

  /**

  * Constructs an empty list with the specified initial capacity

  */

  public ArrayList(int initialCapacity) {

  super()

  if (initialCapacity <

  throw new IllegalArgumentException(Illegal Capacity: +

  initialCapacity)

  thiselementData = new Object[initialCapacity];

  }

  /**

  * Constructs an empty list with an initial capacity of ten

  */

  public ArrayList() {

  this(

  }

  /**

  * Constructs a list containing the elements of the specified

  * collection in the order they are returned by the collections

  * iterator

  */

  public ArrayList(Collection<? extends E> c) {

  elementData = ctoArray()

  size = elementDatalength;

  // ctoArray might (incorrectly) not return Object[] (see

  if (elementDatagetClass() != Object[]class)

  elementData = pyOf(elementData size Object[]class)

  }

  第一個構造方法使用提供的initialCapacity來初始化elementData數組的大小第二個構造方法調用第一個構造方法並傳入參數即默認elementData數組的大小為第三個構造方法則將提供的集合轉成數組返回給elementData(返回若不是Object[]將調用pyOf方法將其轉為Object[])

  ArrayList的其他方法

  add(E e)

  add(E e)都知道是在尾部添加一個元素如何實現的呢?

  [java]

  public boolean add(E e) {

  ensureCapacity(size +   // Increments modCount!!

  elementData[size++] = e;

  return true;

  }

  書上都說ArrayList是基於數組實現的屬性中也看到了數組具體是怎麼實現的呢?比如就這個添加元素的方法如果數組大則在將某個位置的值設置為指定元素即可如果數組容量不夠了呢?

  看到add(E e)中先調用了ensureCapacity(size+)方法之後將元素的索引賦給elementData[size]而後size自增例如初次添加時size為add將elementData[]賦值為e然後size設置為(類似執行以下兩條語句elementData[]=e;size=將元素的索引賦給elementData[size]不是會出現數組越界的情況嗎?這裡關鍵就在ensureCapacity(size+)中了

  根據ensureCapacity的方法名可以知道是確保容量用的ensureCapacity(size+)後面的注釋可以明白是增加modCount的值(加了倆感歎號應該蠻重要的來看看)

  [java]

  /**

  * Increases the capacity of this <tt>ArrayList</tt> instance if

  * necessary to ensure that it can hold at least the number of elements

  * specified by the minimum capacity argument

  *

  * @param   minCapacity   the desired minimum capacity

  */

  public void ensureCapacity(int minCapacity) {

  modCount++;

  int oldCapacity = elementDatalength;

  if (minCapacity > oldCapacity) {

  Object oldData[] = elementData;

  int newCapacity = (oldCapacity * )/ + ;

  if (newCapacity < minCapacity)

  newCapacity = minCapacity;

  // minCapacity is usually close to size so this is a win:

  elementData = pyOf(elementData newCapacity)

  }

  }

  The number of times this list has been structurally modified

  這是對modCount的解釋意為記錄list結構被改變的次數(觀察源碼可以發現每次調用ensureCapacoty方法modCount的值都將增加但未必數組結構會改變所以感覺對modCount的解釋不是很到位)

  增加modCount之後判斷minCapacity(即size+)是否大於oldCapacity(即elementDatalength)若大於則調整容量為max((oldCapacity*)/+minCapacity)調整elementData容量為新的容量即將返回一個內容為原數組元素大小為新容量的數組賦給elementData;否則不做操作

  所以調用ensureCapacity至少將elementData的容量增加的所以elementData[size]不會出現越界的情況

  容量的拓展將導致數組元素的復制多次拓展容量將執行多次整個數組內容的復制若提前能大致判斷list的長度調用ensureCapacity調整容量將有效的提高運行速度

  可以理解提前分配好空間可以提高運行速度但是測試發現提高的並不是很大而且若list原本數據量就不會很大效果將更不明顯

  add(int index E element)

  add(int indexE element)在指定位置插入元素

  [java]

  public void add(int index E element) {

  if (index > size || index <

  throw new IndexOutOfBoundsException(

  Index: +index+ Size: +size)

  ensureCapacity(size+  // Increments modCount!!

  Systemarraycopy(elementData index elementData index +

  size index)

  elementData[index] = element;

  size++;

  }

  首先判斷指定位置index是否超出elementData的界限之後調用ensureCapacity調整容量(若容量足夠則不會拓展)調用Systemarraycopy將elementData從index開始的sizeindex個元素復制到index+至size+的位置(即index開始的元素都向後移動一個位置)然後將index位置的值指向element

  addAll(Collection<? extends E> c)

  [java]

  public boolean addAll(Collection<? extends E> c) {

  Object[] a = ctoArray()

  int numNew = alength;

  ensureCapacity(size + numNew)  // Increments modCount

  Systemarraycopy(a elementData size numNew)

  size += numNew;

  return numNew != ;

  }

  先將集合c轉換成數組根據轉換後數組的程度和ArrayList的size拓展容量之後調用Systemarraycopy方法復制元素到elementData的尾部調整size根據返回的內容分析只要集合c的大小不為空即轉換後的數組長度不為則返回true

  

  addAll(int indexCollection<? extends E> c)

  [java]

  public boolean addAll(int index Collection<? extends E> c) {

  if (index > size || index <

  throw new IndexOutOfBoundsException(

  Index: + index + Size: + size)

  Object[] a = ctoArray()

  int numNew = alength;

  ensureCapacity(size + numNew)  // Increments modCount

  int numMoved = size index;

  if (numMoved >

  Systemarraycopy(elementData index elementData index + numNew

  numMoved)

  Systemarraycopy(a elementData index numNew)

  size += numNew;

  return numNew != ;

  }

  先判斷index是否越界其他內容與addAll(Collection<? extends E> c)基本一致只是復制的時候先將index開始的元素向後移動X(c轉為數組後的長度)個位置(也是一個復制的過程)之後將數組內容復制到elementData的index位置至index+X

  clear()

  [java]

  public void clear() {

  modCount++;

  // Let gc do its work

  for (int i = ; i < size; i++)

  elementData[i] = null;

  size = ;

  }

  clear的時候並沒有修改elementData的長度(好不容易申請拓展來的憑什麼釋放留著搞不好還有用呢這使得確定不再修改list內容之後最好調用trimToSize來釋放掉一些空間)只是將所有元素置為nullsize設置為

  clone()

  返回此 ArrayList 實例的淺表副本(不復制這些元素本身

  [java]

  public Object clone() {

  try {

  ArrayList<E> v = (ArrayList<E>) superclone()

  velementData = pyOf(elementData size)

  vmodCount = ;

  return v;

  } catch (CloneNotSupportedException e) {

  // this shouldnt happen since we are Cloneable

  throw new InternalError()

  }

  }

  調用父類的clone方法返回一個對象的副本將返回對象的elementData數組的內容賦值為原對象elementData數組的內容將副本的modCount設置為

  contains(Object)

  [html]

  public boolean contains(Object o) {

  return indexOf(o) >= ;

  }

  indexOf方法返回值與比較來判斷對象是否在list中接著看indexOf

  indexOf(Object)

  [java]

  public int indexOf(Object o) {

  if (o == null) {

  for (int i = ; i < size; i++)

  if (elementData[i]==null)

  return i;

  } else {

  for (int i = ; i < size; i++)

  if (oequals(elementData[i]))

  return i;

  }

  return ;

  }

  通過遍歷elementData數組來判斷對象是否在list中若存在返回index([size])若不存在則返回所以contains方法可以通過indexOf(Object)方法的返回值來判斷對象是否被包含在list中

  既然看了indexOf(Object)方法接著就看lastIndexOf光看名字應該就明白了返回的是傳入對象在elementData數組中最後出現的index值

  [java]

  public int lastIndexOf(Object o) {

  if (o == null) {

  for (int i = size; i >= ; i

  if (elementData[i]==null)

  return i;

  } else {

  for (int i = size; i >= ; i

  if (oequals(elementData[i]))

  return i;

  }

  return ;

  }

  采用了從後向前遍歷element數組若遇到Object則返回index值若沒有遇到返回

  get(int index)

  這個方法看著很簡單應該是返回elementData[index]就完了

  [java]

  public E get(int index) {

  RangeCheck(index)

  return (E) elementData[index];

  }

  但看代碼的時候看到調用了RangeCheck方法而且還是大寫的方法看看究竟有什麼內容吧

  [java]

  /**

  * Checks if the given index is in range

  */

  private void RangeCheck(int index) {

  if (index >= size)

  throw new IndexOutOfBoundsException(

  Index: +index+ Size: +size)

  }

  就是檢查一下是不是超出數組界限了超出了就拋出IndexOutBoundsException異常為什麼要大寫呢???

  isEmpty()

  直接返回size是否等於

  remove(int index)

  [java]

  public E remove(int index) {

  RangeCheck(index)

  modCount++;

  E oldValue = (E) elementData[index];

  int numMoved = size index ;

  if (numMoved >

  Systemarraycopy(elementData index+ elementData index

  numMoved)

  elementData[size] = null; // Let gc do its work

  return oldValue;

  }

  首先是檢查范圍修改modCount保留將要被移除的元素將移除位置之後的元素向前挪動一個位置將list末尾元素置空(null)返回被移除的元素

  remove(Object o)

  [java]

  public boolean remove(Object o) {

  if (o == null) {

  for (int index = ; index < size; index++)

  if (elementData[index] == null) {

  fastRemove(index)

  return true;

  }

  } else {

  for (int index = ; index < size; index++)

  if (oequals(elementData[index])) {

  fastRemove(index)

  return true;

  }

  }

  return false;

  }

  首先通過代碼可以看到當移除成功後返回true否則返回falseremove(Object o)中通過遍歷element尋找是否存在傳入對象一旦找到就調用fastRemove移除對象為什麼找到了元素就知道了index不通過remove(index)來移除元素呢?因為fastRemove跳過了判斷邊界的處理因為找到元素就相當於確定了index不會超過邊界而且fastRemove並不返回被移除的元素下面是fastRemove的代碼基本和remove(index)一致

  [java]

  private void fastRemove(int index) {

  modCount++;

  int numMoved = size index ;

  if (numMoved >

  Systemarraycopy(elementData index+ elementData index

  numMoved)

  elementData[size] = null; // Let gc do its work

  }

  removeRange(int fromIndexint toIndex)

  [java]

  protected void removeRange(int fromIndex int toIndex) {

  modCount++;

  int numMoved = size toIndex;

  Systemarraycopy(elementData toIndex elementData fromIndex

  numMoved)

  // Let gc do its work

  int newSize = size (toIndexfromIndex)

  while (size != newSize)

  elementData[size] = null;

  }

  執行過程是將elementData從toIndex位置開始的元素向前移動到fromIndex然後將toIndex位置之後的元素全部置空順便修改size

  這個方法是protected及受保護的方法為什麼這個方法被定義為protected呢?

  這是一個解釋但是可能不容易看明白isjavasabstractlistsremoverangemethodprotected

  

  先看下面這個例子

  [java]

  ArrayList<Integer> ints = new ArrayList<Integer>(ArraysasList(

   ))

  // fromIndex low endpoint (inclusive) of the subList

  // toIndex high endpoint (exclusive) of the subList

  intssubList( clear()

  Systemoutprintln(ints)

  輸出結果是[ ]結果是不是像調用了removeRange(int fromIndexint toIndex)!哈哈哈就是這樣的但是為什麼效果相同呢?是不是調用了removeRange(int fromIndexint toIndex)呢?

  set(int indexE element)

  [java]

  public E set(int index E element) {

  RangeCheck(index)

  E oldValue = (E) elementData[index];

  elementData[index] = element;

  return oldValue;

  }

  首先檢查范圍用新元素替換舊元素並返回舊元素

  size()

  size()方法直接返回size

  toArray()

  [java]

  public Object[] toArray() {

  return pyOf(elementData size)

  }

  調用pyOf將返回一個數組數組內容是size個elementData的元素即拷貝elementData從至size位置的元素到新數組並返回

  toArray(T[] a)

  [java]

  public <T> T[] toArray(T[] a) {

  if (alength < size)

  // Make a new array of as runtime type but my contents:

  return (T[]) pyOf(elementData size agetClass())

  Systemarraycopy(elementData a size)

  if (alength > size)

  a[size] = null;

  return a;

  }

  如果傳入數組的長度小於size返回一個新的數組大小為size類型與傳入數組相同所傳入數組長度與size相等則將elementData復制到傳入數組中並返回傳入的數組若傳入數組長度大於size除了復制elementData外還將把返回數組的第size個元素置為空

  trimToSize()

  [java]

  public void trimToSize() {

  modCount++;

  int oldCapacity = elementDatalength;

  if (size < oldCapacity) {

  elementData = pyOf(elementData size)

  }

  }

  由於elementData的長度會被拓展size標記的是其中包含的元素的個數所以會出現size很小但elementDatalength很大的情況將出現空間的浪費trimToSize將返回一個新的數組給elementData元素內容保持不變length很size相同節省空間

  學習Java最好的方式還必須是讀源碼讀完源碼你才會發現這東西為什麼是這麼玩的有哪些限制關鍵點在哪裡等等而且這些源碼都是大牛們寫的你能從中學習到很多



如果有大量的FGC就要查詢是否有內存洩漏的問題了圖中的FGC數量就比較大並且執行時間較長這樣就會導致系統的響應時間較長如果對jvm的內存設置較大那麼執行一次FGC的時間可能會更長
如果為了更好的證明FGC對服務器性能的影響我們可以使用java visualVM來查看一下

從上圖可以發現執行FGC的情況下午:分之前是沒有FGC的之後出現大量的FGC
上圖是jvm堆內存的使用情況下午:分之前的內存回收還是比較合理但是之後大量內存無法回收最後導致內存越來越少導致大量的full gc
下面我們在看看大量full GC 對服務器性能的影響下面是我用loadrunner對我們項目進行壓力測試相應時間的截圖

從圖中可以發現有在進行full GC 後系統的相應時間有了明顯的增加點擊率和吞吐量也有了明顯的下降所以java內存洩漏對系統性能的影響是不可忽視的


 
定位內存洩漏
當然通過上面幾種方法我們可以發現java的內存洩漏問題但是作為一名合格的高級工程師肯定不甘心就把這樣的結論交給開發當然這也的結論交給開發開發也很難定位問題為了更好的提供自己在公司的地位我們必須給開發工程師提供更深入的測試結論下面就來認識一下MemoryAnalyzerexejava內存洩漏檢查工具利器
首先我們必須對jvm的堆內存進行dump只有拿到這個文件我們才能分析出jvm堆內存中到底存了些什麼內容到底在做什麼?
MemoryAnalyzer的用戶我在這裡就不一一說明了我的博客裡也有說明下面就展示我測試的成功圖

其中深藍色的部分就為內存洩漏的部分java的堆內存一共只有M而內存洩漏的部分獨自占有了M所以本次的內存洩漏很明顯那麼我就來看看那個方法導致的內存洩漏

從上圖我們可以發現紅線圈著的方法占用了堆內存的%如果能把這個測試結果交給開發開發是不是應該很好定位呢所以作為一名高級測試工程師我們需要學習的東西太多
本人剛剛完成了雲存儲架構師的培訓學習(包括了linux的內核了解 shell的高級編程linux安全的學習重點iptables和tcp/ip等各種協議的抓包分析linux的集群性能調優等接下來還有dba的課程等待著我挑戰)
 
 


From:http://tw.wingwit.com/Article/program/Java/hx/201311/26553.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.