Java堆空间(Heap Space)

  • Post author:
  • Post category:java


Java 堆空间(Heap Space)

概述

在Java程序中,堆是JVM内存空间中最大的一块,同时我们知道,每个线程都拥有一个虚拟机栈,但是堆不同,Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。

在《Java虚拟机规范》中对Java堆的描述是:“所有 的对象实例以及数组都应当在堆上分配“,但是实际情况是几乎所有的对象都是分配在堆空间的,也有少部分情况比较特殊。这是因为由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。

《Java虚拟机规范》里对Java堆进行了更进一步的细致划分:“Java虚拟机的堆内存分为新生代、老年代、永久代、Eden、Survivor……”,并且会根据区域的不同设计不同的垃圾回收期(GC)。


总结一下要点:

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

  • Java堆区在JVM启动的时候即被创建,其空间大小也就确定了,堆是JVM管理的最大一块内存空间,并且堆内存的大小是可以调节的。

  • 《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  • 所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,简称 TLAB)。

  • 《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。

  • 从实际使用角度看:“几乎”所有的对象实例都在堆分配内存,但并非全部。因为还有一些对象是在栈上分配的(逃逸分析,标量替换)

  • 数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

  • 也就是触发了GC的时候,才会进行回收

  • 如果堆中对象马上被回收,那么用户线程就会收到影响,因为有stop the word

  • 堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

Java堆空间结构

存储在JVM中的Java对象可以被划分为两类:

  • 一类对象的生命周期较短,这种对象的创建和消亡都十分迅速

  • 另一类对象的生命周期很长,在某些极端情况下甚至可以和JVM的生命周期保持一致

Java堆区进一步细分的话,可以划分为

年轻代(YoungGen)和老年代(oldGen)

其中年轻代又可以划分为

Eden空间、Survivor0空间和Survivor1空间

(有时也叫做from区、to区)

  • 在默认的情况下(可以根据实际情况修改设置),新生区占堆空间的三分之一,老年代占堆空间的三分之二。

  • 在HotSpot中,Eden区空间和其他两个Survivor区空间的默认比例是 8 : 1 : 1。同时开发人员可以通果设置选项-XX:SurvivorRatio调整这个空间的比例。

  • 大部分对象都是在Eden区中被创建出来的。

  • 绝大多数的Java对象都在新生代中销毁(朝生暮死)

对象分配过程

对象分配是一个严谨且复杂的过程, 设计者需要考虑内存的分配,以为实际的分配与垃圾回收算法密切相关。


流程说明:

  • 创建出的新对象正常先放到Eden区,但是要判断Eden空间是否足够。

  • 如果足够,就放入Eden区。

  • 如果Eden区空间不足,会对Eden区进行垃圾回收(Minor GC),将伊甸区中不被引用的对象进行销毁操作,将新创建的对象放入Eden区。

  • 同时将Eden区存活的对象移动到Servivor0区

  • 如果之后触发垃圾回收机制,在Servivor0区中存活的对象会放到Servivor1区中,在经历垃圾回收机制Servivor1区存活的对象就在移动到Servivor0区,同时对象有一个”年龄”就是经历垃圾回收的次数,当经历过15次GC时,就会将这个对象移动到Old区。

  • 对于S0和S1区来讲:复制有交换,谁空谁是to

  • 如果Eden区内经历过GC后存活下来的对象转移到Servivor区,但是Servivor存放不下,就将这个对象移动到Old区

  • 在Old区,GC的次数相对少一些,当Old区不足时进行Major GC。

  • 如果进行了Major GC后仍然无法将对象进行储存,就会报OOM


流程图:

关于GC的说明(Minor GC、Major GC、Full GC)

  1. JVM的调优的一个环节,也就是垃圾收集GC,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现STW(Stop the World)的问题,而 Major GC 和 Full GC出现STW的时间,是Minor GC的10倍以上。

  1. JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)

  • 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为:

  • 新生代收集(Minor GC/Young GC):只是新生代(Eden,s0,s1)的垃圾收集

  • 老年代收集(Major GC/Old GC):只是老年代的圾收集。

  • 目前,只有CMS GC会有单独收集老年代的行为。

  • 注意,很多时候Major GC会和Full GC混淆使用,需要具体分辨是老年代回收还是整堆回收。

  • 混合收集(Mixed GC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为

  • 整堆收集(Full GC):收集整个java堆和方法区的垃圾收集。

MinorGC

  • 当年轻代空间不足时,就会触发Minor GC,这里的年轻代满指的是Eden代满。Survivor满不会主动引发GC,在Eden区满的时候,会顺带触发s0区的GC,也就是被动触发GC(每次Minor GC会清理年轻代的内存)

  • 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。这一定义既清晰又易于理解。

  • Minor GC会引发STW(Stop The World),暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行

Major/Full GC

  1. 指发生在老年代的GC,对象从老年代消失时,我们说 “Major Gc” 或 “Full GC” 发生了

  1. 出现了MajorGC,经常会伴随至少一次的Minor GC。(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程)

  • 也就是在老年代空间不足时,会先尝试触发Minor GC,如果之后空间还不足,则触发Major GC

  1. Major GC的速度一般会比Minor GC慢10倍以上,STW的时间更长。

  1. 如果Major GC后,内存还不足,就会报OOM


Major GC通常是跟full GC是等价的,收集整个GC堆。但因为HotSpot VM发展了这么多年,外界对各种名词的解读已经完全混乱了,当有人说“major GC”的时候一定要问清楚他想要指的是上面的full GC还是old GC。


Partial GC(部分GC):

并不收集整个GC堆的模式

  • Young GC:只收集young gen的GC。

  • Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式。

  • Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式。


Full GC:

收集整个堆,包括young gen、old gen、perm gen永久代 (如果存在的话)等所有部分的模式。

  • young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。

  • full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

为对象分配内存 (TLAB)

为什么要有TLAB

  • 堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据

  • 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的

  • 为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

如果有了TLAB,每个线程将自己要操控的对象放到自己的TLAB区域,就能在一定程度上避免了线程安全问题。

TLAB说明

  1. 从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。

  1. 多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

  1. 很多OpenJDK衍生出来的JVM都提供了TLAB的设计。

  1. 尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。

  1. 在程序中,开发人员可以通过选项-XX:UseTLAB设置是否开启TLAB空间。

  1. 默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

  1. 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。


TLAB分配流程图:



版权声明:本文为qq_38270106原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。