堆在 JVM 启动时被创立,大小也随之确定,是 JVM 最大、最重要的一块内存空间。堆在物理上不间断,而在逻辑上间断。尽管堆是每个线程共享,但 也能够划分线程公有的缓冲区
。
简直所有的对象实例和数组都调配在堆中,少部分调配在栈中。对象和数组可能永远不会存储在栈上,栈帧中仅保留援用,而这援用指向堆中的地位。在栈帧完结后,堆中的对象不会马上被移除,仅在垃圾回收时才被移除。
虚拟机的办法区和堆空间,是每个线程共用的,其余数据区为每个线程公有,公有数据区颗粒度较细,难有优化的空间。而共用的数据区,内存调配时能失去较多的内存,所以该数据区是 GC 时的重点。
新生代和老年代
在 Java7 以前,堆空间在逻辑上划分为:新生区 + 老年区 +永恒代
;在 Java8 及之后分为:新生区 + 老年区 + 元空间
永恒代和元空间,是办法区的具体实现,办法区又叫 non-heap,所以永恒代和元空间实际上不归属于堆,只是逻辑划分而已。
存储在 JVM 的 Java 对象能够被划分成两类,一类是生面周期比拟短的刹时对象,这些类的创立和沦亡都十分迅速;另一类对象的生命周期却十分长,在某些极其状况下,还可能与 JVM 的生命周期放弃始终,如 Runtime
类
为什么要进行内存分带?大量数据表明,不同对象的生命周期不同,70%~90% 的对象时长期对象,对对象进行分代能够更有目标的进行 GC,优化 GC 性能。
《2020 最新 Java 根底精讲视频教程和学习路线!》
新生代
新生代能够划分为 Eden
、Survivor0
和Survivor1
,Survivor0 和 Survivor1 有时也叫作 from、to。
- Eden:对象 (简直所有) 最开始创立的地位。
- Survivor0 和 Survivor1:Eden 区没有被 GC 革除的对象会被放到 S0 和 S1 中,S0 和 S1 大小统一,只有一个空间用来存储对象,另一个区用来全量 GC。
绝大部分的 Java 对象都是在新生代销毁,IBM 钻研表明,新生代 80% 的对象都是朝生夕死。
老年代
堆空间除了新生代就是老年代啦。。
对象在堆中的调配过程
1、对象最开始先被调配到 Eden 区,Eden 区满了之后会进行 Young GC,存活下来的对象会被挪动到 Survivor 区
。每个对象都有一个 年龄计数器
,在 Eden 区为 0,被挪动到 S 区则变为 1。 S 区满不会触发 YGC
。
2、存活在 S 区的对象,在通过第二次 GC 之后能存活下来的,年龄计数器会持续 +1。当 年龄计数器达到 15 后,对应的对象会被挪动 (promotion) 至老年区
。该值能够通过 -XX:MaxTenuringThreshold=15 进行设置。
3、当对象占用的空间太大,YGC 前任然超过了 Eden 区的空间时,会间接将该对象调配到老年代。老年代放不下则进行 Full GC,GC 前任然放不下则 OOM。存活在 Eden 区的对象在 YGC 后,S 区放不下,则会间接调配到老年代。
与堆内存设置相干的参数
-Xms
,示意堆的起始内存(新生代和老年代)
全称为 -XX:InitialHeapSize
X 是 JVM 的运行参数,ms 是 memory start。默认单位为字节,有 k、m、g
-Xmx
,示意堆的最大内存
全称为 -XX:MaxHeapSize
默认的堆空间大小:
-Xms = 计算机内存大小 /64
-Xmx = 计算机内存大小 /4
一旦超过了 -Xmx 所制订的最大内存时,将会抛出OutOfMemoryError
。通常会将两个参数设置成雷同的值,让垃圾回收完后,不须要从新分隔计算堆区的大小,进而进步性能。
能够通过以下程序查看 JVM 的堆大小:
// -Xms 堆的起始值,s0,s1 区只算其中的一个
long initalMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024
// -Xmx 堆的最大值
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024
复制代码
敞开自适应的内存调配策略
-XX:-UseAdaptiveSizePolicy:- 为敞开,+ 为开启,默认开启
配置新生代和老年代在对构造的占比
- 默认
-XX:NewRatio=2
,示意新生代占 1,老年代占 2,新生代占整个堆的 1 /3;能够批改为 -XX:NewRatio=4,示意新生代占 1,老年代占 4,新生代占整个堆的 1 /5 - 在 HotSpot 中,Eden 和 S0、S1 的默认比例为 8:1:1,能够通过
-XX:SurvivorRatio
= 8 调整比例
配置新生代的最大内存大小
-Xmn
,个别默认即可,有设置就以这个参数为准。
与堆相干的 GC
JVM 在 GC 时,并不是整体进行垃圾收集,而是频繁收集新生代,较少收集老年代,简直不在永恒代 / 元空间收集。
- Minor GC,只对新生代进行垃圾收集,当 Eden 区内存不足时会触发,Survivor 区满不会 GC。
- Major GC,只对老年代进行垃圾收集
目前只有 CMS GC 会有独自收集老年代的行为
很多时候 Major GC 会和 Full GC 混同应用,须要具体分辨是老年代回收还是整堆回收
Major GC 常常会随同 Minor GC,也就是在老年代空间有余时,会尝试先触发 Minor GC,之后空间还有余,则触发 Major GC。Major GC 的速度比 Minor GC 慢十倍以上,STW 工夫也更长。
尽量避免
- Mixed GC,收集新生代和局部老年代的垃圾,只呈现于 G1 GC
- Full GC,收集整个 Java 堆和办法区的垃圾
引起 Full GC 的几种状况:调用 System.gc()时,零碎倡议执行 Full GC,然而不必然执行;老年代空间有余;办法区空间有余;Minor GC 后进入老年代的均匀大小大于老年代的可用内存;老年代放不下从 Eden 区过去的大对象;
尽量避免
堆内存调配策略
- 优先调配到 Eden 区
- 大对象间接调配到老年代,防止程序中呈现过多的大对象
- 长期存活的对象调配到老年代
- 动静对象年龄判断,
如果 Survivor 区中雷同年龄的所有对象大小总和大于 S 区空间的一半,年龄大于等于的对象能够间接进入老年代,无需等到 MaxTenuringThreshold 要求的年龄
- 空间调配担保,-XX:HandlePromotionFailure
堆内存的线程公有空间:TLAB
TLAB:ThreadLocal Allocation Buffer
堆区时线程共享的区域,任何线程都能够拜访到堆区中共享数据。因为对象实例创立在 JVM 中十分频繁,因而在并发环境下,从堆区划分内存空间是线程不平安的。为了防止多个线程操作同一地址,须要加锁等机制,加锁影响调配速度。因而有必要为每个线程独自调配一块区域存放数据,该区域存在于 Eden 区中,这种策略也叫 疾速调配策略
。
只管不是所有的对象实例都可能在 TLAB 中胜利分配内存,但 JVM 的确将 TLAB 作为内存调配的首选
。能够通过-XX:UseTLAB
设置是否开启,默认开启。默认状况下,TLAB 空间十分小,只占 Eden 区的 1%,能够通过 -XX:TLABWasteTargetPercent
进行设置占用的百分比。
一旦对象在 TLAB 调配失败时,JVM 会尝试加锁确保数据操作的原子性,从而间接在 Eden 区调配。