java

位置:IT落伍者 >> java >> 浏览文章

java内存洩漏的定位与分析


发布日期:2020年05月17日
 
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的课程等待着我挑战)

               

上一篇:研究Java的反编译性

下一篇:Java设计模式-----Visitor访问者模式