Java 堆内存(Heap)

标签: java 内存 heap | 发表时间:2013-08-26 17:54 | 作者:
出处:http://www.iteye.com

        堆(Heap)又被称为:优先队列(Priority Queue),是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。
        堆的数据结构如图所示:



 

        Heap 是一种数据结构,而我们平时常说的Heap 其实指的是"Heap Memory"(堆内存),下面所指"Heap"亦均指为堆内存概念。

        Heap 是应用程序在运行期请求操作系统分配给自己的向高地址扩展的数据结构,是不连续的内存区域。由于从操作系统/JVM 管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率比较低。但是堆的优点在于:编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。在Java 中,要求创建一个对象时,只需用new 关键字及相关的代码即可。执行这些代码时,JVM 会在堆内存中自动进行数据存储空间的分配。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因之一。
        所以堆内存最大的特点就是: 堆允许程序在运行时动态地申请某个大小的内存空间。

       

        在Java 中,Heap 用来存储数组与new 关键字创建的对象,例如:

Student stu = new Student();

 
        (方法中定义的基本类型的变量和对象的引用变量都在会栈内存中分配)
        首先JVM 会在Heap 中分配出Student 对象存储的内存区域,并将地址返回,然后再在Stack 中创建Student 对象的引用用来存放Heap 分配的Student 地址。之后就可以在程序中使用栈中的引用对象来访问堆中的数组或对象。当使用完对象后,我们不必显式的管理堆内存释放工作,堆内存的释放会由GC(垃圾收集器)自动完成。

 

        在HotSpot JVM 实现中Heap 内存被“分代”管理,默认的本节以此为例讲解。

        JVM的内存首先被分割成两部分:

 
- Heap Memory(堆内存)
- Non Heap Memory(非堆内存,除了堆内存区域用来存放存活(living)的数据,JVM还需要尤其是类描述、元数据等更多信息。所以这些信息统一被存放在命名为Permanent generation(永久/常驻代)的区域。)

 

 

        Heap Memory 又被分为两大区域:

 
- Young/New Generation 新生代
- Old/Tenured Generation 老年代

 

 

        综上如图所示:



 
        1.Young/New Generation 新生代
        程序中新建的对象都将分配到新生代中,新生代又由Eden(伊甸园)与两块Survivor(幸存者) Space 构成。Eden 与Survivor Space 的空间大小比例默认为8:1,即当Young/New Generation 区域的空间大小总数为10M 时,Eden 的空间大小为8M,两块Survivor Space 则各分配1M,这个比例可以通过-XX:SurvivorRatio 参数来修改。Young/New Generation的大小则可以通过-Xmn参数来指定。

 

        Eden:刚刚新建的对象将会被放置到Eden 中,这个名称寓意着对象们可以在其中快乐自由的生活。

