您的位置:首页 > 编程语言 > Java开发

常见的JVM内存问题以及分析工具

2018-02-02 04:27 681 查看
内存溢出(OutOfMemory,简称OOM)主要是某一堆内存空间耗尽的时候会出现。导致出现OOM的原因有很多,这里总结一下常见的一些:包括堆溢出、直接内存溢出、永久区溢出等,并介绍一下常的堆分析工具。

1.栈溢出

java栈是一块线程私有的内存空间。如果说,java堆与程序数据密切相关,那java栈就与线程执行密切相关。线程的执行的基本行为是函数的调用,而每一次调用都是都是栈帧出入栈的过程,函数被调用时,都会有一个对应的栈帧被压入java栈,调用结束,栈帧被弹出。大家大概先了解一下这个过程,详细的java栈的 操作,我们以后再了解。就是由于每一次被调用都占用一定的栈空间,因此如果栈空间不足,则会抛出StackOverFlowError。

-Xss 可用于指定最大栈的空间 该参数直接决定的函数被调用的最大深度


/**
* 用于测试-Xss 测试栈深度 -Xss128K
* @author lhn
*
*/
public class TestSackDeep {

private static int count = 0 ;

/**
* 递归
*/
public static void add() {
count++;
add();
}
@Test
public void TestStack() {
try {
add();
} catch (Throwable e) {
System.err.println("最大深度为:"+count);
e.printStackTrace();
}
}

}

Console: -Xss128K
最大深度为:1045
java.lang.StackOverflowError
at worm.TestSackDeep.add(TestSackDeep.java:18)

Console: -Xss256K
最大深度为:3351
java.lang.StackOverflowError
at worm.TestSackDeep.add(TestSackDeep.java:19)


2.堆溢出

堆:在java中是对象的直接归宿。他就是一栋楼的一个个家庭,而引用就像是这些家庭的门牌号。因此当楼面积一定时,对象占有的空间就固定了,如果超出了,那就很空间堆溢出。而且现实中我们出现的大部分内存溢出,都是堆溢出。因为我们创建的大量对象占据了堆空间,而我们绝大部分时候都是用的强引用,在调用结束前是无法回收的。

-Xmx -Xms用于配置分配堆的大小


/**
* @author lhn
* 用于测试堆溢出 -Xmx512M -Xms512M
*/
public class TestHeapSpace {

@Test
public void SimpleHeapOOM() {

ArrayList<byte[]> list = new ArrayList<byte[]>();
for(int i=0;i<1024;i++) {
list.add(new byte[1024*1024]);
}
}
}
Console:
java.lang.OutOfMemoryError: Java heap space
at worm.TestHeapSpace.SimpleHeapOOM(TestHeapSpace.java:21)


Java heap space
表明是一个堆溢出。

解决一般方法:

1. 为了缓解我们可以扩大Xmx的大小。

2. 但堆是不可能无限增长,所以如果出现这种错误,就要通过MAT或者Visual VM分析原因,对程序进行优化并合理分配大小。

3.直接内存溢出

在java中很少有直接操作内存的,但在NIO中,支持直接内存的使用。也就是通过java代码,直接获取一块堆外的内存空间,该空间是直接向操作系统申请的。直接内存的分配一般要比堆内存慢,但在访问速度上要比堆内存快。因此,一般那些可以复用的,并且会被经常访问的空间,使用直接内存是可以提高系统性能的。但由于它没有被java虚拟机完全托管,使用不当,容易触发直接内存溢出,导致宕机.即便是Netty对NIO做了很多封装,也提供一些内存泄漏的监测机制。

/**
* @author lhn
* 用于测试直接内存的溢出
*/
public class DirectBufferOOM {

@Test
public void TestOOM() {
for(int i=0;i<1024;i++) {
ByteBuffer.allocateDirect(1024*1024);
System.err.println(i);
System.gc();
}
}
}


这个结果我本机没运行结果,要运行结果,最好在32位系统上,因为32位计算机系统对应用程序的可用最大内存有限制。如windows,32位系统中进程的寻址空间为4G,其中2G为用户空间,2G为系统空间。帮实际可用只有2G,很容易OOM,但要是64位就大了去了,大概是2^46B 即:64TB。

解决的一般方法:

