JVM内存储器管理学习笔记

JVM内存管理学习笔记


JVM内存储器管理学习笔记
 

内存管理

  在所有的语言中, 一般是实现了以下两种内存管理的方式之一。

第一种是以 c/c++ 为典型代表的,是需要程序员显示的管理内存,如 c malloc /free   c++ new delete 。优点非常明显,就是程序员对所有的内存具有完全的控制权,和那些语言层面实现了内存管理的语言相比,效率很高。但是缺点也很明显,就是一不小心,很容易内存泄露, 学习成本较高, 特别是新手,很容易出错。

第二种是以后出现的众多高级语言,例如 java python c# 等等, 这些语言都不需要主动去管理内存,而是语言层面已经帮你完成了这部分功能,帮你管理内存,对于新手来说,也不容易写出太差的代码。但是同样缺点也很明显,就是你不能很明确的控制什么时候回收内存块。

内存分配和内存的回收是 JVM gc 主要需要完成的事情, 我们只有通过详细的了解 gc 相关的触发机制,才可以写出更加建壮的程序。

java 的内存区域

1.堆区(head)

    存放着所有通过new生成的对象都放在这个区域,并通过一个指针指向对应的对象。堆区一般按对象的生命周期分为年轻代和老年代?等等。。。。。。,为什么要分这样呢?

 

    分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

 

    比如String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。试想,在不进行对象生命周期区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在, 后面的GC算法也会提到。

 

JVM内存储器管理学习笔记

这里说说年轻代:

  所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个 Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制 过来的并且此时还存活的对象,将被复制“年老区(Tenured)”。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时 存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空 的。同时,根据程序需要,Survivor区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。

 

2.栈区(stack)

    存放线程调用方法时的局部对象和操作数栈。随线程而生,随线程而灭,栈中的帧随着方法进入、退出而有条不紊的进行着出栈入栈操作。

   -Xss256k 设置每个线程的堆栈大小 ,默认为512K(win32, jdk1.6),根据应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右 下面这段代码测试堆栈大小的线程的影响。

 

package test;

/**
 * 测试-Xss对方法调用栈的影响
 * -Xss128k     2619次
 * -Xss256k     5895次
 * -Xss512k     7534次
 * -Xss1024k    25556次
 * 默认是 7534次
 * @author yunpeng.jiangyp
 */
public class TestStack {

    private static int count = 0;

    public static void test() {
        System.out.println(count);
        ++count;
        test();
    }

    public static void main(String[] args) {
        test();

    }
}
 

 

 

3.方法区(method)

存放类的元数据常量

关于方法区很多人认为是没有GC的,《Java虚拟机规范》中确实说过可以不要求虚拟机在这区实现GC,而且这区GC的“性价比”一般比较低:在堆中,尤其是 在新生代,常规应用进行一次GC可以一般可以回收70%~95%的空间,而永久代的GC效率远小于此。虽然VM Spec不要求,但当前生产中的商业JVM都有实现永久代的GC,主要回收两部分内容:废弃常量与无用类。这两点回收思想与Java堆中的对象回收很类 似,都是搜索是否存在引用,常量的相对很简单,与对象类似的判定即可。而类的回收则比较苛刻,需要满足下面3个条件:

  1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
  2.加载该类的ClassLoader已经被GC。
  3.该类对应的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

  是否对类进行回收可使用-XX:+ClassUnloading参数进行控制,还可以使用-verbose:class或者-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看类加载、卸载信息。

  在大量使用反射、动态代理、CGLib等bytecode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都需要JVM具备类卸载的支持以保证永久代不会溢出。

 

GC要解决的几个问题。

1. 什么时候回收?

什么情况下触发垃圾回收? 由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC(yong gc)

  一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

  Full GC

  对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

  · 年老代(Tenured)被写满

  · 持久代(Perm)被写满

  · System.gc()被显示调用

  ·上一次GC之后Heap的各域分配策略动态变化

 

