java特种兵读书笔记(3-5)——java程序员的OS之OOM

HeapSize OOM


public static void main(String[] args) {
List<String> list = Lists.newArrayList();
while (true) {
list.add("hello");
}
}

会抛如下异常

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:213)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:187)
at java.util.ArrayList.add(ArrayList.java:411)
at oscar.test.oom.TOom.main(TOom.java:17)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:601)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

这是java堆空间的溢出,也就是说Old区剩余的内存,已经无法满足要晋升到Old区对象的大小了。

OOM原因


代码问题:

一般内存泄露不会这么显而易见。

可能有很多对象有较长的生命周期,或者在全局区域增加一条数据导致隐藏的数据膨胀,或者死循环写入数据。这种时候就需要改代码。

其他问题:

由于并发导致内存无法被GC,或者说很多对象还有引用,即对象所在声明周期内的代码还没有执行完。这时候想到的方法就是“提速”。代码提速了,相同的对象声明周期会更加短暂,会很快被当做垃圾回收。

强制赋值null:

如果一个占据很大内存的对象,在后续的声明周期中没有用途,但后续声明周期依然很长(比如后面会做一些IO操作),这时可以用Object=null来帮助GC。

不提倡大部分代码都这样写!当方法脱离作用域后,相应的局部变量会自动被注销掉,只有它引用的对象后续有较长的声明周期,且对象占用空间较大时(达到KB级别),才有必要这样做,否则代码很不干净。

public void method() {

List<String> list = getList();//该方法返回一个超级大的list

list = null;//后面对list没有任何操作了,而且后面有个操作时间很长,这时候帮助gc

// do something

Thread.sleep(10000);

// 如果不帮助gc,当该方法脱离作用域后,该list局部变量才会被自动注销,多存在了10秒钟

}

配置:

当代码无法优化,程序跑的很快时候还发生OOM,这时就考虑改配置。最需要改的是堆的大小,将堆的空间设置的最后大,来承载更多时间单位内并发所用到的内存区域。

这个需要一个平衡,很大的内存不容易GC,但是如果很大的内存发生GC会很恐怖。

不停GC,GC很慢


这个时候的内存泄露很难发现。

因为绝大多数对象是活着的,GC过程标记活着的对象很耗时。对压缩环节来说,因为存在大量存活对象,空隙变得很小,压缩时间会变长。

这个时候系统表现出来的是,不停做FullGC,每次GC释放一点点内存,马上又满了,不断反复。当次数达到一定量,并且平均FullGC时间达到一定比例,会报错:OutOfMemory:GC over head limite exceed。

Sun 官方对此的定义是:"并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。"

这个时候不是真正的OOM,而是提前抛出异常,防止OOM发生。

举例:Session

Tomcat的session问题。程序中将一个session保存的一个全局的ConcurrentHashMap。由于外部有大量的HttpClient访问创建的临时session,这些httpclient程序在访问时并没有保存sessionKey和cookie,导致每次请求都以为以前没有创建过session,都会重新创建session。session的创建时发生在request.getSession()方法调用时,这些操作在一些框架中也会隐藏创建,虽然这些session很小,但是积少成多,积土成山。

OOM的后果


OOM不一定会宕机。大多OOM都是静态变量指向了一个只增不减的对象,比如一个HashMap。但有时候OOM不一定是这么来的。

如果一个局部变量引发的OOM,可以try catch住,而不是抛到容器顶层来处理,这样可以让进程继续存在。当抛出的错误在方法外捕获的时候,局部变量的作用域其实已经脱离,如果发生一次FullGC,内存可以释放掉,系统可以照常运行,比如:

public static void testOOM() {
try {
List<String> list = Lists.newArrayList();
while (true) {
list.add("hello");
}
catch (Throwable e) {
System.out.println("message:" + e.getMessage());
}
}

但是这样的话,不容易发现问题,只有这段代码引发的错误被大家知道,才会去跟进,不如OOM宕机能够更加直接的让人认识到问题的严重性。

而且,这种情况的OOM,用jmap将内存dump出来的时候,或者内存溢出时dump出来看到的内存情况可能很正常,因为在dump的时候会先做一次FullGC,这个时候局域变量脱离了作用域,就没办法捕获它了。

PermGen OOM


永久代存放的东西有Class和一些常量。例如:

public static void testGenOOM() {
List<String> list = Lists.newArrayList();
int i = 0;
while (true) {
list.add(("hello1234567890-qwertyuioasdfghjkl;zxcvbnm,.!@#$%^&*()" + (i++)).intern());
}
}

这样就会导致Perm的OOM,OutOfMemoryError,PermGen space。

如果把List去掉,只是不停的intern(),则不会OOM。但是并不是说这样就没有问题了,因为这时候系统在不停的做FullGC(用-XX:PrintGCDetail就可以看到)。FullGC会回收常量池中的内容,这也是FullGC的原因之一。

JDK1.7的String常量已经不放在PermGen区了。

Class的加载也可能导致PermGen的OOM。注意这里Class并没有被卸载,而是导致内存溢出。

因为Class的卸载条件非常苛刻,只有这个Class所对应的的ClassLoader下的所有Class(包括其它类的Class)都没有活着的对象引用,才会被卸载。

如果每次CGlib动态创建时,都重新给它设置一个ClassLoader,这个ClassLoader只加载这个类的话,且加载之后这个类很快就没有引用了,这样就不会OOM了。

所以:如果想要动态加载一些类,或者动态编译一段java代码,最好有一个单独的ClassLoader,这样如果Class发生变化或者被替换,原先的Class就可以被当做垃圾释放了。

DirectBuffer OOM


java的普通IO采用输入输出流来实现。

输入流:终端->直接内存->JVM。输出流:JVM->直接内存->终端。

这期间有多次Kernel与JVM之间的内存拷贝。有时为了提高速度,会想办法利用直接内存

Java中有一块区域叫做DirectBuffer,它不是Java Heap的一部分,而是C Heap的一部分。它有大小限制,在FullGC的时候会回收,该区域使用不当会有“大坑”。

StackOverFlow Error


程序运行过程中,方法分配时会分配栈帧(Frame)来存放本地变量,后进先出栈,PC寄存器等信息,如果方法嵌套或者递归调用,在不断分派过程中会占用十分多的空间。

Java为了控制线程栈无休止的增长(防止死递归的出现),就会设置一个私有栈空间大小,量级大概是256KB~1MB。线程私有栈所占用的不再是堆内存,通常叫做Native Memory。若使用空间超过限制,就会出现StackOverFlowError。

如果需要使用递归解决问题,一定要设置好递归调用层数,或者设置好递归结束的条件。

死递归与死循环不同。死循环是类似于while(true),不会造成栈空间的递增。而死递归需要记录退回的路径,就必须记住递推过程中的方法调用过程,以及每个方法运行过程中的本地变量,我们称之为上下文信息。

随着内容增加,就会占用很多内存空间,防止其无限制增长,做了安全处理。

举例:

子类调用父类方法(为了复用),父类再直接或者间接调用子类方法(多态),因为某种因素形成了一个环,变成了死递归。

定位StackOverFlow很容易,线程栈信息明确给出了调用路径。

其他内存溢出现象


unnable to create new native thread

栈本身是占用空间的,只是它占用的是native memory。每个私有栈空间都有大小限制,每一个线程对应它自己的栈空间。如果反复申请了大量线程并让它们处于运行状态,那么这些线程所占用的native memory空间就会很多。

如果物理内存不够,或者操作系统限制了单个进程使用的最大内存,那么当有“大量”线程分配的时候,可能会抛出异常:unnable to create new native thread。它表示无法分配本地内存。

request {} byte for {} out of swap

地址空间开始不够用了。不一定是物理地址,还有swap,显卡,网卡等等。

IOException:too many open files

有太多没有关闭的文件或者套接字

还有一些其他的比如JNI调用问题,Swap与内存频繁交互导致系统假死,JVM内核bug导致进程crash掉(此时还需要看crash日志)。

遇到OOM时,可以设定HeapDumpOnOutOfMemoryError参数,设定参数之后,JVM会在OOM时dump出一份内存的二进制文件,该文件可以用MAT工具来分析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值