1. 为避免直接内存溢出,可以合理的显示GC,可以降低直接内存溢出的概率。

2. 设置合理的-XX:MaxDirectMemorySize 并打开-XX:+DisableExplicitGC 。
-XX:MaxDirectMemorySize 此参数的含义是当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC  而看ByteBuffer.allocateDirect源码会发现,每次调用该方法时都会显示的调用 System.gc()


3. 如果有必要还是用Netty吧,它对NIO做了很多优化。

4.过多的线程导致OOM

每一个纯种的开启都要占用系统内存,线程很多时,也可能导致OOM。由于线程的栈空间也是堆外分配,因此和直接内存相似。如果想使用更多的线程,则要有一个较小的堆空间。

解决一般方法:

1. 如果出现会报
unable to create new native thread
也可以减少栈空间的大小-Xss但如果这个参数过小,就增大栈溢出的风险喽。

5、永久区溢出

永久区(Perm)是存放类元数据的区域。如果一个系统有太多的类型,永久区是有可能溢出的。不过在JDK1.8中,已经不存在永久区了,而是由元数据区替代了。功能相似。最常出现溢出的是动态生成新类

解决一般方法:

1. 增加MaxPermSize的值

2. 减少系统需要的类的数量。

3. 使用ClassLoader合理装载各个类,并定期进行回收。

6.GC效率低下引起的OOM

GC是内存回收的关键,如果GC效率低下,那么系统就会受到严重影响。如果系统的堆太小,那么GC所占的时间就会较多,并且回收所释放的内存就会较少。根据GC占用的系统时间,以及释放内存的大小,JVM会评估GC的效率,一旦认为GC效率过低,就会直接OOM。但JVM对效率过低的判断还是有一定的要求的。一般JVM会检查以下情况:

花在GC上的时间是否>98%

老年代释放的内存是否<2%

eden区释放的内在是否<2%

是事连续5次GC都出现以上情况(注:同时出现)

满足所有条件的话,就会抛出以下异常

java.lang.OutOfMemoryError: GC overhead limit exceeded


虽然要求严格,但在很多程序中还是会抛出堆溢出的。只是这个OOM只启辅助作用,用于提醒分配的堆可能太小,并不强制一定要开启这个错误。可以通过关闭开关
-XX:-USeGCOverheadLimit
来禁止这种OOM的产生。

7.内存泄漏

所属泄漏,并不是内存消失了,而是由于错误或者疏忽造成程序未能释放已不再使用的内存,从而导致可用内存越来越小,最终导致内存溢出。

最典型的是JDK1.6中的
String
他的很多方法都会导致内存泄漏,原因,就是他的组成:代表字符数组的Value,偏移量offset和长度count。也就是它的实际内容并不是只有value决定,而是这三部分共同决定,就就为泄漏埋下伏笔。不过在JDK1.7中已经去掉了offset和count。问题已经解决了。

虚拟机java堆分析工作 MAT

MAT(Memory Analyzer)的简称,是一个功能强大的Java堆内存分析器。可以查找内存泄漏以及查看内存消耗情况。MAT是基于eclipse开发的,可以直接在eclipse->help->Eclipse Marketplace直接搜索Memory Analyzer 导入就可

我们以下面的代码

/**
* @author lhn
* 用于测试堆溢出 -Xmx512M -Xms512M
*/
public class TestHeapSpace {

@Test
public void SimpleHeapOOM() {

ArrayList<byte[]> list = new ArrayList<byte[]>();
for(int i=0;i<1024;i++) {
list.add(new byte[1024*1024]);
}
}
}


-Xmx512m -Xms512m -XX:+HeapDumpOnOutOfMemoryError  -XX:HeapDumpPath=D:\dump
来获取内存快照文件

打开快照



整体运行界面说明如下



我们可以从多个方面找到我们内存溢出的痕迹。

首先我们可以从线程信息中查看



从中我们可以找到深堆最大的线程,并可以看到本线程中的局部变量,可以看到就是我们程序中ArrayList占用最大。

同时我们选中某一个对象可以查看对象的出入引用



入引用:引用当前对象的对象,效果如下:



出引用:该对象引用的对象



浅堆和深堆

