java内存区域与内存溢出异常(内存泄漏)

Java内存管理模型-运行时数据区域

 

  • 程序计数器

一块较小的内存空间,可以看作当前线程所执行的字节码行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能回到正确的位置,每条线程都有独立的程序计数器,这类内存为“线程私有”内存。

 

  • java虚拟机栈

与程序计数器一样,java虚拟机栈也是线程私有的,生命周期与线程相同。描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。每个方法的开始到结束执行,对应一个栈帧在虚拟机栈中入栈到出栈的过程。通常java以栈、堆内存简单的区分方法,所指的栈就是虚拟机栈或栈中的局部变量表部分。

局部变量表存放了编译期可知的基本数据类型,对象引用,和指向字节码的指令地址。

这个区域规定了两种异常:StackOverFlowError和OutOfMemoryError。

异常解析移步:Java的常见异常及Demo-StackOverFlow和OutOfMemery 部分

 

  • 本地方法栈

为虚拟机使用的Native方法服务。本地方法栈也会抛出StackOverFlowError和OutOfMemoryError。

简单地讲,一个Native Method就是一个java调用非java代码的接口。

 

  • java堆

内存中最大的一块,所有线程共享内存区域,在虚拟机启动时创建。

此内存区域唯一目的就是存放对象的实例,几乎所有对象的实例都在这里分配内存。

Java堆是垃圾收集器管理的主要区域,也被称为GC堆。

由于收集器基本采用分代收集算法,所以Java堆中通常细分为新生代和老年带。下图标明了堆中的典型区域分配。

有关新生代、老年代和详细的内存分配,请移步:javaGC与内存分配策略解析

 

  • 方法区

别名“非堆”,是线程共享的内存区域,用于存储已被虚拟机加载的类信息,常量,静态变量,编译后的代码等数据。

运行时常量池,是方法区的一部分,用于存放编译期生成的各种字面量和符号引用。

 

  • 直接内存(DirectMemory)

直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁的使用,而且也可能导致OutOfMemoryError异常出现。

        在JDK1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,他可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一场场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

        显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存(包括RAM以及SWAP区或者分页文件)大小以及处理器寻址空间的限制。服务器管理员在配置虚拟机参数时,会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

 

Minor GC与Full GC触发条件

Minor GC触发条件:

当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法去空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

(5)由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小

 

堆外内存导致的溢出错误

例如:一个学校的小型项目,基于B/S的电子考试系统。为了实现客户端实时接收考试数据,系统使用逆向AJAX技术,选用CometD1.1.1作为服务端推送框架。硬件为普通PC机,I5,4GB内存,32位Windows系统。

测试期间发现服务端不定期抛出内存溢出异常,管理员尝试把堆开到最大,32位基本1.6GB就无法再加大(定义上限2GB),开大没效果,反而抛出异常更加频繁。

工具监控发现Eden区,Survivor区,老年代和永久代内存稳定。内存溢出后查看系统日志异常堆栈,显示

[org.eclipse.jetty.util.log]handle failed java.lang.OutOfMemoryError:null 
at sun.raise.Unsafe.allocateMemory (Native Method )
at java.nio.DirectByteBuffer.<init> (DirectByteBuffer.java :99 )
at java.nio.ByteBuffer.allocateDirect (ByteBuffer.java :288 )
at org.eclipse.jetty.io.nio.DirectNIOBuffer.<init>
...

32位windows平台划给进程内存的上显示2G,其中1.6GB划给了java堆,而DirectMemory只能从剩余的0.4GB空间分出一部分。此应用导致溢出的关键是:垃圾收集时,DirectMemory只能等待老年代满了后FullGC,然后顺便帮它清理掉内存废弃对象。否则只能等抛出内存溢出异常时,先catch掉,再在catch里大喊一声"System.GC()!".如果虚拟机还是不听(譬如打开了-XX:DisableExplicitGC),那就只能抛出异常。

而CometD 1.1.1框架,正好有大量的NIO操作需要用到DirectMemory。

 

Java中什么情况会引起内存泄漏

