堆的认识

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



 

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

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

       

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

Java代码   收藏代码
  1. 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(非堆内存):

 

 

 

        Heap Memory 又被分为两大区域:

        - Young/New Generation 新生代

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

        - 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:幸存者区域是新生代与老年代的缓冲区域,两块幸存者区域分别为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 清理过程(图中标X为垃圾):
        1.清理之前



 

        2.清理之后


        注意:图中的"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 的各个过程:



 

         从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)。与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老年代/永久代GC
Client串行GC串行GC
Server并行回收GC并发GC(CMS)


        下篇文章用来分析断代法GC。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值