浅堆(Shallow Heap)和深堆(Retained Heap)分别表示一个对象结构所占用的内存大小和一个对象被GC回收后,可以真实释放的内存大小。浅堆指对象本身占用的内存,不包括其内部引用对象的大小。一个对象的深堆指只能通过该对象访问到的直接或间接所有对象的浅堆之和。

浅堆是指一个对象所消耗的内存。在32位系统中,一个对象引用会占据4个字节,一个int类型会占据4个字节,long型变量会占据8个字节,每个对象头需要占用8个字节。

类型字节数
int4个字节
char2个字节
byte1个字节
short2个字节
long8个字节
float4个字节
double8个字节
根据堆快照模式不同,对象的大小可能会向8字节进行对齐。以String对象为例,在JDK1.7中

inthash320
inthash0
refvalue
2个int类型占8个字节,对象引用占4个字节,对象头8个字节。合计20字节,向8字节对齐,点24字节。

这24字节为String对象的浅堆大小。它与String的Value实际取值无关,无论字符串长度如何,浅堆大小始终是24字节。

深堆比较复杂。要理解深堆,先要了解保留集(Retained Set)。对象A的保留集指当对象A被垃圾回收后,可以被释放的所有的对象集合(包括对象A本身),即对象A的保留集可以被认为是只能通过对象A被直接或者间接访问到的所有对象的集合。通俗地说,就是指仅被对象A所持有的对象集合。深堆是指对象的保留集中所有对象的浅堆大小之和。

注意:深堆的大小与对象的实际大小不同。对象的实际大小为一个对象所有能触及的所有对象的浅堆大小之和,而这些对象不一定在GC一定回收,可能还被其它对象引用。这些对象所有实际大小中,但不归属于深堆大小中。

我们来看一下例子,来看一下堆中对象的情况

public class Student {
private int id;
private String name;
private List<WebPage> history = new Vector<WebPage>();
public Student(int id,String name) {
super();
this.id = id;
this.name = name;
}

public void visit(WebPage webPage) {
history.add(webPage);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<WebPage> getHistory() {
return history;
}
public void setHistory(List<WebPage> history) {
this.history = history;
}

}
public class WebPage {

private String url;
private String content;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}

}

public class TraceStudent {

static List<WebPage> webpages = new Vector<WebPage>();

public static void createWebPage() {
for(int i=0;i<100; i++) {
WebPage wp = new WebPage();
wp.setUrl("http://www."+Integer.toString(i)+".com");
wp.setContent(Integer.toString(i));
webpages.add(wp);
}
}
@Test
public void test() {
createWebPage();
Student st3 = new Student(3, "billy");
Student st5 = new Student(5, "alice");
Student st7 = new Student(7, "taotao");
for(int i=0;i<webpages.size();i++) {
if(i%st3.getId() == 0) {
st3.visit(webpages.get(i));
}
if(i%st5.getId() == 0) {
st5.visit(webpages.get(i));
}
if(i%st7.getId() == 0) {
st7.visit(webpages.get(i));
}
}
webpages.clear();
System.gc();
}
}


运行参数

-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=D:\dump\test


MAT打开快照



为了获得某一个对象的信息,可以在某一个对象上通过“出引用”(Outing References)查找,就可以找到该学生对象能触及的对象了。如下



然后我们再查看http://www.0.com对象都被谁引用了。选中该对象通过”入引用”(Incoming References)查找。

如下:



显示这个对象被三个对象引用着,下面我们算一下深堆。

如下:



可以看出该对象引用了数据有34条,每一条数据占大部分占152,而有两条144,这是因为如下原因



char类型数组多了一个char,会增加2字节,向8对齐 48+8=56

34条数据共为 152*30+4*144=5136字节 而elementData=3648.这是因为部分对象既被billy引用,又被其它引用。并不会计算在billy的深堆中,根据程序我们可以知道,只要能被3和5整除或者被3和7同时整除的都不应算在内,这包括0,15,21,30 ,42,45,60, 63,75,84,90 共 11个 144+152*10=1664 5136-1664+176= 3648 其中176为elementData本身的浅堆。由于elementData数组长40,每一个引用为4个字节,合许4*40=80字节,数组对象头8字节,数组长度点4字节,合计 4*40+8+4=172字节 向8字节对齐为176字节。

待续。。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息