• java虚拟机堆空间


    • 堆是一个进程唯一的,是内存管理的核心区域

    • jvm启动时 堆 就会被创建,大小也就确定了,是jvm中管理的最大的一块内存,堆的大小是可以调节的

    • 《java虚拟机规范》规定,堆可以处于物理上不连续的空间,但在逻辑上它应该是连续的,《java虚拟机规范》对堆的描述是:所有的对象实例以及数组都应该在运行时分配在堆上

    • 数组和对象对象可能永远不会存储与栈上(特殊情况:栈上分配),因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置

    • 方法结束后,堆中的对象不会马上被移除,在垃圾回收时才会被移除

    • 多个线程会共享一个堆,但是也可以给线程划分私有的缓冲区,例如volatile

    • 堆是GC()执行垃圾回收的重点,栈和pc寄存器是没有垃圾回收的

    堆的内存细分:

    现代垃圾收集器大部分都基于分代收集理论设计,堆空间细分为:

    java 7之前堆在逻辑上分为:新生区(新生代、年轻代)、养老区(老年区、老年代)、永久区(永久代)

    8 及以后堆内存逻辑分为:新生区、养老区、元空间

    设置堆空间的大小

    堆用于存储java对象实例,在启动时就已经设定好了,想要设置堆的内存大小可以通过"-Xmx"和"-Xms"指令来设置

    • -Xmx 表示堆的起始内存
    • -Xms 表示堆的最大内存

    -X 是jvm的运行参数

    ms 是memory start

    一旦堆区中内存大小超过指定的最大内存,会抛出oom (OutOfMemoryError)

    默认情况下,初始内存大小为:物理内存大小的/64 ,最大内存大小是:物理内存大小的/4

    实际中建议,最大堆内存和其实堆内存相等,避免堆的频繁扩容和垃圾回收,影响效率

    在这里插入图片描述

    年轻代和老年代

    存储在jvm中的对象可以分为两类:

    • 一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速
    • 另一类是生命周期很长,在某些情况下甚至和虚拟机的生命周期保持一致

    所以堆区进一步细分的话,可以分为年轻代和老年代

    年轻代可以划分为:

    • Eden空间(伊甸园)
    • Survivor0空间(from区),幸存者0区
    • Survivor1空间(to区),幸存者1区,这两个区的大小一摸一样大

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H25k8DNZ-1659364965417)(C:\Users\IsTrueLove\AppData\Roaming\Typora\typora-user-images\image-20220731154521405.png)]

    配置新生代和老年代在堆结构的占比:

    • 默认-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3

    可以修改对应的占比,但是一般不做修改,除非明确知道系统中的不同时长生命周期对象的占比

    • 在hotspot中,Eden空间和另外两个Survivor空间的默认比例是 8:1:1 ,但是实际上因为有内存自适应分配,所以不是这个比例,如果想严格按照这个比例来,需要显示设置 -XX:SurvivorRatio=8

    • 几乎所有的对象都在Eden区被new出来的

    • 绝大多数的java对象的销毁都是在新生代进行的,大多数对象的生命周期都是比较短的

    • -Xmn:设置新生代的空间大小(一般不用设置,因为有总的堆空间,和比例,其实也就已经设置了新生代的大小了)

    对象分配过程

    为新对象分配内存是一件很严谨和复杂的任务,jvm的设计者不仅需要考虑内存如何分配、在哪里分配,并且由于内存分配算法与内存回收算法密切相关,还需要考虑GC执行完后是否会在内存空间中产生内存碎片

    一般过程:

    • 新生的对象在Eden区,一旦达到Eden内存的上限,就需要进行GC,这里的垃圾回收也叫做YGC(年轻代的垃圾回收)
    • 没被使用的对象,就会被清理,还在被占用,没有被清理的对象,就会提升到幸存者区(每个对象都有一个年龄计数器:age,从eden提升到幸存者区就赋值为1),这样就清空了Eden区
    • 然后继续创建对象,当伊甸园区再次达到上限时,就会再次触发垃圾回收,还在被占用,没有被清理的对象,会继续被放到幸存者区,至于放在哪一个幸存者区就看哪个幸存者区空闲(to区),而之前放在幸存者区的对象也会判断是否需要垃圾回收,如果不需要,就age加1,并移到另一个幸存者区(从from到to区),然后就清空了其中一个幸存者区和Eden区,(所以两个幸存者区,from和to并不固定,空的那一个就是to区,to区一定是空的)
    • 然后上述过程不断地执行,直到age达到阈值(默认16,可以设置),就会从幸存者区移动到老年代,移动到老年代后,这个age就不再变化了,因为已经没有意义了
      • -XX:MaxTenuringThreshold=< N > 可以设置这个阈值
    • 只有伊甸园区满,才会执行YGC,幸存者区满了,并不会触发这个机制

    垃圾回收会频繁的在新生区收集,很少在养老去收集,几乎不再永久代/元空间收集

    在这里插入图片描述

    特殊情况:

    • 新建对象,当Eden触发YGC后,发现Eden依然无法放下这个对象(YGC后,Eden是空的,依然无法存储这个对象,这个对象肯定是个超大的对象),就会直接把这个对象放到老年代
    • 如果老年代依然放不下,就会触发FGC、najorGc,回收后如果依然放不下,就直接报oom(在不能动态的调整内存的情况下)
    • 进行YGC时,幸存者区也会进行垃圾回收,回收后依旧存在的对象会移动到to区,同时从Eden区中提升的对象也要放入to区,如果to区不能放下这些对象,就会直接把这些对象放到老年代

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7YURy7c-1659364965418)(C:\Users\IsTrueLove\AppData\Roaming\Typora\typora-user-images\image-20220731170527153.png)]

    Minor GC、Major GC、Full GC

    所谓的jvm调优其实就是希望垃圾回收的次数能少一点,垃圾回收很影响运行效率

    Minor GC (YGC,新生代的GC)

    Full GC (老年代GC)报oom之前,一定是经过Full GC 操作的,只有老年代空间不足,才会报omm,其他空间不足,对象都可以向上提升

    为什么堆要分代:

    • 不同的对象的生命周期不同,大部分对象(70%-99%)都是临时对象,不分带当然也是可以运行的,分代的理由就是优化GC的性能
    • 如果不分代,所有的对象都在一起,垃圾回收时需要扫描的对象就会更多,分区可以减少一部分对象垃圾回收的频率,调高垃圾回收的数据(新生代是需要频繁垃圾回收的,老年代就不需要这么高的频率)

    内存分配策略

    针对不同年龄段的对象分配原则:

    • 优先分配到Eden
    • 大对象直接分配到老年代(程序中应该避免创建过多的大对象)
    • 长期存活的对象分配到老年代(垃圾回收的次数达到设置的阈值)
    • 动态的对对象的年龄进行判断(如果幸存区的相同年龄的所有对象大小总和大于幸存者区空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,不需要等到阈值)
    • 空间分配担保:-XX:HandlePromotinalFailure ,幸存者区一般较小,所以需要老年代做空间担保
      • 在发生新生代垃圾回收之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,
      • 如果大于,这次新生代垃圾回收(Minor GC)就是安全的,
      • 如果小于,就需要看是否允许的担保失败
        • 如果允许,会继续检查老年代最大可以连续空间是否大于历次晋升到老年代的对象的平均大小,
        • 如果大于就尝试进行新生带垃圾回收,但这次的垃圾回收是由风险的,如果小于,就进行一次Full GC
      • 如果不允许担保失败,就直接进行Full GC
      • 在jdk1.7之后,HandlePromotinalFailure的参数就不会影响到虚拟机的空间分配策略了,虽然还是定以了它,但是并没有使用了,所以只要 老年代最大可用的连续空间大于新生代所有对象的总空间,或者老年代最大可以连续空间是否大于历次晋升到老年代的对象的平均大小,就会进行Minor GC,否则进行 Full GC

    TLAB

    Thread local Allocation Buffer

    • 堆是线程共享区域,任何线程都可以访问到堆中的共享数据
    • 由于对象实例的创建在jvm中非常频繁,因此在并发环境下从堆区中划分内存空间是很不安全的
    • 为了避免对各线程操作同一地址,需要使用枷锁机制,进而影响分配速度

    所以,从内存模型而不是垃圾回收的角度来看,jvm为每个线程分配了一个私有缓存区域,也就是TLAB,包含在eden空间内

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

    • TLAB的内存空间非常小,只占整个Eden的1%(可自己设置大小,并且可以决定是否启用),所以无法满足所有对象实例都在TLAB中分配内存,但jvm确实是将TLAB作为内存分配的首选
    • 一旦在TLAB中内存分配失败,jvm就会尝试使用加锁机制确保数据操作的原子性,从而在Eden中分配内存

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PKtUIUgT-1659364965419)(C:\Users\IsTrueLove\AppData\Roaming\Typora\typora-user-images\image-20220731220715803.png)]

    代码优化

    栈上分配,逃逸分析

    • 随着 JIT(及时编译技术)编译器的发展与逃逸分析技术的逐渐成熟,栈上分配、标量替换技术的发展,对象就不一定要在堆中了

    • 如果经过逃逸分析,对象并没有逃逸出方法,那么就有可能被优化为栈上分配,这样就无需在堆上分配内存,也不用进行垃圾回收(方法结束,栈帧弹出,对象也就消失了,不需要垃圾回收),很明显是可以提高效率的

    • 另外还有一些定制的虚拟机,也会在堆外空间分配对象,目的就是减少GC的次数,提高效率

    想要将堆上的对象分配到栈,就需要使用逃逸分析,可以有效减少java程序中同步负载(TLAB满了后,就会加锁,会影响效率)和内存堆分配的压力,通过逃逸分析,hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上

    逃逸分析的基本行为就是分析对象的作用域:

    • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸
    • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生了逃逸,例如作为调用参数传递到其他地方中
    • 所以如果只是一个局部变量,就尽量不要在方法外定义

    jdk 1.7 以后,hotSpot 就默认开启了逃逸分析,之前版本可以手动开启

    同步省略

    在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断,同步块锁使用的锁对象,是否只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步,也就是所谓的锁消除,加锁的代价是很高的,消除锁可以大大提高并发性和性能,取消同步的过程就叫做同步省略,也叫锁消除(字节码文件依然会有加锁操作,执行的时候会去掉)

    分离对象或标量替换

    有的对象可能不需要作为一个连续的内存结构测u那种也可以被访问到,那么对象的部分或全部,可以不存储在内存,而是存储在cpu寄存器中或者栈中

    标量是指无法在分解成更小的数据的数据,java的原始数据类型就是标量,相对的,还可以分解的数据叫做聚合量,可以分解为其他聚合量和标量,例如可以把一个复杂的对象分解为很多个基础类型的标量,把对象打散,放在栈上

    默认是开启标量替换的,参数为:-XX:+EliminateAllocations

    所以堆并不是分配对象的唯一空间

    • 另外,逃逸分析需要在Server模式下,当然默认情况下就是server模式,
    • 逃逸分析技术到jdk1.6才有实现,但就算直到如今也不是很成熟,根本原因在于无法保证逃逸分析所提高的性能消耗一定高于逃逸分析操作本身的消耗,因为逃逸分析自身就是一个很消耗性能的操作,最极端的情况下就是经过一系列的逃逸分析后没有发现一个不逃逸对象,就会很浪费性能
    • 而hotSpot其实并没有使用栈上分配,而是使用的是标量替换

    所以,在hotSpot中,对象确实是都分配在堆空间的

  • 相关阅读:
    milvus数据库-查询
    ubutun上编译出现undefined reference to symbol ‘dladdr@@GLIBC_2.2.5‘的错误
    【redis】7.5 分布式缓存方案与技术选型:Redis VS Memcache VS Ehcache
    CLIPBERT(2021 CVPR)
    安卓逆向-马蜂窝zzzghostsigh算法还原--魔改的SHA-1
    测试周期被压缩?教你9个方法去应对
    elasticsearch定期删除策略 - 日志分析系统ELK搭建
    【C语言】详细分析库函数qsort
    linux命令学习
    【Vue组件之间的三种通信】
  • 原文地址:https://blog.csdn.net/persistence_PSH/article/details/126111674