堆
堆(Heap)概述
1、堆针对一个 JVM 进程是唯一的
(1)即一个进程只有一个 JVM
(2)进程包含多个线程,多个线程共享同一堆空间
(3)一个 JVM 实例只存在一个堆内存,堆是 Java 内存管理的核心区域
(4)所有线程共享 Java 堆,但还可以划分出线程私有的缓冲区(TLAB:Thread Local Allocation Buffer)
2、Java 堆区在 JVM 启动时即被创建,同时确定其空间大小
(1)是 JVM 管理的最大一块内存空间
(2)堆内存的大小是可以调节的
3、JVM 规范
(1)规定堆可以处于物理上不连续的内存空间,但在逻辑上被视为连续
(2)对 Java 堆的描述:所有对象实例、数组都应当在运行时分配在堆上
(3)数组、对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象 / 数组在堆中的位置
4、GC:Garbage Collection,垃圾收集器
(1)堆是执行垃圾回收的重点区域
(2)在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集时才会被移除
堆内存细分
1、Java 7 及之前堆内存逻辑上分为三部分:新生代 + 老年代 + 永久代
2、Java 8 及之后堆内存逻辑上分为三部分:新生代 + 老年代 + 元空间
3、新生区再划分
(1)伊甸园(Eden Space)
(2)幸存者区(Survivor)
4、实际上
(1)堆空间 = 新生代 + 老年代
(2)永久代、元空间属于方法区
堆空间大小的设置
1、Java 堆区用于存储 Java 对象实例,则堆的大小在 JVM 启动时就已经设定
2、通过选项 -Xmx、-Xms 设置堆大小
(1)-Xms:表示堆区的起始内存,等价于 -XX:InitialHeapSize
(2)-Xmx:表示堆区的最大内存,等价于 -XX:MaxHeapSize
(3)一旦堆区中的内存大小超过 -Xmx 所指定的最大内存时,将会抛出 OutOfMemoryError 异常
(4)通常会将 -Xms 和 -Xmx 两个参数配置相同的值,为了能在 Java 垃圾回收机制清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能
3、默认情况下
(1)初始内存大小 = 物理内存大小 / 64
(2)最大内存大小 = 物理内存大小 / 4
新生代、老年代
1、存储在 JVM 中的 Java 对象可以被划分为两类
(1)生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
(2)对象的生命周期非常长,在某些极端的情况下能与 JVM 的生命周期保持一致
2、新生代
(1)Eden Space
(2)Survivor0、Survivor1,或称 from 区、to 区
3、堆结构占比
(1)-XX:NewRatio 选项:设置新生代、老年代在堆空间的占比,一般使用默认值
(2)默认 -XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整个堆的 1 / 3
(3)-XX:SurvivorRatio 选项:设置伊甸园、幸存者区在新生代的占比,一般使用默认值
(4)在 HotSpot 默认情况下,Eden Space : Survivor0 : Survivor1 = 8 : 1 : 1
4、生命周期
(1)几乎所有 Java 对象都在 Eden 区被 new 出来
(2)绝大部分 Java 对象的销毁都在新生代进行
(3)新生代中 80% 对象生命周期较短
(4)-Xmn 选项:设置新生代最大内存大小,一般使用默认值
为新对象分配内存
1、问题
(1)内存分配大小
(2)内存分配位置
(3)GC 执行完内存回收后,是否会在内存空间中产生内存碎片
2、Eden 分配过程
(1)new 对象先放 Eden,此区有大小限制
(2)当 Eden 填满时,程序又需要创建对象,JVM 垃圾回收器将对 Eden 进行垃圾回收(Minor GC),销毁 Eden 中的不再被其他对象所引用的对象,加载新对象到 Eden
(3)然后将 Eden 中的剩余对象,移动到 Survivor0,被移动对象的年龄 + 1
(4)如果 Eden 再次填满,创建新对象触发垃圾回收,销毁 Eden、Survivor0 中的不再被其他对象所引用的对象,加载新对象到 Eden
(5)然后将 Eden 中的剩余对象、Survivor0 对象,移动到 Survivor1,被移动对象的年龄 + 1
(6)如果再次触发垃圾回收,销毁 Eden、Survivor1 中的不再被其他对象所引用的对象,加载新对象到 Eden
(7)然后将 Eden 中的剩余对象、Survivor1 对象,移动到 Survivor0,被移动对象的年龄 + 1
(8)以此类推
3、老年代分配过程
(1)对象年龄超过 -XX:MaxTenuringThreshold(默认值 15),移动到老年代
(2)Minor GC 后,Survivor 不能存放移动自 Eden 的对象,移动到老年代
(3)Minor GC 后,Eden 不能存放新对象,移动到老年代
(4)当老年代内存不足时,触发 GC(Major GC)清理老年代内存
(11)若老年代执行 Major GC 后,仍无法进行对象的保存,就会产生 OOM 异常
4、全流程图
常用调优工具
1、JDK 命令行
2、Eclipse:Memory Analyzer Tool
3、Jconsole
4、VisualVM
5、Jprofiler
6、Java Flight Recorder
7、GCViewer
8、GC Easy
Minor GC,Major GC、Full GC
1、JVM 在进行 GC 时,并非每次都对上面三个内存区域一起回收
2、针对 Hotspot VM 实现,其中 GC 按照回收区域,分为两大种类型
(1)部分收集(Partial GC)
(2)整堆收集(Full GC)
3、部分收集:不是完整收集整个 Java 堆垃圾
(1)新生代收集(Minor GC / Young GC):只回收新生代垃圾
(2)老年代收集(Major GC / Old GC):只回收老年代垃圾
(3)只有 CMS GC 会有单独收集老年代的行为
(4)Major GC 会与 Full GC 混淆使用,需要具体分辨是老年代回收还是整堆回收
(5)混合收集(Mixed GC):回收整个新生代、部分老年代的垃圾
(6)只有 G1 GC 会混合收集
4、整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集
Minor GC / Young GC 触发机制
1、当年轻代空间不足时,触发 MinorGC
(1)年轻代满指 Eden 满
(2)Survivor 满不会引发 GC
2、大多数 Java 对象生命周期短,Minor GC 非常频繁,一般回收速度比较快
3、Minor GC 引发 STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行
Major GC / Full GC
1、出现 Major Gc,经常会伴随至少一次 Minor GC,但非绝对
(1)在 Parallel Scavenge 收集器的收集策略中,有直接进行 MajorGC 的策略选择过程
(2)在老年代空间不足时,会先尝试触发 Minor GC,如果之后空间还不足,则触发 Major GC
2、Major GC 速度一般会比 Minor GC 慢 10 倍以上,STW 时间更长
3、如果 Major GC 后,内存仍不足,则抛出 OOM 异常
4、Full GC 触发机制
(1)调用 System.gc() 时,系统建议执行 Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)Minor GC 后,对象大小大于 To 区可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小
5、在 JVM 调优中需要避免 Major GC / Full GC
堆空间分代思想
1、不同对象的生命周期不同,70% - 99% 对象是临时对象
2、分代唯一目的:优化 GC 性能
(1)如果没有分代,则所有对象都在一块,GC 会对堆的所有区域进行扫描
(2)如果分代,GC 先对新生代回收
内存分配策略
1、针对不同年龄段的对象分配原则
(1)优先分配到 Eden
(2)大对象直接分配到老年代,应尽量避免程序中出现过多的大对象
(3)长期存活的对象分配到老年代
(4)动态对象年龄判断:如果 Survivor 区中,相同年龄的所有对象的大小总和,大于 Survivor 空间一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
(5)空间分配担保:-XX:HandlePromotionFailure
为对象分配内存:TLAB
1、Thread Local Allocation Buffer:线程本地分配缓存区
2、原因
(1)堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
(2)由于对象实例的创建在 JVM 中非常频繁,因此在并发环境下,从堆区中划分内存空间是线程不安全的
(3)为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度
3、作用
(1)从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,JVM 为每个线程分配了一个私有缓存区域,它包含在 Eden 空间内
(2)快速分配策略:多线程同时分配内存时,使用 TLAB 可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量
(3)所有 OpenJDK 衍生 JVM 都提供 TLAB 设计
4、设置
(1)尽管不是所有的对象实例都能够在 TLAB 中成功分配内存,但 JVM 将 TLAB 作为内存分配的首选
(2)-XX:UseTLAB 选项:设置是否开启 TLAB 空间
(3)默认情况下,TLAB 空间的内存非常小,仅占有整个 Eden 空间 1%,
(4)-XX:TLABWasteTargetPercent 选项:设置 TLAB 空间所占用 Eden 空间的百分比大小
(5)一旦对象在 TLAB 空间分配内存失败时,JVM 尝试使用加锁机制,确保数据操作的原子性,从而直接在 Eden 空间中分配内存
Minor GC 之前
1、虚拟机检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间
2、如果大于,则此次 Minor GC 安全
3、如果小于,则虚拟机查看 -XX:HandlePromotionFailure 设置值,是否允担保失败
4、如果 HandlePromotionFailure=true,则继续检查老年代最大可用连续空间,是否大于历次晋升到老年代的对象的平均大小
(1)如果大于,则尝试进行一次 Minor GC,但这次 Minor GC 仍有风险
(2)如果小于,则改为进行一次 Full GC
5、如果 HandlePromotionFailure=false,则改为进行一次 Full GX
6、在 JDK 6 Update24 之后
(1)HandlePromotionFailure 参数不再影响虚拟机的空间分配担保策略,,虽然源码中定义 HandlePromotionFailure 参数,但在代码中已经不会再使用它
(2)规则:只要老年代的连续空间,大于新生代对象总大小,或历次晋升的平均大小,就会进行 Minor GC,否则将进行 Full GC
堆不是分配对象的唯一选择
1、深入理解 Java 虚拟机:随着 JIT 编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,使所有的对象都不绝对分配到堆上
2、一般情况下,对象在 Java 堆中分配内存
(1)特殊情况:如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法,则可能被优化成栈上分配,无需在堆上分配内存,也无须进行垃圾回收,这是最常见的堆外存储技术
(2)TaoBaoVM,其中创新的 GCIH(GC invisible heap)技术实现 off-heap,将生命周期较长的 Java 对象从 heap 中移至 heap 外,并且 GC 不能管理 GCIH 内部的 Java 对象,以此达到降低 GC 的回收频率和提升 GC 的回收效率的目的
逃逸分析概述
1、将堆上的对象分配到栈,需要使用逃逸分析手段
2、一种有效减少 Java 程序中,同步负载、内存堆分配压力的跨函数全局数据流分析算法
3、通过逃逸分析,Java Hotspot 编译器能够分析出一个新的对象的引用的使用范围,从而决定是否要将这个对象分配到堆上
4、基本行为:分析对象动态作用域
(1)当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
(2)当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸,例如:作为调用参数传递到其他地方中
5、没有发生逃逸的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除
6、参数设置
(1)在 JDK 6u23 版本之后,HotSpot 默认开启逃逸分析
(2)-XX:+DoEscapeAnalysis 选项:显式开启逃逸分析
(3)-XX:+PrintEscapeAnalysis 选项:查看逃逸分析的筛选结果
7、开发中能使用局部变量的,就不要使用在方法外定义
逃逸分析:编译器可以对代码优化
1、栈上分配
(1)将堆分配转化为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配
(2)JIT 编译器在编译期间,根据逃逸分析的结果,如果一个对象并没有逃逸出方法,就可能被优化成栈上分配,分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收,无须进行垃圾回收
(3)逃逸场景:成员变量赋值、方法返回值、实例引用传递参数
2、同步省略 / 锁消除
(1)如果一个对象被发现只有一个线程被访问,则对于这个对象的操作可以不考虑同步
(2)在动态编译同步块时,JIT 编译器可以借助逃逸分析,判断同步块所使用的锁对象,是否只能被一个线程访问,而没有被发布到其他线程
(3)如果没有,则 JIT 编译器在编译这个同步块时,就会取消对这部分代码的同步,能提高并发性和性能
3、分离对象 / 标量替换
(1)有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,则对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中
(2)标量(Scalar):一个无法再分解成更小数据的数据
(3)Java 中的原始数据类型就是标量
(4)聚合量(Aggregate):还可以分解的数据
(5)Java 中的对象就是聚合量,可以分解成其他聚合量和标量
(6)标量替换:在 JIT 阶段的逃逸分析,发现一个对象不会被外界访问,并且对象可以被进一步分解时,JVM 不会创建该对象,而是将该对象成员变量,分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间
(7)-XX:EliminateAllocations:默认开启标量替换,允许将对象打散分配到栈上
逃逸分析并不成熟
1、根本原因:无法保证逃逸分析提升的性能一定高于消耗
(1)虽然经过逃逸分析可以标量替换、栈上分配、锁消除
(2)但逃逸分析自身需要进行一系列复杂的分析的,是一个相对耗时的过程
(3)经过逃逸分析之后,发现所有对象都是逃逸,则逃逸分析会浪费性能
(4)虽然这项技术并不十分成熟,但是也是即时编译器优化技术中一个十分重要的手段
2、逃逸分析取决于 JVM 设计者的选择
(1)Oracle Hotspot JVM 不是真正的栈上分配,而是借用标量替换来实现栈上分配
(2)即所有对象实例都是分配在堆上
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战