2.哪些内存需要回收?

  在堆里面存放着Java世界中几乎所有的对象,在回收前首先要确定这些对象之中哪些还在存活,哪些已经“死去”了,即不可能再被任何途径使用的对象。

 

  引用计数算法(Reference Counting) 

    最初的想法,也是很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的。
    客观的说,引用计数算法实现简单,判定效率很高,在大部分情况下它都是一个不错的算法,但引用计数算法无法解决对象循环引用的问题。举个简单的例子:对象A和B分别有字段b、a,令A.b=B和B.a=A,除此之外这2个对象再无任何引用,那实际上这2个对象已经不可能再被访问,但是引用计数算法却无法回收他们。

 

  根搜索算法(GC Roots Tracing)

      在实际生产的语言中,都是使用根搜索算法判定对象是否存活。算法基本思路就是通过一系列的称为“GC Roots”的点作为起始进行向下搜索,当一个对象到GC Roots没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。在Java语言中,GC Roots包括:
  1.在VM栈(帧中的本地变量)中的引用
  2.方法区中的静态引用
  3.JNI(即一般说的Native方法)中的引用
   判定一个对象死亡,至少经历两次标记过程:如果对象在进行根搜索后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并在稍后执行他的finalize()方法(如果它有的话)。这里所谓的“执行”是指虚拟机会触发这个 方法,但并不承诺会等待它运行结束。这点是必须的,否则一个对象在finalize()方法执行缓慢,甚至有死循环什么的将会很容易导致整个系统崩溃。 finalize()方法是对象最后一次逃脱死亡命运的机会,稍后GC将进行第二次规模稍小的标记,如果在finalize()中对象成功拯救自己(只要 重新建立到GC Roots的连接即可,譬如把自己赋值到某个引用上),那在第二次标记时它将被移除出“即将回收”的集合,如果对象这时候还没有逃脱,那基本上它就真的离 死不远了。根搜索算法具体可以分为:


  a.标记-清除算法(Mark-Sweep)
   如它的名字一样,算法分层“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象,整个过程其实前一节讲对象标记判定的时候已经基本介绍完了。说它是最基础的收集算法原因是后续的收集算法都是基于这种思路并优化其缺点得到的。它的主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。

  b.复制(Copying)
  为了解决效率问题,一种称为“复制”(Copying)的搜集算法出现,它将可用内存划分为两块,每次只使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空间一次过清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。
 
   现在的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM有专门研究表明新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden和survivor还存活的对象一次过拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。Sun Hotspot虚拟机默认eden和survivor的大小比例是8:1,也就是每次只有10%的内存是“浪费”的。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有10%以内的对象存活,当survivor空间不够用时,需要依赖其他内存(譬如老年代)进行分配担保(Handle Promotion)。


  c.Mark-Compact(标记-整理)

   没有搞明白。

3. 如何回收回收?

   没有研究。

 

GC调优。

1. 衡量尺度
    分配内存,GC的频率, GC导致应用暂停的时间 ,应用的响应时间和QPS。

 

2.经验值

   1 对于32位, JVM必须建议控制在2G以下。
   2 对于64位, 最好开启指针压缩的选项-XX:+UseCompressedOops
   3 根据应用的需求, 选择合适的垃圾收集器

   4 计算出可能存活的对象数量,并进行相应的设置。

 

3.GC的主要的应对策略
  一 降低FGC的频率 :
      1,很明显增大old
      2,降低从new->old的晋升频率。   
   二 降低FGC的暂停时间
     1 .减小Heap
     2 .换成CMS
     3.升级CPU(据说最靠谱)
   三降低FGC的频率
     1 增大新生代
   四降低FGC的暂停时间
     1减少新生代,可能造成频繁FGC
     2升级CPU
     3写出GC友好的代码
       1.尽量减少使用autobox, 因为在Java在将原始对类型变成对象的过程中, 需要new一个新的对象过程中,在某些时候,可能会使用大量堆的内存造成内存溢出。
       2.合理控制动态增长的数据结构的大小,很多时候内存的超出错误(OOM)都是这些动态数据结构造成, 他给我们带来方便的同时, 也同样带来了不可控制性。
       3.合理使用reference。
       4.不要使用finalize()来回收资源,据写sdk的人说,Finalizers并不能保证一定会被执行,在某些特殊的时刻,可能会被跳过,这样很容易导致内存泄漏。

 

参考文档:
0. 非常详细GC学习笔记  http://blog.csdn.net/fenglibing/article/details/6321453
1. 深入理解JVM—gc相关 http://sunshine-1985.iteye.com/blog/1132011
2. GC算法 http://jarit.iteye.com/blog/1010835
3. 优化JVM参数提高eclipse运行速度 http://www.iteye.com/topic/756538
4. Yourkit官方文档 http://www.yourkit.com/docs/kb/sizes.jsp
5. 理解Heap Profling名词-Shallow和Retained Sizes http://rdc.taobao.com/team/jm/archives/900

6. JVM 几个重要的参数 http://blog.sina.com.cn/s/blog_5465f7f20100tuur.html

7. Java虚拟机日志查看工具 gclogviewer| http://www.oschina.net/p/gclogviewer