深入理解Java对象内存模型&JVM内存模型

前言

首先,在前几篇的文章中,我们了解了运行时数据区的设计由来,并相继引出了(堆、方法区)线程共享、(虚拟机栈【线程栈】、本地方法栈、程序计数器)线程私有等相关概念,那堆、方法区、线程栈之间的相互指向是怎样的呢?

我们可以很清楚的知道线程栈 --> 堆【方法内部有一对象,地址指向堆】;方法区–> 堆【方法区存放的是静态变量、常量、即时编译的代码,那在类文件中创建了静态对象,方法区指向堆】;堆 -->方法区,在堆中存放的是具体数据,我们是如何知道由哪个类创建的,这就和Java对象内存模型有关系了,接下来一起深入理解Java对象内存模型吧!

Java对象内存模型

Java对象内部布局:
在这里插入图片描述上面我们遗留了一个问题,堆是如何指向方法区,在我们Java对象内存布局中,可以清晰知道通过类型指针指向对象对应的类元数据的内存地址,进而找到相关信息。
数据是在内存和CPU交互的,通过寄存器计算,而数据在Java对象中是大端存储还是小端存储呢?大端和小端存储的好处各是什么?
如:int a =1
在这里插入图片描述
小端存储优点:如果a是long类型,小端存储下,转化为int类型时,高位部分可以直接截掉;便于数据之间的类型转换!
大端存储优点:便于数据类型的符号判断,因为最低地址位数据即为符号位,可以直接判断数据的正负号,Java中使用大端存储!

/**
 * 判断当前环境字节是大端还是小端字节序存储
 * Little-Endian 高位字节在前,低位字节在后。
 * Big-Endian 低位字节在前,高位字节在后。
 * 在x86的计算机中,一般采用的是小端字节序
 */
public class EndianTest {

    public static void main(String[] args) {
        Unsafe unsafe = EndianTest.getUnsafe();
        long a = unsafe.allocateMemory(8);
        try {
            unsafe.putLong(a, 0x0102030405060708L);
            byte b = unsafe.getByte(a);
            ByteOrder byteOrder;
            switch (b) {
                case 0x01: byteOrder = ByteOrder.BIG_ENDIAN;     break;
                case 0x08: byteOrder = ByteOrder.LITTLE_ENDIAN;  break;
                default:
                    byteOrder = null;
            }
            System.out.println(byteOrder);
        } finally {
            unsafe.freeMemory(a);
        }

    }

