JVM学习笔记—JVM运行时内存之堆空间
1. Java 堆简介
- 对于Java应用程序来说, Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。
- Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。
- 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。
- “几乎”是指从实现角度来看, 随着Java语 言的发展, 现在已经能看到些许迹象表明日后可能出现值类型的支持, 即使只考虑现在, 由于即时编译技术的进步, 尤其是逃逸分析技术的日渐强大, 栈上分配、 标量替换优化手段已经导致一些微妙的变化悄然发生, 所以说Java对象实例都分配在堆上也渐渐变得不是那么绝对了。
2. Java 堆的特点
- 是Java虚拟机所管理的内存中最大的一块。
- 堆是jvm所有线程共享的。
堆中也包含私有的线程缓冲区 Thread Local Allocation Buffer (TLAB),这部分是私有
- 在虚拟机启动的时候创建。
- 唯一目的就是存放对象实例,几乎所有的对象实例以及数组都要在这里分配内存。
- Java堆是垃圾收集器管理的主要区域。
- 因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、From Survivor空间、To Survivor间。
- java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
- 方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
- 如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
3. 设置堆空间的大小
- 内存大小设置:-Xmx(最大)/-Xms(最小)
4. 堆的分类
- 现在垃圾回收器都使用分代理论,堆空间也分类如下:
- JDK7中将java的堆空间分为以下几个结构:
青年代
:Young Generation,Eden区,S0区,S1区老年代
:Old Generation永久代
:Permanent Generation
- 在Java8以后,由于方法区的内存不在分配在Java堆上,而是存储于本地内存元空间Metaspace中,所以永久代就不
存在了,在几天前(2018年9约25日)Java11正式发布以后,关于Java11中垃圾收集器的官方文档,文档中没有提到“永久代”,而只有青年代和老年代。 - 原来对应的永久代被换到了本地内存中的元空间
5. 堆的年轻代和老年代
- JVM中存储java对象可以被分为两类:
年轻代(Young Gen)
:年轻代主要存放新创建的对象(在Eden区创建对象),内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。年老代(Tenured Gen)
:年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。- 年轻代和老年代大小的比例是1:2
- 年轻代中Eden,S0,S1的比例是8:1:1
6. 堆中年轻代和老年代结构占比
- 默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
- 修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5
- Eden空间和另外两个Survivor空间占比分别为8:1:1
- 可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8
- 几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁.
- 由下图可知:
- 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
- 默认来说,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。
- 新生代 ( Young ) 被细分为 Eden 和 两个Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
- 默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
- JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
7. 堆中对象位置分配过程
- JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收后是否存在空间中间产生内存碎片。
分配过程
:- new的对象先放在伊甸园区。该区域有大小限制
- 当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区
- 然后将伊甸园区中的剩余对象移动到幸存者0区
- 如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区
- 如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区,这样新生代每次垃圾的年龄都会+1,而且要保证每次S0或者S1区中都有一个是空的。
- 如果累计次数到达默认的15次,这会进入养老区。可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
- 养老区内存不足是,会再次出发GC:Major GC 进行养老区的内存清理
- 如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常.
8. 分配对象的流程
给创建的新对象分配区域的过程:
- 创建一个新的对象,需要申请对应的内存,新创建的对象我们要放在Eden区
- 判断Eden区有没有对应的空间?有空间的话我们正常存,如果Eden区空间不足就要清理一下Eden区,进行垃圾回收,执行MinorGC也叫YGC
- 清理完之后查看Eden区是否放得下?如果能就放入Eden,如果放不下只能把这个对象放到老年区了
- 如果老年区放得下我们依旧会分配内存,如果老年区还放不下说明这个对象特别大,这时候要清理一下老年区,执行一个Full GC(FGC)的操作,FGC会将年轻代和老年代都清理
- FGC清理完之后看还能不能放得下?如果能就分配内存放到老年区了,如果放不下就报错OOM
现在看一下YGC的过程(给垃圾分配区域的过程):
- 在Eden区放不下,要进行YGC,先查看Servivor区是否有足够的内存,如果有就放到S0/S1区,否则放到老年代
- 判断幸存者区中的独享是否超过阈值,没超过则放入年轻代,否则放入老年代
- 如果老年代还放不下就要报错OOM了
10. 堆GC
-
Java 中的堆也是 GC 收集垃圾的主要区域。
-
GC 分为两种:
一种是部分收集器(Partial GC)另一类是整堆收集器(Fu’ll GC) -
部分收集器:
不是完整收集java堆的的收集器,它又分为:- 新生代收集(Minor GC / Young GC): 只是新生代的垃圾收集
- 老年代收集 (Major GC / Old GC): 只是老年代的垃圾收集 (CMS GC 单独回收老年代)
- 混合收集(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收, region区域回收)
-
整堆收集(Full GC):
收集整个java堆和方法区的垃圾收集器,但是回收速度也会非常慢 -
年轻代GC触发条件
:- 年轻代空间不足,就会触发Minor GC, 这里年轻代指的是Eden代满,Survivor满也不会引发GC
- Minor GC会引发STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复
-
老年代GC (Major GC)触发机制
- 老年代空间不足时,会尝试触发MinorGC(YGC). 如果空间还是不足,则触发Major GC
- 如果Major GC , 内存仍然不足,则报错OOM
- Major GC的速度比Minor GC慢10倍以上.
-
FullGC 触发机制:
- 调用System.gc() , 系统会执行Full GC ,不是立即执行.
- 老年代空间不足
- 方法区空间不足
- 通过Minor GC进入老年代平均大小大于老年代可用内存
-
注意:
条有的过程中尽量减少FGC的执行