Java实践(三)---heap(堆内存)

来源:互联网 发布:辣鸡软件表情 编辑:程序博客网 时间:2024/06/17 04:38

在Java语言中,垃圾回收(Garbage Collection,GC)主要作用是回收程序中不再使用的内存;垃圾回收器要负责完成3个任务:

  1. 分配内存
  2. 确保被引用对象的内存不被错误地回收
  3. 回收不再被引用的对象的内存空间

垃圾回收器的存在,提高了开发效率,屏蔽了释放内存的方法,保证了程序的稳定性;但是垃圾回收也带来了问题,垃圾回收器为了实现垃圾回收必须做到:

  1. 跟踪内存的使用情况
  2. 释放没用的对象
  3. 完成内存释放后需要处理堆中的碎片

这些操作增加JVM的负担,从而降低程序的执行效率

对于垃圾回收器来说,使用有向图来记录和管理堆内存中的所有对象,通过有向图来记录和管理内存中的所有对象,通过有向图可以识别哪些对象可达,哪些对象不可达,所有不可达对象都是可被垃圾回收的

垃圾回收算法

1. 引用计数算法(Reference Counting Collector)

引用计数算法是一种简单但是效率低下的方法,其主要原因如下:在堆中每个对象都有一个引用计数器,当对象被引用时,引用计数器加1;当引用被置为空或离开作用域时,引用计数减1,由于这个方法被引用时无法解决相互引用的问题,因此JVM并没有采用

2. 追踪回收算法(Tracing Collectior)

追踪回收算法利用JVM维护的对象引用图,从根节点开始会遍历对象的引用图,同时标记遍历到的对象;当遍历结束后,未标记的对象就是目前已不被使用的对象,可以被回收了

3. 压缩回收算法(Compacting Collectior)

压缩回收算法的主要思路如下:把堆中活动的对象移动到堆中一端,这样就会在堆中另外一端留出很大的一块空闲区域,相当于对堆中的碎片进行了处理;虽然这种方法简化消除堆碎片的工作,但是每次处理都会带来性能的损失

4. 复制回收算法(Coping Collector)

把堆分成两个大小相同的区域,在任何时刻,只有其中的一个区域被使用,直到整个区域被消耗完为止,此时垃圾回收器会中断程序的执行,通过遍历的方法把所有活动的对象复制到另外一个区域中,在复制的过程中它们是紧挨着布置的,从而可以消除内存碎片;当复制过程结束后程序接着运行,直到这块区域被使用完,然后再采用上面的方法继续进行垃圾回收

5. 按代回收算法(Generational Collector)

复制回收算法的主要缺点:每次算法执行时,所以出于活动状态的对象都要被复制,效率低下;由于程序有“程序创建的大部分对象的生命周期都很短,只有一部分对象有较高的生命周期”的特点;按代回收算法把堆分为两个或多个子堆(每一个子堆被视为一代);在算法运行过程中,优先收集那些“年幼”的对象,如果一个对象经过多次收集仍然“存活”,那么就可以把这个对象转移到高一级的堆里,减少对其扫描的次数

在JVM的堆内存中分为2块:

1. Permanent Space:即持久层,主要存放的是Java类定义信息,与垃圾回收器要收集的Java对象关系不大
2. Heap Space:Heap由 old(即年老代) new(即年轻代)【new由Eden,from,to组成】 组成。年轻代和年老代的划分对垃圾回收影响比较大

1- 持久代

用于存放静态类型数据,如 Java Class, Method 等。持久代对垃圾回收没有显著影响。但是有些应用可能动态生成或调用一些Class,例如 hibernate CGLib 等,在这种时候往往需要设置一个比较大的持久代空间来存放这些运行过程中动态增加的类型。

2- 堆空间

这里写图片描述
2-1 年老代

在年轻代中经历了N次(可配置)垃圾回收后仍然存活的对象,就会被复制到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

针对年老代的垃圾回收即 Full GC

2-2 年轻代

所有新生成的对象首先都是放在年轻代。年轻代的目标就是尽可能快速的收集那些生命周期短的对象。年轻代一般分3个区,1个Eden区,2个Survivor区(from 和 to)。

针对年轻代的垃圾回收即 Young GC

  1. 大部分对象在Eden区中生成
  2. 当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个)
  3. 当一个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区
  4. 当另一个Survivor区也满了的时候,从前一个Survivor区复制过来的并且此时还存活的对象,将可能被复制到年老代

2个Survivor区是对称的,没有先后关系,所以同一个Survivor区中可能同时存在从Eden区复制过来对象,和从另一个Survivor区复制过来的对象;而复制到年老区的只有从另一个Survivor区过来的对象。

因为需要交换的原因,Survivor区至少有一个是空的。特殊的情况下,根据程序需要,Survivor区是可以配置为多个的(多于2个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

创建对象时,内存的申请过程:

  1. JVM会试图为相关Java对象在年轻代的Eden区中初始化一块内存区域。
  2. 当Eden区空间足够时,内存申请结束。否则执行下一步。
  3. JVM试图释放在Eden区中所有不活跃的对象(Young GC)。释放后若Eden空间仍然不足以放入新对象,JVM则试图将部分Eden区中活跃对象放入Survivor区
  4. Survivor区被用来作为Eden区及年老代的中间交换区域。当年老代空间足够时,Survivor区中存活了一定次数的对象会被移到年老代。
  5. 当年老代空间不够时,JVM会在年老代进行完全的垃圾回收(Full GC)。
  6. Full GC后,若Survivor区及年老代仍然无法存放从Eden区复制过来的对象,则会导致JVM无法在Eden区为新生成的对象申请内存,即出现“Out of Memory”。

OOM(“Out of Memory”)异常一般主要有如下2种原因:

  1. 年老代溢出,表现为:java.lang.OutOfMemoryError:Javaheapspace 这是最常见的情况,产生的原因可能是:设置的内存参数Xmx过小或程序的内存泄露及使用不当问题。
    例如循环上万次的字符串处理、创建上千万个对象、在一段代码内申请上百M甚至上G的内存。还有的时候虽然不会报内存溢出,却会使系统不间断的垃圾回收,也无法处理其它请求。这种情况下除了检查程序、打印堆内存等方法排查,还可以借助一些内存分析工具,比如MAT就很不错。

  2. 持久代溢出,表现为:java.lang.OutOfMemoryError:PermGenspace 通常由于持久代设置过小,动态加载了大量Java类而导致溢出,解决办法唯有将参数 -XX:MaxPermSize
    调大(一般256m能满足绝大多数应用程序需求)。将部分Java类放到容器共享区(例如Tomcat share
    lib)去加载的办法也是一个思路,但前提是容器里部署了多个应用,且这些应用有大量的共享类库。

更多详细内容参考这个博客:http://blog.csdn.net/sivyer123/article/details/17139443

0 0