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

Java 堆数据结构定义 java中的堆数据结构_数据结构与算法


 

        Heap 是一种数据结构,而我们平时常说的Heap 其实指的是"Heap Memory"(堆内存)。

        Heap 是应用程序在运行期请求操作系统分配给自己的向高地址扩展的数据结构,是不连续的内存区域。由于从操作系统/JVM 管理的内存分配,所以在分配和销毁时都要占用时间,因此用堆的效率比较低。但是堆的优点在于:编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。事实上,面向对象的多态性,堆内存分配是必不可少的,因为多态变量所需的存储空间只有在运行时创建了对象之后才能确定。
        所以堆内存最大的特点就是:堆允许程序在运行时动态地申请某个大小的内存空间。

 

        在JVM 中,堆(Heap)是可供各条线程共享的运行时内存区域,也是供所有类实例和数组对象分配内存的区域。
        Java堆在虚拟机启动的时候就被创建,它存储了被自动内存管理系统(Automatic Storage Management System,也即是常说的“Garbage Collector(垃圾收集器)”)所管理的各种对象,这些受管理的对象无需,也无法显式地被销毁。Java堆的容量可以是固定大小的,也可以随着程序执行的需求动态扩展,并在不需要过多空间时自动收缩。Java堆所使用的内存亦不需要保证是连续的。
        JVM 实现应当提供给程序员或者最终用户调节Java堆初始容量的手段,对于可以动态扩展和收缩Java堆来说,则应当提供调节其最大、最小容量的手段。

        在Java 中,要求创建一个对象时,只需用new 关键字及相关的代码即可。执行这些代码时,JVM 会在堆内存中自动进行数据存储空间的分配。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!这也正是导致我们刚才所说的效率低的原因之一。

       

        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(永久/常驻代)的区域。

        非堆内存其实就是JVM 留给自己用的,所以方法区、JVM 内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码等都在非堆内存中。

        非堆内存由JVM 管理,我们无法在程序中使用。

 

        下图为Heap 在Runtime Data Area(运行时数据区)中的位置,可以说除了Heap 都属于Non Heap(非堆内存):

 

Java 堆数据结构定义 java中的堆数据结构_数据结构与算法_02

 

 

        Heap Memory 又被分为两大区域:

        - Young/New Generation 新生代

        新生对象放置在新生代中,新生代由Eden 与Survivor Space 组成。

        - Old/Tenured Generation 老年代

        老年代用于存放程序中经过几次垃圾回收后还存活的对象

Java 堆数据结构定义 java中的堆数据结构_数据结构与算法_03


        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:幸存者区域是新生代与老年代的缓冲区域,两块幸存者区域分别为s0 与s1,当触发Minor GC 后将仍然存活的对象移动到S0中去(From Eden To s0)。这样Eden 就被清空可以分配给新的对象。
        当再一次触发Minor GC后,S0和Eden 中存活的对象被移动到S1中(From s0To 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 清理过程(图中红色区域为垃圾):
        1.清理之前

Java 堆数据结构定义 java中的堆数据结构_java_04


  

        2.清理之后

Java 堆数据结构定义 java中的堆数据结构_Java 堆数据结构定义_05


  

注意:图中的"From" 与"To" 只是逻辑关系而不是Survivor Space 的名称,也就是说谁装着对象谁就是"From"。

        一个对象在幸存者区被移动/复制的次数决定了它是否会被移动到堆中。

        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 可以清晰的观察出JVM 的各个过程:

Java 堆数据结构定义 java中的堆数据结构_运维_06


 

         从jvmstat中可以清晰的观察到汇编,加载,垃圾回收消耗的时间与各区域内存使用情况,在图中s0与s1的内存使用永远都是相斥的,即至多只有一个会在使用。

        还可以使用'YourKit Java Profiler'这个强大的工具观察更多的内存及class情况,关于YourKit Java Profiler 可以参考另一篇文章。

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


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

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

-Xmn:新生代的内存空间大小,即Eden+ 2个survivor space。
在保证堆大小不变的情况下,增大新生代后,将会减小老生代大小。此值对系统性能影响较大,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。


 

        大型的应用系统常常会被两个问题困扰:
        一个是启动缓慢,因为初始Heap 非常小,必须由很多major 收集器来调整内存大小。
        另一个更加严重的问题就是默认的Heap 最大值对于应用程序来说“太可怜了”。根据以下经验法则(即拇指规则,指根据经验大多数情况下成立,但不保证绝对):
        (1)给于虚拟机更大的内存设置,往往默认的64mb 对于现代系统来说太小了。
        (2)将-Xms 与-Xmx 设置为相同值,这样做的好处是GC 不用再频繁的根据内存使用情况去动态修改Heap 内存大小了,而且只有当内存使用达到-Xmx 设置的最大值时才会触发垃圾收集,这给GC 及系统减轻了负担。
        (3)当CPU 数量增加后相应也要增加物理内存的数量,因为JVM 中有并行垃圾收集器。 

 

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

Java 堆数据结构定义 java中的堆数据结构_Java 堆数据结构定义_07


 

 GC 的默认使用情况:

 

新生代

老年代/永久代

Client

串行收集器

串行收集器

Server

并行压缩收集器

CMS

        最近再仔细研究下Java 堆栈方面的细节,发现网上很多文章有很多错误的地方,我尽量去查阅官方的说法,做到大部分正确不会误导大家,如果出现哪些错误可以及时提出。

 

        疑惑:

        1.有的朋友会对Heap 范围这个概念有所疑惑,在Java 虚拟机规范中有这样一段原文:


The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This version of the Java Virtual Machine specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.


 

        大致意思是方法区逻辑意义上属于Heap(堆),但通常的做法是将两者分开,从而更加便于管理。

        也就是说方法区在虚拟机规范中被当作堆的一部分,但是在具体实现中往往是将两者分开,并称为"Non-Heap"(非堆区)。

        所以广义的Heap=heap + method area(老年代+新生代+方法区)。

        狭义的Heap(也就是HotSpot JVM 实现)指的就是堆内存,其中分为老年代和新生代。

        通常情况下我们常用的是狭义Heap,所以大家在文章未声明的情况下都可以认为Heap 所指即为JVM 中的堆内存,用来存储对象的。

 

        2.对于习惯在HotSpot 虚拟机上开发和部署程序的开发者来说,很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot 虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9等)来说是不存在永久代的概念的。即使是HotSpot虚拟机本身,根据官方发布的路线图信息,现在也有放弃永久代并“搬家”至Native Memory 来实现方法区的规划了。