    public static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

内存模型设计之klass Pointer

句柄池访问:
在这里插入图片描述使用句柄池访问对象,会在堆中开辟一块内存作为句柄池句柄中存储了对象实例数据的内存地址,访问类型数据的内存地址(类信息、方法类型信息),对象实例数据一般在heap中开辟,类型数据一般存储在方法区中!
优点:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要改变
缺点:增加了一次指针定位时间开销
直接对象访问:
在这里插入图片描述直接指针访问方式指reference中直接储存对象在heap中的内存地址,但对应的类型数据访问地址需要 在实例中存储。
优点 :节省了一次指针定位的开销。
缺点 :在对象被移动时(如进行GC后的内存重新排列),reference本身需要被修改
直接访问比句柄访问快一倍,而在Java内存布局模型中,Java对象采用的是直接对象访问的方式去做的

内存模型设计之指针压缩

案例代码:

public class JOLSample {
    public static void main(String[] args) {
         ClassLayout layout = ClassLayout.parseInstance(new Object());
         System.out.println(layout.toPrintable());
         System.out.println();
         ClassLayout layout1 = ClassLayout.parseInstance(new int[]{});
         System.out.println(layout1.toPrintable());
         System.out.println();
         A a = new A();
         System.out.println(a.hashCode());
         ClassLayout layout2 = ClassLayout.parseInstance(a);
         System.out.println(layout2.toPrintable());
         }
         // ‐XX:+UseCompressedOops 默认开启的压缩所有指针
         // ‐XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer
         // Oops : Ordinary Object Pointers
         public static class A {
         //8B mark word
         //4B Klass Pointer 如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8B
         int id; //4B
         String name;    //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
         byte b; //1B
         Object o; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
     }
    }

从上文的Java对象内存布局中,我们知道引用类型是占8个字节,但在jdk1.6后默认开启了指针压缩,指针压缩,压缩是对应的引用地址存储大小,那么思考一个问题,为什么要引入指针压缩这项技术呢?
①在64位平台的HotSpot中使用32位指针,内存使用会多出1.5倍左右,使用较大指针在主内存和缓存之间移动数据,占用较大宽带,同时GC也会承受较大压力
②在JVM中,32位地址最大支持4G内存(2的32次方),可以通过对对象指针的压缩编码、解码方式进行优化,使得JVM只用32位地址既可以支持更大的内存配置(小于等于32G)
③为了减少64位平台下内存的消耗,启用指针压缩
④堆内存小于4G时,不需要启用指针压缩,JVM会直接去除高32位地址,即使用低虚拟地址空间
⑤堆内存大于32G时,压缩指针会失效,会强制使用64位即8字节来对象寻址,这就会出现1的问题
32位系统的CPU 最大支持2^32 = 4G
如果是64位系统,最大支持 2^64, 但是对其填充是按照8字节进行填充,指针压缩可以理解为在32位系统在64位上面使用,因为32位系统的CPU寻址空间最大支持4G,对其填充*8 = 32G,这就是内存>32G指针压缩失效的原因

内存模型设计之对齐填充

对齐填充的意义是提高CPU方法数据的效率,主要针对会存在该实例对象数据跨内存地址区域存储的情况!64位机器下一次读取8字节
例如在没有对齐填充的情况下,内存地址存放情况如下:
在这里插入图片描述因为处理器只能0x00-0x07,0x08-0x0F这样读取数据,所以当我们想获取这个long类型的数据时,处理器必须要读两次内存,第一次(0x00-0x07),第二次(0x08-0x0F),很明显第二次读取的时候,long型数据并不是完整的。

例如在对齐填充的情况下,内存地址存放情况如下:
在这里插入图片描述现在处理器只需要直接一次读取(0x08-0x0F)的内存地址就可以获得我们想要的数据了。
到这为止,貌似对齐填充已经很完善了,但实则还是会有问题,如果说我们还想再存储一个布尔类型,是又重头开始8字节去存储,还是用别的什么方式呢!按照我们所想的,放进0x07那个空位就很完美了,不然会产生空间碎片,而在HotSpot源码中就有这一策略:

当我们的策略为0时,这个时候我们的排序是 基本类型 > 填充字段 > 引用类型
当我们策略为1时,引用类型 > 基本类型 > 填充字段
策略为2时,父类中的引用类型跟子类中的引用类型放在一起 父类采用策略0 子类采用策略1

这样操作可以降低空间的开销 ,

JVM内存模型

总图:
在这里插入图片描述在深入理解运行时数据区文章中,依次介绍了堆、线程栈、方法区、本地方法栈、程序计数器等相关概念作用。运行时数据区是一种规范,而JVM内存模式是对该规范的实现。其主要实现是针对线程共享而言的,那我们来看另一幅图:
在这里插入图片描述我们知道98%的对象都是朝生夕死的,而我们触发GC种类有部分partialGC和FullGC,部分GC分为YoungGC和OldGC,首先可以暂且抛开部看这个内存模型,我们可以思考,如果我们堆总共只有一个大的区,这样能行嘛,这样会导致一个问题,一直存一直存,满了直接触发FullGC,这样垃圾收集时间会较长,会STW,影响用户线程,会造成卡顿。这时我们想到了可以分区,可以分成Old区和Young区,但为什么又将Young区分成了Eden区、s0区、s1区

Young区的划分是为了减少GC的悲观策略【进入old区】,在Java对象内存布局中,在对象头中有分代年龄的标记,首先对象是先被分配到eden区的,如果经过一次youngGC后,存活下来了,分代年龄+1,分代年龄到15就会被移动到老年代

为什么要有两个survivor区,假设只有一个的,我们知道eden区满了后,存活对象会被放入s0区,但是在s0区中的对象不一定是连续放的,会存在空间碎片问题,如果有另一个survivor区的话,可以互相倒腾,避免产生空间碎片!注意不一定是一样大小!

悲观策略如何产生,首先我们知道大对象会直接进入老年代;在jdk1.8时,survivor区所有年龄对象总和大于50%直接进入老年代;

什么时候会触发Full GC, Full GC是针对 元空间 + Old区 + Young区:
①自己手动调用System.gc();
②Meta Space区域空间不足
③之前每次晋升的对象的平均大小 > 老年代剩余空间 //JVM每次有对象晋升时,都会做相应计算统计
④Young GC之后,存活对象超过了老年代所剩空间大小

总结

看到最后,想必大家都理解了本文的所有内容,接下来大家跟着我的总结,一起回顾吧!
从最开始的线程栈、堆、方法区的指向,我们产生了这么一个问题,堆 --> 方法区,从而引出了Java对象内存布局的概念,而它则是有对象头、类型指针、实例数据,对齐填充等组成,在图片上解释了他们的各个区域是干什么的,作用是什么。数据和CPU交互,是如何存储的,从而引出了大端存储、小端存储及其特点,知道了Java对象是大端存储。
根据对象内存模型之Class Pointer引出了句柄池访问、直接对象访问的特点及优缺点,做了相应的图解根据Java对象内存模型证明了Java对象中是用直接对象访问方式进行的,效率比句柄池高一倍。
根据对象内存模型之指针压缩,我们从5点解释了为什么要使用指针压缩,并在最后解释了为什么超过32G就失效了。
根据对象内存模型之对齐填充,首先从未对齐填充图介绍了它们的是如何访问的,在64位机器下,是按8字节读取的,没有对齐填充会产生什么问题,满足了对齐填充又会产生什么问题,会产生浪费问题,有三种策略去解决了空间浪费问题!
JVM内存模型,首先给出了一张整体图,同时从堆和非堆方面去设计一幅图,紧接着抛开这幅设计不看,一步步自己去推导不这样会产生什么问题,提出相应问题并解决!

为什么要学JVM1、一切JAVA代码都运行在JVM之上,只有深入理解虚拟机才能写出更强大的代码,解决更深层次的问题。2、JVM是迈向高级工程师、架构师的必备技能,也是高薪、高职位的不二选择。3、同时,JVM又是各大软件公司笔试、面试的重中之重,据统计,头部的30家互利网公司,均将JVM作为笔试面试的内容之一。4、JVM内容庞大、并且复杂难学,通过视频学习是最快速的学习手段。课程介绍本课程包含11个大章节,总计102课时,无论是笔试、面试,还是日常工作,可以让您游刃有余。第1章 基础入门,从JVM是什么开始讲起,理解JDK、JRE、JVM的关系,java的编译流程和执行流程,让您轻松入门。第2章 字节码文件,深入剖析字节码文件的全部组成结构,以及javap和jbe可视化反解析工具的使用。第3章 类的加载、解释、编译,本章节带你深入理解类加载器的分类、范围、双亲委托策略,自己手写类加载器,理解字节码解释器、即时编译器、混合模式、热点代码检测、分层编译等核心知识。第4章 内存模型,本章节涵盖JVM内存模型的全部内容,程序计数器、虚拟机栈、本地方法栈、方法区、永久代、元空间等全部内容。第5章 对象模型,本章节带你深入理解对象的创建过程、内存分配的方法、让你不再稀里糊涂。第6章 GC基础,本章节是垃圾回收的入门章节,带你了解GC回收的标准是什么,什么是可达性分析、安全点、安全区,四种引用类型的使用和区别等等。第7章 GC算法与收集器,本章节是垃圾回收的重点,掌握各种垃圾回收算法,分代收集策略,7种垃圾回收器的原理和使用,垃圾回收器的组合及分代收集等。第8章 GC日志详解,各种垃圾回收器的日志都是不同的,怎么样读懂各种垃圾回收日志就是本章节的内容。第9章 性能监控与故障排除,本章节实战学习jcmd、jmx、jconsul、jvisualvm、JMC、jps、jstatd、jmap、jstack、jinfo、jprofile、jhat总计12种性能监控和故障排查工具的使用。第10章 阿里巴巴Arthas在线诊断工具,这是一个特别小惊喜,教您怎样使用当前最火热的arthas调优工具,在线诊断各种JVM问题。第11章 故障排除,本章会使用实际案例讲解单点故障、高并发和垃圾回收导致的CPU过高的问题,怎样排查和解决它们。课程资料课程附带配套项目源码2个159页高清PDF理论篇课件1份89页高清PDF实战篇课件1份Unsafe源码PDF课件1份class_stats字段说明PDF文件1份jcmd Thread.print解析说明文件1份JProfiler内存工具说明文件1份字节码可视化解析工具1份GC日志可视化工具1份命令行工具cmder 1份学习方法理论篇部分推荐每天学习2课时,可以在公交地铁上用手机进行学习。实战篇部分推荐对照视频,使用配套源码,一边练习一遍学习。课程内容较多,不要一次性学太多,而是要循序渐进,坚持学习。      
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值