Heap和Method Area是共享的,其他都是私有的
为什么不建议在程序中显式的生命System.gc()?
public class HeapOOM {
public static void main(String []args) {
Listlist = new ArrayList();
while(true) {
list.add("内存溢出呀,内存溢出呀!");
}
}
}
运行不久,就会报错,并看到这样的关键字
这就是java堆空间的溢出,也就是说,Old区域剩余的内存,已经无法满足将要晋升到Old区域的对象大小,此时就会报出这样的错误。
这句话要好好理解,不是说Heap没有内存了,是说新申请内存的对象大于Heap空闲内存,比如现在Heap还空闲1M,但是新申请的内存需要1.1M,于是就会报OutOfMemory了,可能以后的对象申请的内存都只要0.9M,于是就只出现一次OutOfMemory,GC也正常了,看起来像偶发事件,就是这么回事。
年老代堆空间被占满
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3210)
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:261)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
at java.util.ArrayList.add(ArrayList.java:458)
at com.doordu.soa.service.comm.HeapOOM.main(HeapOOM.java:10)
当程序运行几天后,发现系统超级慢,系统不断在做FULL GC,且每次做FULL GC的时间非常长,为何?因为绝大部分的内存对象都是活着的,所以在GC过程中标记活着的对象所需要的时间也很长。相应的,在GC过程中压缩环节由于存在大量存活的对象,所以空隙变得更小,那么压缩的时间也会变长。这个时候,系统所表现出来的状态是不断在做FULL GC,每次GC完后释放一点点内存,然后一下子就又满了,不断反复,当次数达到一定量,并且平均FULL GC时间达到一定比例时,就会报错
这种现场很难模拟,但是通常在发生这种现象前,系统会变得奇慢无比(值得注意的是,系统奇慢无比的原因不止这一种)
下面是一段不断做FULL GC的代码
public class GCOverHead {
/*这里先占用掉Old区超过14M的空间*/
public final static byte[]DEFAULT_BYTES = new byte[12 * 1024 * 1024];
public static void main(String []args) {
List<byte[]>temp = new ArrayList<byte[]>();
while(true) {
temp.add(new byte[1024 * 1024]);
if(temp.size() > 3) {
temp.clear();
}
}
}
}
这个很常见,检查应用或调整堆内存大小。
永久代(PermGen space)是JVM实现方法区的地方,因此该异常主要设计到方法区和方法区中的常量池。永久代存放的东西有class和一些常量。perm是放永久区的。如果一个系统定义了太多的类型,那永久区可能会溢出。jdk1.8中,被称为元数据区。
Perm空间被占满。无法为新的class分配存储空间而引发的异常。这个异常以前是没有的,但是在Java反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致Perm区被占满。
更可怕的是,不同的classLoader即便使用了相同的类,但是都会对其进行加载,相当于同一个东西,如果有N个classLoader那么他将会被加载N次。因此,某些情况下,这个问题基本视为无解。当然,存在大量classLoader和大量反射类的情况其实也不多。
string常量对象会在常量池(包含类名,方法名,属性名等信息)中以hash方式存储和访问,hash表默认的大小为1009,当string过多时,可以通过修改-xx:stringtableSize参数来增加Hash元素的个数,减少Hash冲突。
当常量池需要的空间大于常量池的实际空间时,也会抛出OutOfMemoryError: PermGen space异常。
例如,Java中字符串常量是放在常量池中的,String.intern()这个方法运行的时候,会检查常量池中是否存和本字符串相等的对象,如果存在直接返回对常量池中对象的引用,不存在的话,先把此字符串加入常量池,然后再返回字符串的引用。那么可以通过String.intern方法来模拟一下运行时常量区的溢出.
由于class被卸载的条件十分的苛刻,这个class所对应的classLoader下面所有的class都没有活对象的应用才会被卸载。
方法区(Method Area)不仅包含常量池,而且还保存了所有已加载类的元信息。当加载的类过多,方法区放不下所有已加载的元信息时,就会抛出OutOfMemoryError: PermGen space异常。主要有以下场景:
使用一些应用服务器的热部署的时候,会遇到热部署几次以后发现内存溢出了,这种情况就是因为每次热部署的后,原来的class没有被卸载掉。
如果应用程序本身比较大,涉及的类库比较多,但分给永久代的内存(-XX:PermSize和-XX:MaxPermSize来设置)比较小的时候也可能出现此种问题。
解决方法:在每次CGlib动态创建时,都重新给它设置一个classLoader,这样在运行代码就不会出现OOM,会发现大量的class被卸载。
VisualVm工具,查看PermGen标签页、类加载标签页中的趋势。随着类装载的数量增加,最终会出现了java.lang.OutOfMemoryError: PermGen space。
示例:如果不断产生新类,而没有回收,那最终很可能会导致永久区溢出。
解决的话从几方面入手:
● 增加MaxPermSize
● 减少系统需要的类数量
● 使用classloader合理的装载各个类,并定期进行回收
分配给Java虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给Java虚拟机的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss来减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。
解决:
说明:一般就是递归没返回,或者循环调用造成
因为一个线程把Stack内存全部耗尽了,一般是递归函数造成的
说明:java中一个线程的空间大小是有限制的。JDK5.0以后这个值是1M。与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部分
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
-XX:MaxPermSize=n:设置持久代大小
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
前段时间系统经常出现OOM,服务很不稳定,偶尔会有java进程不存在的情况,临时解决方案只能是重启。
用top查看,发现内存占用(%MEM)挺多,其他指标均正常。
如果发现自己的java进程突然消失了,那么就要借助dmesg来查看开机之后的系统日志
命令为dmesg | grep -i ‘kill’或者搜索oom(out of memory),如果能搜索到相关信息,则说明java进程是被操作系统kill了,操作系统有一种机制,它会在机器的内存耗尽前,挑选几个占用内存较大的进程杀死(实际也是有一定的计算规则),通常被杀死的就是java进程,那么接下来就是看看是什么原因造成内存这么大。dmesg 输出的格式不易查看,可以通过命令进行转换。date -d “1970-01-01 UTC echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+12288812.926194"|bc
seconds”
OOM的原因一般为内存泄露,创建了对象不能释放,也有可能是突然间创建了大对象,有时加载过多的class也是原因。线上遇到OOM需要做两件事情第一个是dump内存,第二个是看下GC日志。
找出当前java进程号1234
linux环境下可能要先执行
export JAVA_HOME=//
export PATH= JAVAHOME/bin: J A V A H O M E / b i n : PATH
从这一步查出,full gc次数频繁,由此可见原因是老年代空间不足
接下来就是排查问题最重要的一步,dump内存最容易想到的是
jmap -dump:format=b,file=heap.hprof 1234。
注意如果用jmap来dump的话,一来非常慢,二来可能会出异常,在linux JDK1.6某个版本里使用jmap可能会让系统挂掉,可以通过-d64来解决(jmap -J-d64 -dump:format=b,file=dump.bin PID)。而jdk7的某个版本则会抛出异常,这是jdk的bug.
一般dump下来的内存有几个G,而有时候dump下来只有一两百兆,说明jmap有问题,需要多执行几次jmap -dump才能得出正常结果,这个时候可以选用
gcore 把整个内存dump出来,然后再使用jmap把core dump转换成heap dump。
做法就是用gcore 1234命令来生成c版的core文件,再用命令jmap -dump:format=b,file=heap.hprof /bin/java core.1234.
在用eclipse分析dump文件时有可能因为文件过大而报异常,这时要调大eclipse本身占用的堆内存大小,在eclipse.ini文件里面,因为eclipse本身也是一个java程序。
通过自动dump下来的内存文件很快发现有一个对象占用内存非常大,解决后系统恢复正常。
排查OOM通常要结合tomcat的日志、gc日志来查看。如果没有任何JVM参数设置,gc日志默认打印在stdout.log文件里,里面可能会打其他的日志,而且GC日志也不会输出时间,所以在JVM启动参数里最好加以下命令,规范下GC日志输出到/home/admin/logs/gc.log,并且打印GC时间。
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/admin/logs
-Xloggc:/home/admin/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps