• 深入理解Java虚拟机之【堆】


    深入理解Java虚拟机之【堆】

    一、基本概念

    堆是Java内存管理的核心区域,一个JVM实例只存在一个堆内存,在JVM启动时被创建并确认空间大小。

    堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

    所有的线程共享Java堆,但是可以划分线程私有的缓冲区(TLAB)

    堆是GC执行垃圾回收的重点区域,方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

    空间划分:
    在这里插入图片描述

    新生代与老年代空间默认比例1:2
    
    在HotSpot中,Eden空间和另外两个Survivor空间缺省所占的比例是:8:1:1
    
    几乎所有的Java对象都是在Eden区被new出来的。Eden放不了的大对象,直接进入老年代了。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    设置堆内存的大小:

    -Xms :表示堆空间的起始内存。
    -Xmx:表示堆空间的最大内存

    通常将-Xms和-Xmx两个参数配置相同的值,其目的是为了能够在java垃圾会后清理完堆区后,不需要重新分隔计算堆区的大小,从而提高性能。

    二、对象分配步骤

    1. new的对象先放伊甸园区。此区有大小限制。

    2. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中不再被其他对象所引用的对象进行销毁

    3. 然后将伊甸园中的剩余对象移动到幸存者0区。

    4. 如果再次触发垃圾回收,此时上次幸存下来的放到幸存者0区的,如果没有回收,就会放到幸存者1区。

    5. 如果再次经历垃圾回收,此时会重新放回幸存者0区,接着再去幸存者1区。

    6. 啥时候能去养老区呢?可以设置次数。默认是15次。可以设置参数: -XX:MaxTenuringThreshold=进行设置。

    7. 在养老区,相对悠闲。当养老区内存不足时,再次触发GC: Major GC,进行养老区的内存清理。

    8. 若养老区执行了Major GC之后发现依然无法进行对象的保存,就会产生OOM异常

    在这里插入图片描述

    三、Minor GC、Major GC与Full GC

    按照回收区域划分:
    在这里插入图片描述
    年轻代GC(Minor GC )触发条件

    当年轻代空间不足时,就会触发MinorGC,这里的年轻代指的是Eden代满,Survivor满不会触发GC。
    
    因为Java对象大多朝生夕灭,所以MinorGC非常频繁
    
    MinorGC会引发STW,在STW 状态下,出来GC线程,JAVA的所有线程都是停⽌执⾏的
    
    • 1
    • 2
    • 3
    • 4
    • 5

    老年代GC (Major GC/Full GC)触发条件

    出现了MajorGC,经常会伴随至少一次MinorGC(非绝对)
    
    MajorGC的速度比MinorGC慢10倍以上,STW的时间更长
    
    如果MajorGC后,内存还不足,就报OOM了
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Full GC触发机制

    1、调用System.gc()时,系统建议执行FullGC,但是不必然执行
    
    2、老年代空间不足
    
    3、方法区空间不足
    
    4、通过MinorGC后进入老年代的平均大小,大于老年代的可用内存
    
    5、由Eden区,Survivor 0区向Survivor 1区复制时,对象的大小大于ToSpace可用内存,则把改对象转存到老年代,且老年代的可用内存小于该对象的大小
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    四、内存分配策略

    1. 优先分配到Eden

    2. 如果对象在Eden出生并经过第一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。

    3. 对象在Survivor区中每熬过一次MinorGC,年龄就增加1 岁,当它的年龄增加到一程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代中。

    4. 大对象直接分配到老年代

    动态对象年龄分配

    如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,
    
    年龄大于或等于该年龄的对象可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄
    
    • 1
    • 2
    • 3

    空间分配担保

    在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间,是否大于新生代所有对象的总空间
    
    如果大于,则此次MinorGC是安全的
    
    如果小于,则查看-XX:HandlePromotionFailure设置是否允许担保失败
    
    	是:会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小
    			大于,则尝试进行一次MinorGC,但是这次MinorGC依然是有风险的
    			小于,则改为进行一次FullGC
    
    	否:则改为进行一次FullGC
    
    jdk6update24之后,这个参数不会再影响到虚拟机的空间分配担保策略
    	
    	规则改为只要老年代的连续空间大于新生代对象总大小,或者历次晋升的平均大小,就会进行MinorGC
    
    	否则进行FullGC
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    五、为对象分配内存TLAB

    解决问题:

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

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

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

    Thread Local Allocation Buffer
    在这里插入图片描述
    从内存模型而不是垃圾收集的角度,对Eden区域进行划分,JVM为每个线程分配了一个私有缓存区域,包含在Eden空间中

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

    尽管不是所有的对象实例都能够在TLAB中成功分配内存,但是JVM确实是将TLAB作为内存分配的首选
    
    默认情况下,TLAB空间内存非常小,仅占有整个Eden空间的1%
    
    一旦对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
    
    • 1
    • 2
    • 3
    • 4
    • 5

    六、堆是分配对象的唯一选择吗?

    随着JIT编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术,将会导致一些微秒变化,所有对象分配到堆上渐渐变得不那么绝对了。

    有一种特殊情况,如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配,这样无需堆上分配,也不需要垃圾回收了,也是最常见的堆外存储技术

    逃逸分析:

    1. 逃逸分析的基本行为就是分析对象动态作用域
    当一个对象在方法中定义后,对象只在方法内部使用,则认为没有发生逃逸
    
    当一个对象在方法中被定义后,它被外部方法引用,则认为发生逃逸
    
    • 1
    • 2
    • 3
    1. 栈上分配
    将堆分配转为栈分配,如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配
    
    • 1
    1. 同步策略
    如果一个对象被发现只能从一个线程被访问到,对于这个对象的操作可以不考虑同步
    
    JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象,是否只能够被一个线程访问,而没有被发布到其他线程。
    
    如果没有,那么JIT编译器在编译这个同步块的时候,就会取消对这部分代码的同步。
    
    这样就大大提高并发性和性能,这个取消同步的过程就叫同步省略,也叫锁消除
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1. 分离对象或标量替换
    有的对象可能不需要作为一个连续的内存结构存在,也可以被访问到,那么对象的部分(或全部)可以不存储在内存。而是存储在CPU寄存器中
    
    标量是指一个无法再分解的更小的数据的数据。Java中原始数据类型就是标量
    
    可以分解的数据叫聚合量,Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量
    
    • 1
    • 2
    • 3
    • 4
    • 5

    七、堆空间的参数设置

    -XX: +PrintFlagsInitial :查看所有的参数的默认初始值
    
    -XX: +PrintFlagsFinal :查看所有的参数的最终值(可能会存在修改,不再是初始值)
    
    -Xms:初始堆空间内存( 默认为物理内存的1/64)
    
    -Xmx:最大堆空间内存(默认为物理内存的1/4)
    
    -Xmn: 设置新生代的大小。(初始值及最大值)
    
    -XX:NewRatio: 配置新生代与老年代在堆结构的占比
    
    -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例
    
    -XX : MaxTenuringThreshold: 设置新生代垃圾的最大年龄
    
    -XX: +PrintGCDetails: 输出详细的GC处理日志
    
    -XX:+PrintGC / -verbose:gc 打印gc简要信息
    
    -XX: HandlePromotionFailure: 是否设置空间分配担保
    
    -XX:UseTLAB: 设置是否开启TLAB空间
    
    -XX:TLABWasteTargetPercent: 设置TLAB空间所占用Eden空间的百分比大小
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    OVER

  • 相关阅读:
    Vue开发实例(六)实现左侧菜单导航
    C++11新特性② | 左值、左值引用、右值与右值引用
    E138: Can‘t write viminfo file
    [MyBatisPlus]DQL编程控制②(查询投影、查询条件)
    基于分步表单的实践探索
    37. UE5 RPG创建自定义的Ability Task
    nestjs 结合LibreOffice 实现word转pdf在线预览
    mysql包select结果无法同步的问题
    2520. 统计能整除数字的位数 --力扣 --JAVA
    Java中各种数据格式-json/latex/obo/rdf/ turtle/owl/xml介绍对比示例加使用介绍
  • 原文地址:https://blog.csdn.net/NICK_53/article/details/126245834