Day16 JVM对象内存分配

JVM对象创建与内存分配

  • 对象创建流程

    image.png

    1. 类加载检查。虚拟机遇到对象创建指令时,先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有,则先执行对应类的加载过程。

    2. 分配内存。类加载完成后,可以完全确定对象所需内存的大小,此时需要把一块大小确定的内存从堆中划分出来

      如何划分内存?

      1. 指针碰撞(Bump the Pointer,默认方式):如果java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间使用指针作为分界,分配内存时就只需要将指针往空闲空间的那边移动一段与新对象 大小相等的距离即可
      2. 空闲列表(Free List):如果java堆中的内存不是规整的,而是已使用的内存和空闲内存相互交错,那虚拟机就会维护一个列表,记录哪些内存块可用,分配时从列表里找到一块足够大的空间划分给对象实例,并更新列表上的记录 如何解决对象并发创建?
      3. CAS(compare and swap) 虚拟机采用CAS配上失败重试的方式保证更新操作的原子性,来对分配内存空间的动作进行同步处理
      4. 本地线程分配缓冲(Thread Local Allocation Buffer TLAB)把内存分配的动作按线程划分再不同的空间中进行,即每个线程在Java堆中预先分配一小块内存。通过-XX:+/-UserTLAB参数来设定是否开启,默认是开启的,-XX:TLABSize指定TLAB的大小
    3. 初始化零值。内存分配完成后,虚拟机会将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程可以提前至TLAB分配时进行。这步操作保证了对象的实例字段在java代码中可以不赋初始值就能直接使用,程序能访问到这些字段数据类型对应的零值。

      对象在内存中的存储包含三个部分: 对象头、实例数据、对齐填充(保证对象是8个字节的整数倍)

    4. 设置对象头。初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头Object Header之中。

      32位机器对象头

      64位机器对象头

    5. 执行方法,按照程序设定执行构造方法和赋值

  • 对象大小和指针压缩

    • 什么是指针压缩Compressed Ordinary Object Pointers? 一种用于减少对象引用内存占用的优化技术,-XX:+UseCompressedOops(默认开启),禁止指针压缩:-XX:-UseCompressedOops
      • 为什么需要指针压缩?

        减少堆内存占用、提高CPU缓存命中率、降低GC压力

        在64位JVM中,对象引用默认使用8字节表示,相比32位的4字节引用,会显著增加对内存的使用量,使用较大指针在主内存和缓存之间移动数据,会占用较大带宽,同时GC也会承受较大压力,为了减少64位的堆内存消耗,引入了指针压缩功能。

        真实地址>>3得到32位地址存储,拿到32位地址<<3得到真实地址。 32位地址最大支持4G(2^32)内存,通过优化后使用32位的地址就可以实际做到寻址空间为2^35=4G*8=32G 所以当堆内存小于等于4G时,不需要启用指针压缩,jvm会直接去除高32位地址,当堆内存大于32G时,压缩指针会失效,强制使用64位来对java对象寻址。

  • 对象内存分配

    image.png

    • 对象栈上分配

      什么是对象逃逸分析? 通过分析对象(-XX:+DoEscapeAnalysis)动态作用域,当一个对象在方法中被定义后,JIT会分析判断它会不会逃离当前方法或当前线程。 方法逃逸:对象被return出去,或者作为参数传递 线程逃逸:对象被赋值给静态变量、实例变量,或者被其他线程访问

      标量替换(-XX:+EliminateAllocations):如果不会逃逸,并且对象可以分解时,就不会创建对象,而是把对象的成员变量当成独立的局部变量来存 同步消除(-XX:+EliminateLocks): 对象只在单线程内使用,不会被其他线程看到,那加synchronized就是多余的,直接去掉

      经过逃逸分析后,确定该对象不会被外部访问,就会把该对象放在栈上分配内存,让对象随着栈帧出栈而销毁,减轻垃圾回收负担

    • 对象在Eden区分配 大多数情况下,对象都在新生代中的Eden区分配,当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC(Young GC),可通过添加运行JVM参数: -XX:+PrintGCDetails打印堆占用情况 Minor GC(Young GC):指发生新生代的垃圾回收操作,Minor GC非常频繁,回收速度也比较快 Major GC(Full GC):会回收老年代、新生代、方法区的垃圾,回收速度慢

    • 大对象直接进入老年代 大对象就是指需要大量连续内存空间的对象,JVM参数 -XX:PretenureSizeThreshold (unit:byte)可以设置大对象的大小,如果对象所需的内存空间超过这个值,为了避免大对象分配内存的时候的复制操作,就会跳过新生代内存,直接被分配到老年代,个参数只在 Serial 和ParNew两个收集器下有效。

      • 长期存活的对象进入老年代 虚拟机会给每个对象分配一个对象年龄计数器,如果对象在Eden区出生,在经过第一次Young GC仍然存活,并且能够被Survivor容纳的话,将会被转移到Survivor空间中,并将对象年龄设置为1,对象在Survivor区每熬过一次Young GC,年龄就增加1岁,当他的年龄增长到一定程度时,就会被放到老年代中,晋生到老年代的阈值可以通过**-XX:MaxTenuringThreshold** 设置

        如果Eden到Survivor区存不下怎么办? 如果Survivor容纳不下Eden区存活的对象,会将对象直接放入老年代

    • 对象动态年龄判断(Young GC后触发) 当前存放对象的Survivor区里,一批对象的总大小大于这块区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代

    • 老年代空间分配担保机制 老年代每次Yonug GC之前JVM都会计算老年代剩余可用空间,如果这个可用空间小于新生代里所有的对象大小之和(包括垃圾对象),就会看一个-XX:-HandlePromotionFailure的参数(默认有值),如果有值,会判断老年代的可用内存大小,是否大于之前每一次Young GC后进入老年代的对象的平均大小,若是小于,就会触发full gc,如果回收玩还是不够存储新的对象就会造成OOM。

    image.png

  • 对象内存回收

    对垃圾进行回收之前,先要判断哪些对象已经死亡

    • 引用计数法 给每个对象维护一个引用计数器,有引用就加一,引用失效就减一,计数器为0则回收。实现简单效率高,但是无法解决对象之间相互引用的问题

    • 可达性分析 从GC Roots出发,顺着引用链往下遍历,能遍历到的对象都是活的,其余的都是要回收的。能解决循环引用问题,但需要花更多的时间遍历做标记 image.png

    • 引用类型

      按引用强度分为四种,强度越弱,GC越容易回收

      • 强引用 普通的变量引用,例如User user = new User();
      • 软引用 通过SoftReference包裹的引用,没存够用的时候不会回收,内存吃紧快要OOM的时候才回收,适合做缓存
      • 弱引用 通过WeakReference包裹的引用,GC会直接回收
      • 虚引用 通过PhantomReference包裹的引用,包裹后拿不到对象实例,永远返回null,几乎不用 image.png
    • 如何判断一个类是无用的类

      • 该类的实例对象都已被回收,也就是堆中不存在该类的任何实例
      • 加载该类的ClassLoader已被回收
      • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问到改类的方法
0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP