堆的核心概述
对一个进程而言是唯一的。一个进程对应一个JVM实例、一个JVM实例拥有一个运行时数据区,仅拥有一个堆结构,也是Java内存管理的核心区域。
Java堆区在JVM启动时便被创建,并且设定好了大小。是JVM中最大的一块内存空间(可调节)
通过Java Visual JVM查看 通过 -Xms…m -Xmx…m
堆在物理上可以存在不连续的内存空间,但逻辑上应当视为连续的。
所有线程共享堆。堆还能划分为线程私有的缓冲区(TLAB,Thread Local Allocation Buffer)
所有的对象实例 以及数组 都应当在 运行时 分配在堆上。
数组和对象可能永远都不会存储在栈上,因为栈帧保存引用,而这个引用指向对象或者数组在堆中的位置。
在方法结束后,堆中的对象不会马上移除,而是等待垃圾收集时移除。
堆 是 垃圾收集器(GC,Garbage Collection)执行垃圾回收的重点区域。
内存细分
现代的垃圾收集器大致基于 分代收集器理论设计。
1.JAVA7 之前:新生区+养老区+永久区
2.Java8:新生去 + 养老区 +元空间
元空间是逻辑上属于堆区。实际存储在方法区
设置堆内存大小与OOM
1.设置堆空间的大小:
-Xms 用于表现堆区(年轻代+养老代)的起始内存,等价于-XX:InitialHeapSize 【ms memory start】
-Xmx 用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区的内存大小超过了—Xmx 所指定的最大内存,将会抛出OOM(OutOfMemoryError)异常。
通常会将 两个参数 配置相同的值,目的:在Java垃圾回收机制清理完堆区后不需要重新分割计算区间的大小,以此提高性能。
查看设置的参数
jps
jstat -gc 进程id
or
+XX:+PrintGCDetails
OOM错误
对象与数组创建超过堆空间可容纳空间
堆区的结构
在JVM中的java对象可以划分成两类:
生命周期较短的瞬时对象(创建与消亡都很快)
生命周期很长,甚至于JVM生命周期保持一致的对象。
java堆区进一步划分又可以分为年轻代(YoungGen) 和 老年代(OldGen)
其中年轻代又可划分为Eden空间、Survior0与Survivor1空间【也可称为form区、to区】
配置新生代与老年代在堆结构中的占比
默认: -XX:NewRatio = 2 [新生代占1,老年代占2.即新生代占据整个堆的1/3]
通过 -XX:NewRation = ? 进行修改。eg: = 3。即新生代占1,老年代占3.所以新生代占整个堆的1/4
配置新生区中Eden空间与两个Survivor空间缺省占比。 默认比例是8:1:1。可以通过 -XX:SurvivorRatio = ? 来进行调整。还可以通过-Xmn 来设置新生代最大内存大小。
几乎所有的Java对象都是在Eden区被New出来的。也大多在新生代进行销毁。(大部分对象的使用周期短)
对象分配过程
为新对象分配内存是一项复杂的任务。JVM不仅需要考虑内存如何分配、哪里分配。还需考虑内存分配与内存回收。因此要考虑GC执行完内存回收后是否会在内存空间产生内存碎片。
对象分配的流程
1.new的对象将存放于Eden区。
2.当Eden区填满,而程序又需要创建对象时,JVM的垃圾回收器会对Eden区进行垃圾回收[Minor GC],将Eden区中不再被其他对象所引用的对象进行销毁。再加载新的对象放到Eden区。
3.将Eden区剩余对象 移动到 Servivor0区。
4.若后续再次触发垃圾回收,则上次幸存下来的对象就会被放到Servivor0区。若无回收,便会存放到Servivor1区。
5.如果在经历垃圾回收,此时将会被放回Servivor0区,再去Servivor1区。
此时Servivor两个区,也可以被称为 TO区 和 Form区
TO:下一次垃圾回收的幸存者往哪里存放的区。【每当幸存一次,则会对幸存者赋值+1】
6.何时从新生代去老年代?
默认为幸存15次。可以通过-XX:MaxTenuringThreshold=<N>
进行设置
初始GC机制
GC按照回收区域大致可分为两种类型。
部分收集(Partial GC):不完整收集整个Java堆的垃圾收集
新生代收集(Minor GC):仅针对新生代收集
老年代收集 (Major GC):仅针对老年代收集
1.注意 此处Major GC 时常会和 Full GC混淆使用。具体需要分辨为。老年代回收还是整堆回收
2.只有CMS GC会单独收集老年代的行为。
混合收集
整堆收集(Full GC):针对整个新生代以及部分老年代的垃圾收集。
Minor GC 触发机制:
1.当 年轻代的Eden 空间不足时,便会触发。 【特别注意的是:只有Eden区满才会触发,S区满不会触发】
2.由于Java对象大多 具有 使用周期短 的 特性。因此Minor GC非常频繁,但回收速度也较快。
3.Minor GC 会引发 STW。暂停其他用户的线程,等垃圾回收结束,用户线程才恢复运行。
Major GC 触发机制:
1.老年区满时,便会触发Major GC
2.当出现Major GC时,通常会伴随至少一次MInor GC。即当老年区满时,会先尝试Minor GC。在此之后空间仍然不足,则会触发Major GC
3.Major GC得速度相较Minor GC会慢许多(10倍以上),STW时间更长。
4.Major GC后,内存不足,则会报OOM。
Full GC触发机制:
1.调用System.gc()
2.老年代空间不足
3.方法区空间不足
4.通过Minor GC后进入老年代的平均大小 大于 老年代的可用内存
5.由Eden区、From区向To区复制时,对象大小 大于To区可用内存,则会将该对象转存老年代,且老年代的可用内存小于 该对象大小。
-Full GC时开发或调优时尽量避免的。-
堆空间分代思想
在java开发中,不同对象的生命周期不同。但大致都是临时对象。
为什么要进行分代?
分代的唯一理由便是优化GC性能。
若没有分代,则所有对象都在一起。GC时,要分辨哪些对象需要回收,则将对堆的所有区域进行扫描。而许多对象都是临时对象,将加大GC时间。
若分代,则新创建的对象放到某一地方,而GC时,优先对临时对象的区域进行回收,则效率大大提高。
内存分配策略
如果对象在Eden区出生,并通过第一次MInor GC依然存活,且能被Survivor区容纳,则将被移动到Survivor空间中,并且对象年龄将设为1. 对象每经历一次Minor GC,年龄变+1岁。到一定程度时,便晋升为老年区。【默认为15次,可通过-XX:MaxTenuringThreshold
设置】
针对不同年龄的对象分配原则:
-
优先分配至 Eden
-
大对象直接分配到老年区【即在开发时,需避免出现过多的大对象】
-
长期存活的对象分配到老年区。
-
动态对象年龄判断
如果Survivor区中相同年龄的所有对象大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可直接进入老年区。
-
空间分配担保 可通过
-XX:HandlePromotionFailure
为对象分配内存:TLAB
为什么需要TLAB?
由于堆是线程共享区域,任何的线程都可以访问堆区中的共享数据。而对象实例的创建频繁,导致在并发环境中从堆区划分内存空间是不安全的。
为避免多个线程操作同一地址,则需要使用锁等机制,便会影响分配速度。
什么是TLAB(Thread Local Allocation Buffer)?
从内存模型的角度,对Eden区继续划分,JVM为每个线程分配了一个私有缓存的区域。
此时,当多线程同时分配内存时,使用TLAB可以避免一系列线程安全问题。同时还可以提升内存分配的吞吐量。而这种内存分配方式叫作 快速分配策略。
1.并非是所有的对象实例都分配在TLAB,但JVM将TLAB作为内存分配的首选。
2.开发人员 可用过
-XX:UserTLAV
设置开启TLAB空间3.默认情况下,TLAB空间的内存非常小。可以通过
-XX:TLABWasteTargetPercent
设置TLAB空间在Eden的占比百分比。4.一旦对象在TLAB空间分配内存失败时。JVM就是尝试通过 加锁机制 确保数据操作的原子性。从而直接在Eden区中分配内存。
TLAB中对象分配过程:
堆空间的整个流程:
在发生Minor GC前,JVM会 检查老年代最大可用的连续空间 是否大于 新生代所有对象总空间。
大于:Minor GC安全
小于:JVM查看-XX:HandlePromotionFailure
设置值是否允许担保失败。
若为true,则继续老年代最大可用的连续空间 是否大于 历次晋升老年代的对象平均大小。
如果大于,则尝试Minor GC,但仍有风险。
如果小于,则进行一次Full GC。
若为false,则进行一次Full GC