Survivor Space:幸存者区域是新生代与老年代的缓冲区域,两块幸存者区域分别为From Space(s0) 与To Space(s1),当触发Minor GC 后将仍然存活的对象移动到S0中去。这样Eden 就被清空可以分配给新的对象。
        当再一次触发Minor GC后,S0和Eden 中存活的对象被移动到S1中,S0即被清空。在同一时刻, 只有Eden和一个Survivor Space同时被操作。所以s0与s1两块Survivor 区同时会至少有一个为空闲的。

        当每次对象从Eden 复制到Survivor Space 或者从Survivor Space 之间复制,计数器会自动增加其值。 默认情况下如果复制发生超过16次,JVM 就会停止复制并把他们移到老年代中去。如果一个对象不能在Eden中被创建,它会直接被创建在老年代中。

        新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,通常很多的对象都活不过一次GC,所以Minor GC 非常频繁,一般回收速度也比较快。
        Minor GC 清理过程(图中标X为垃圾):
        1.清理之前



 

        2.清理之后



 
        2.Old/Tenured Generation 老年代
        老年代用于存放程序中经过几次垃圾回收后还存活的对象,例如缓存的对象等,老年代所占用的内存大小即为-Xmx 与-Xmn 两个参数之差。
        堆是JVM 中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new 对象的开销是比较大的,鉴于这样的原因,Hotspot JVM 为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB(Thread Local Allocation Buffer),其大小由JVM 根据运行的情况计算而得,在TLAB 上分配对象时不需要加锁,因此JVM 在给线程的对象分配内存时会尽量的在TLAB 上分配,在这种情况下JVM 中分配对象内存的性能和C 基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配,TLAB 仅作用于新生代的Eden,因此在编写Java 程序时,通常多个小的对象比大的对象分配起来更加高效,但这种方法同时也带来了两个问题,一是空间的浪费,二是对象内存的回收上仍然没法做到像Stack 那么高效,同时也会增加回收时的资源的消耗,可通过在启动参数上增加 -XX:+PrintTLAB来查看TLAB 这块的使用情况。

 

        老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,通常会伴随至少一次Minor GC(但也并非绝对,在ParallelScavenge 收集器的收集策略里则可选择直接进行Major GC)。MajorGC 的速度一般会比Minor GC 慢10倍以上。

        虚拟机给每个对象定义了一个对象年龄(age)计数器。如果对象在 Eden 出生并经过第一次 Minor GC 后仍然存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为 1。对象在 Survivor 区中每熬过一次 Minor GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15 岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

 

        当一个Object被创建后,内存申请过程如下:
        1.JVM 会试图为相关Java 对象在Eden 中初始化一块内存区域。
        2.当Eden 空间足够时,内存申请结束。否则进入第三步。
        3.JVM 试图释放在Eden 中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden 空间仍然不足以放入新对象,则试图将部分Eden 中活跃对象放入Survivor 区。
        4.Survivor 区被用来作为新生代与老年代的缓冲区域,当老年代空间足够时,Survivor 区的对象会被移到老年代,否则会被保留在Survivor 区。
        5.当老年代空间不够时,JVM 会在老年代进行0级的完全垃圾收集(Major GC/Full GC)。
        6.Major GC/Full G后,若Survivor 及老年代仍然无法存放从Eden 复制过来的部分对象,导致JVM 无法在Eden 区为新对象创建内存区域,JVM 此时就会抛出内存不足的异常。

 

        通过jvmstat 可以清晰的观察出程序中对象的衰老过程:



 

        jvmstat相关问题可以查看: http://286.iteye.com/blog/1922630

 

        在32位系统下可以为JVM 分配最大2GB 堆内存大小,64位则没有限制,下列是一些常用与Heap 相关的参数:

 
-Xms:初始堆大小,默认为物理内存的1/64(<1GB);默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制

-Xmx:最大堆大小,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

-Xmn:新生代的内存空间大小,注意:此处的大小是(eden+ 2 survivor space)。与jmap -heap中显示的New gen是不同的。整个堆大小=新生代大小 + 老生代大小 + 永久代大小。
在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-XX:SurvivorRatio:新生代中Eden区域与Survivor区域的容量比值,默认值为8。两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。

-Xss:每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。应根据应用的线程所需内存大小进行适当调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。一般小的应用, 如果栈不是很深, 应该是128k够用的,大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"-Xss is translated in a VM flag named ThreadStackSize”一般设置这个值就可以了。

-XX:PermSize:设置永久代(perm gen)初始值。默认值为物理内存的1/64。

-XX:MaxPermSize:设置持久代最大值。物理内存的1/4。

 

        下面就是几种断代法可用GC汇总:



        断代法GC的讲解将在GC 这节进行。 



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [java 内存 heap] 推荐:

Java 堆内存(Heap)

- - ITeye博客
        堆(Heap)又被称为:优先队列(Priority Queue),是计算机科学中一类特殊的数据结构的统称. 堆通常是一个可以被看做一棵树的数组对象. 在队列中,调度程序反复提取队列中第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权.

JAVA内存释放

- - Java - 编程语言 - ITeye博客
(问题一:什么叫垃圾回收机制. ) 垃圾回收是一种动态存储管理技术,它自动地释放不再被程序引用的对象,按照特定的垃圾收集算法来实现资源自动回收的功能. 当一个对象不再被引用的时候,内存回收它占领的空间,以便空间被后来的新对象使用,以免造成内存泄露. (问题二:java的垃圾回收有什么特点. ) JAVA语言不允许程序员直接控制内存空间的使用.