从实践经验的角度出发,除了Java堆和永久代之外,我们注意到下面这些区域还会占用较多的内存,这里所有的内存总和受到操作系统进程最大内存的限制。

  • Direct Memory : 可通过-XX : MaxDirectMemorySize调整大小,内存不足时拋出OutOfMemoryError或者OutOfMemoryError : Direct buffer memory。
  • 线程堆栈:可通过-Xss调整大小,内存不足时拋出StackOverflowError (纵向无法分配, 即无法分配新的栈帧)或者OutOfMemoryError : unable to create new native thread (横向无法分配 ,即无法建立新的线程)。
  • Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会拋出IOException : Too many open files异常。
  • JNI代码 (JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信[主要是C&C++]):如果代码中使用JNI调用本地库,那本地库使用的内存也不在堆中。

有关JNI:

1、你可以使用JNI来实现“本地方法”(native methods),并在JAVA程序中调用它们。

2、JNI支持一个“调用接口”(invocation interface),它允许你把一个JVM嵌入到本地程序中。本地程序可以链接一个实现了JVM的本地库,然后使用“调用接口”执行JAVA语言编写的软件模块。例如,一个用C语言写的浏览器可以在一个嵌入式JVM上面执行从网上下载下来的applets。

JNI调用的内存是不能进行GC操作的,那该如何解决?
1、堆内内存与堆外内存之间数据拷贝的方式(并且在将堆内内存拷贝到堆外内存的过程JVM会保证不会进行GC操作):比如我们要完成一个从文件中读数据到堆内内存的操作,即FileChannelImpl.read(HeapByteBuffer)。

这里实际上File I/O会将数据读到堆外内存中,然后堆外内存再讲数据拷贝到堆内内存,这样我们就读到了文件中的内存。

2、直接使用堆外内存,如DirectByteBuffer:这种方式是直接在堆外分配一个内存(即,native memory)来存储数据,程序通过JNI直接将数据读/写到堆外内存中。因为数据直接写入到了堆外内存中,所以这种方式就不会再在JVM管控的堆内再分配内存来存储数据了,也就不存在堆内内存和堆外内存数据拷贝的操作了。这样在进行I/O操作时,只需要将这个堆外内存地址传给JNI的I/O的函数就好了。

http://www.importnew.com/26334.html

  • 虚拟机和GC:虚拟机、GC的代码执行也要消耗一定的内存。

 

一般来说,只要类是自己管理内存,程序员就应该警惕内存泄漏的问题

 

  • Stack类自己管理内存,当栈收缩,而不处理过期引用则会引起内存泄漏 (Effectivejavap2)

如果一个栈先是增长,再收缩,那么从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,他们也不会被回收。因为栈内部维护着这些对象的过期引用。会引起内存泄漏。

这类问题的修复很简单,一旦对象引用已经过期,只需清空引用即可。

对例子中的stack类而言,只要一个单元被弹出栈,指向他的引用就过期了。

public Object pop(){
    if(size==0) 
        throw new EmptyStackException();
    
    Object result = elements[--size];
    
    elements[size]=null; //解除过期对象的引用

    result result;
     
}

 

  •     内存泄漏的另一个常见来源是缓存

一旦你把对象引用放到缓存中,就很容易被遗忘,例如:

我们加载了一个对象放在缓存中(例如放在一个全局 map 对象中),然后一直不再使用它,这个对象一直被缓存引用,但却不再被使用。

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是 java 中内存泄露的发生场景。

通俗地说,就是程序员可能创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用, 即这个对象无用但是却无法被垃圾回收器回收的,这就是 java 中可能出现内存泄露的情况。

同时,如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。

检查 java 中的内存泄露,一定要让程序将各种分支情况都完整执行到程序结束,然后看某个对象是否被使用过,如果没有,则才能判定这个对象属于内存泄露。

 

  • 内存泄露的第三个常见来源是监听器和其他回调

如果你实现了一个API,客户端在这个API中注册回调,但没有显式的取消注册,那么除非你进行处理,否则它们就会累积。

确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用,例如只保存成WeakHashMap中的键。

 

  • Hash引起的内存泄漏

内存泄露的另外一种情况:当一个对象被存储进 HashSet(Map)集合中以后,不能修改这个对象中的那些参与计算哈希值的字段。

否则,对象修改后的哈希值与最初存储进 HashSet(Map)集合中时的哈希值就不同了。

在这种情况下,即使在 contains 方法 用该对象的当前引用作为的参数去Hash集合中检索对象,也将返回找不到对象的结果。

同时,也导致无法从Hash集合中单独删除当前对象而造成内存泄露。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值