java内存泄漏

- - 编程语言 - ITeye博客
不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址. Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Heap)中分配的,所有对象的回收都是由Java虚拟机通过垃圾回收机制完成的. GC为了能够正确释放对象,会监控每个对象的运行状况,对他们的申请、引用、被引用、赋值等状况进行监控,Java会使用有向图的方法进行管理内存,实时监控对象是否可以达到,如果不可到达,则就将其回收,这样也可以消除引用循环的问题.

深入Java内存模型

- - ImportNew
你可以在网上找到一大堆资料让你了解JMM是什么东西,但大多在你看完后仍然会有很多疑问. happen-before是怎么工作的呢. 用volatile会导致缓存的丢弃吗. 为什么我们从一开始就需要内存模型. 通过这篇文章,读者可以学习到足以回答以上所有问题的知识. 它包含两大部分:第一部分是硬件层次的大体架构,第二部分是深入OpenJdk源代码和实现.

Java内存之"栈"与"堆"

- - ITeye博客
        昨天中午,发了一篇 equals和==区别的博文,晚上再看时有几位大牛指出了其中的一些错误,很感谢他们的留言,一句简简单单的留言给了我对这些错误知识点改正的机会. 或许这就是从事互联网行业所提倡的互帮互助的精神吧,因为有分享,有交流,互联网才会发展的如此迅猛. 大牛提的一个观点很好,好的东西可以拿出来分享,错的东西却可能带给别人错误的理解,这一点我确实得向看了我写了一些bug博客的人道个歉.

浅谈Java--内存泄漏

- - ITeye博客
      JAVA的垃圾回收机制,让许多程序员觉得内存管理不是很重要,但是内存内存泄露的事情恰恰这样的疏忽而发生,特别是对于Android开发,内存管理更为重要,养成良好的习惯,有利于避免内存的泄漏..     这里可以把许多对象和引用看成是有向图,顶点可以是对象也可以是引用,引用关系就是有向边.

Java 内存模型 JMM

- - 码蜂笔记
JMM,Java Memory Model,Java 内存模型. 什么是内存模型,要他何用. 假定一个线程为变量var赋值: var = 3;,内存模型要回答的问题是:在什么条件下,读取变量var的线程可以看到 3这个值. 如果缺少了同步,线程可能无法看到其他线程操作的结果. 导致这种情况的原因可以有:编译器生成指令的次序可以不同于源代码的“显然”版本,编译器还会把变量存储在寄存器而不是内存中;处理器可以乱序或并行执行指令;缓存会改变写入提交到主存得到变量的次序;存储在处理器本地缓存中的变量对其他处理器不可见 等等.

Java的内存泄露

- - Java译站
Java有垃圾回收,因此不会出现内存泄露. 尽管Java的确有垃圾回收器来回收那些不用的内存块,但你不要指望它能够点铁成金. GC减轻了开发人员肩上的负担,而原本的那些工作非常容易出错,不过并不是所有内存分配的问题它都能够解决. 更糟糕的是,Java的设计允许它可以欺骗GC,使得它能够保留一些程序已经不再使用的内存.

Java的内存机制

- - Java - 编程语言 - ITeye博客
1.Java的内存机制.  Java 把内存划分成两种:一种是栈内存,另一种是堆内存. 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配,当在一段代码块定义一个变量时,Java 就在栈中为这个变量分配内存空间,当超过变量的作用域后(比如,在函数A中调用函数B,在函数B中定义变量a,变量a的作用域只是函数B,在函数B运行完以后,变量a会自动被销毁.

Java 内存溢出排查

- - ImportNew
Java OOM 毫无疑问是开发人员常见并且及其痛恨的问题,但是任何服务的开发都没法避免 OOM. 因此,OOM 的排查及定位是每个 Java 工程师都必备的技能. 在使用 scala 开发的一个 web 服务,在用户使用中,经常出现:  java.lang.OutOfMemoryError: Java heap space .