• JVM学习三


    初始化

    前面已经过完了类加载的过程,接下来是类的初始化过程,主要分为三个步骤,分别是
    1、vertification
    验证文件是否符合JVM规范
    2、preparation
    静态成员变量默认值
    3、resolution
    将类、方法、属性等符号引用解析为直接引用
    常量池中的各种符号引用解析为指针,偏移量等内存地址的直接引用

    在这里插入图片描述
    4、initializing
    静态变量初始化

    public class T001_ClassLoadingProcedure {
        public static void main(String[] args) {
            System.out.println(T.count);
        }
    }
    
    class T {
    	/*
         public static int count = 2; 
          public static T t = new T(); 
          此方式count = 3,另外一种输出count=2
    	*/
    	
    	//初始化过程首先调用new T() count++,count = 1,然后第二步赋值为2
        public static T t = new T(); // null
        public static int count = 2; //0
    
        //private int m = 8;
    
        private T() {
            count ++;
            //System.out.println("--" + count);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    new 一个对象T,里面有成员变量m,第一步先给这个对象申请内存空间,然后先给这里面的成员变量
    先赋一个默认值值0,然后调用构造方法给成员变量赋值一个初始值8
    第一步new是申请内存赋默认值
    invokespecial调用构造方法赋初始值
    astore_1赋值引用
    其上指令可能会重排序,所以单例的DCL需要加上volatile修饰
    Object o = new Object() 分三步
    1.new 分配空间
    2.invokespecial 调用构造方法
    3.astore_1将对象赋给o
    乱序指的就是比如像2,3这两条无关的命令之间可能存在乱序, 假如线程先执行了3, 再执行2,就会导致对象初始化到一半时线程就挂起了,当另一个线程来的时候, o不为空,直接返回一个有问题的对象

    在这里插入图片描述

    JMM(Java内存模型)

    Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

    硬件层数据一致性

    在这里插入图片描述
    CPU读取数据的时候先去离CPU最近的L1里面去找,如果没有逐步向下去找,然后向上缓存,
    在这里插入图片描述
    底层多个cpu之间的缓存一致性则通过缓存一致性协议去保证。
    参考博客:https://www.cnblogs.com/z00377750/p/9180644.html
    在这里插入图片描述
    现在的CPU的底层的一致性是通过缓存一致性协加上总线锁来实现的。

    缓存行概念

    CPU读取数据是按缓存行去读取的数据,CPU的多数缓存行为64个字节,
    假如有两个数x,y,一个CPU是只要x而另一个CPU只要y,但是他们同时读入了
    x,y这两个数。就会导致其中一个CPU将x的值改了,就要通过缓存一致性协议去通知
    另一个CPU重新读取x的值【重新读取了缓存行数据】。
    在这里插入图片描述

    乱序问题

    CPU为了提高效率会打乱原有的执行顺序,会在一条指令执行的过程中(比如从
    内存读数据)去同时执行另一条指令,前提是,两条指令没有依赖关系。
    参考链接: https://www.cnblogs.com/liushaodong/p/4777308.html
    写指令也可能是乱序的
    WCBuffer(write combing 合并写):CPU写操作进行合并,对一个数做多次更改合并最终的结果到下级的缓存
    里面去。
    在这里插入图片描述
    在这里插入图片描述

    //写时合并代码
    public final class WriteCombining {
    
        private static final int ITERATIONS = Integer.MAX_VALUE;
        private static final int ITEMS = 1 << 24;
        private static final int MASK = ITEMS - 1;
    
        private static final byte[] arrayA = new byte[ITEMS];
        private static final byte[] arrayB = new byte[ITEMS];
        private static final byte[] arrayC = new byte[ITEMS];
        private static final byte[] arrayD = new byte[ITEMS];
        private static final byte[] arrayE = new byte[ITEMS];
        private static final byte[] arrayF = new byte[ITEMS];
    
        public static void main(final String[] args) {
    
            for (int i = 1; i <= 3; i++) {
                System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne());
                System.out.println(i + " SplitLoop  duration (ns) = " + runCaseTwo());
            }
        }
    
        public static long runCaseOne() {
            long start = System.nanoTime();
            int i = ITERATIONS;
            //第一个是一起改
            while (--i != 0) {
                int slot = i & MASK;
                byte b = (byte) i;
                arrayA[slot] = b;
                arrayB[slot] = b;
                arrayC[slot] = b;
                arrayD[slot] = b;
                arrayE[slot] = b;
                arrayF[slot] = b;
            }
            return System.nanoTime() - start;
        }
    
        //第二个是分成两次做同样的操作
        public static long runCaseTwo() {
            long start = System.nanoTime();
            int i = ITERATIONS;
            while (--i != 0) {
                int slot = i & MASK;
                byte b = (byte) i;
                arrayA[slot] = b;
                arrayB[slot] = b;
                arrayC[slot] = b;
            }
            i = ITERATIONS;
            while (--i != 0) {
                int slot = i & MASK;
                byte b = (byte) i;
                arrayD[slot] = b;
                arrayE[slot] = b;
                arrayF[slot] = b;
            }
            return System.nanoTime() - start;
        }
    }
    
    
    
    • 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
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    乱序证明
    public class T01_Disorder {
        private static int x = 0, y = 0;
        private static int a = 0, b = 0;
    
        public static void main(String[] args) throws InterruptedException {
    
            for (long i = 0; i < Long.MAX_VALUE; i++) {
                x = 0;
                y = 0;
                a = 0;
                b = 0;
                CountDownLatch latch = new CountDownLatch(2);
    
                Thread one = new Thread(new Runnable() {
                    public void run() {
                        a = 1;
                        x = b;
    
                        latch.countDown();
                    }
    
                });
    
                Thread other = new Thread(new Runnable() {
                    public void run() {
                        b = 1;
                        y = a;
    
                        latch.countDown();
                    }
                });
                one.start();
                other.start();
                latch.await();
                String result = "第" + i + "次 (" + x + "," + y + ")";
                if (x == 0 && y == 0) {
                    System.err.println(result);
                    break;
                }
            }
        }
    
    }
    
    • 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
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    硬件级别保证有序【内存屏障】

    为了保证特定情况下的有序性:volatile

    CPU级别内存屏障

    加CPU级别的内存屏障:
    s:save
    l:load
    m:mix
    在这里插入图片描述
    lock指令后面有一条指令,当我这个指令执行完成之前,你lock对某个内存是锁定的

    在这里插入图片描述

    JVM内存屏障

    属于JSR规范里面的,由JVM自身实现的
    在这里插入图片描述

    volatile实现细节

    字节码层面

    加了一个ACC_VOLATILE标记
    在这里插入图片描述

    JVM层面

    在JVM读取到这个ACC_VOLATILE这个access_flag层面的时候,
    对所有的写操作的前面加了一个StoreStoreBarrier,在后面加了一个StoreLoadBarrier
    对所有的读操作的前面加了一个LoadLoadBarrier,在后面加了一个LoadStoreBarrier
    在这里插入图片描述

    OS和硬件层面

    使用hsdis -HotSpot Dis Assembler(观察虚拟机编译好的字节码在CPU级别是用什么指令来完成的)
    发现是lock指令xxx执行xxx指令的时候保证对内存区域加锁。
    windows上采用lock指令实现的
    参考博客:https://blog.csdn.net/qq_26222859/article/details/52235930

    synchronized实现细节

    字节码层面

    以如下代码为例:

    public class TestSync {
        synchronized void m() {
    
        }
    
        void n() {
            synchronized (this) {
    
            }
        }
    
        public static void main(String[] args) {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    先来看m方法,就是加了一个acc_flag的标记ACC_SYNCHRONIZED:
    在这里插入图片描述
    再来看n方法:
    会看到这样一个指令
    monitorenter:监视器进入
    monitorexit:监视器退出
    两个exit是为了当发现异常的时候自动退出
    在这里插入图片描述

    JVM层面

    C/C++调用了操作系统提供的同步机制

    OS和硬件层面

    通过lock指令实现:
    x86:lock comxchg xxx
    参考文章:https://blog.csdn.net/21aspnet/article/details/88571740

    Happens_before原则

    java语言的要求
    在这里插入图片描述

    as if serial

    不管如何重排序,单线程执行的结果不变

    对象的内存布局

    1、解释一下对象的创建过程?
    分为如下几步:
    (1)class loading
    (2)class linking(verification,preparation,resolution)
    (3)class initializing【静态语句块初始化】
    (4)申请对象内存
    (5)成员变量赋默认值
    (6)调用构造方法 【成员变量顺序赋初始值,执行构造方法语句】

    2、对象在内存中的存储布局?
    普通对象:
    (1)对象头 markword 占用8个字节
    (2)ClassPointer指针:-xx:+UseCompressedClassPointers开启为四字节,不开启为8字节【
    class的指针,指针指向class的对象(t.class)】
    (3)实例数据(一般指成员变量):
    1、引用类型:-XX:UseCompressedOops开启为4字节,不开启为8字节
    Oops Ordinary Object Pointers
    (4)Padding对齐,8的倍数【读取是按块来读的,这样增加效率】
    数组对象:
    (1)对象头 markword 占用8个字节
    (2)ClassPointer指针:-xx:+UseCompressedClassPointers为四字节,不开启为8字节【
    class的指针,指针指向class的对象(t.class)】
    (3)数组长度:4字节
    (4)数组数据
    (5)对齐的8的倍数

    3、Object的大小 new Object
    16个字节
    markword:8个字节
    查看JVM的配置参数发现是用了类压缩指针的
    所以classPointer是4个字节
    再加上padding是4个字节
    总的为16个字节
    在这里插入图片描述
    4、查看数组的大小: new int[]
    首先markword 8个字节
    然后是classpointer,压缩了4个字节
    然后数组长度,4个字节
    总共16个字节
    实验发现当不压缩classpointer发现占用24个字节
    markword 8个字节
    classpointer 8个字节
    数组长度 4个字节
    再加上padding 4个
    总共24个字节

    5、下面看一个对象:
    总共占用32个字节

       private static class P {
                            //8 _markword
                            //4 _class pointer
            int id;         //4
            //是个引用类型,正常占8,但是+UseCompressedOops【普通对象指针】开启了变成4个字节
            String name;    //4
            int age;        //4
    
            byte b1;        //1
            byte b2;        //1
    
            Object o;       //4
            byte b3;        //1
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    6、对象头具体包括什么?
    来源于如下文件:
    在这里插入图片描述

    三位代表锁的状态
    只有调用hashcode的时候才会被记录在对象头里面去

    这里是32位的图:
    在这里插入图片描述
    轻量级锁:是在JVM栈上实现的一个锁,没有涉及到内核
    重量级锁:调用内核的锁,消耗资源比较高
    问题:为什么GC年龄默认为15?对象头最大4位年龄为15
    参考链接:
    https://cloud.tencent.com/developer/article/1480590
    https://cloud.tencent.com/developer/article/1484167
    https://cloud.tencent.com/developer/article/1485795
    https://cloud.tencent.com/developer/article/1482500

    7、对象怎么定位
    如T t = new T();
    1、句柄池【简介】(查找效率低,但是GC效率高)
    其中的小t是怎么找到 new T()的呢,第一种方式就是通过句柄池,通过一个间接指针,
    这个间接指针分为两个指针,第一个是指向它的对象,第二个是指向t.class【将classpointer单独拿出来了】
    2、直接指针(hotspot用)查找效率高
    就是通过指针直接指向那个对象,然后这个对象又指向t.class

    8、对象怎么分配
    首先尝试往栈里面放,栈能放下则放在则栈弹出对象消失,如果栈上放不下且对象大,
    则直接分配到堆内存,如果不大会先在线程本地分配,线程本地分配不下则找eden区

    在这里插入图片描述

  • 相关阅读:
    实时数仓:实时数据平台之技术篇
    【8. 4位数码管TM1637】转载记录
    软件测试永远的家——银行测试,YYDS
    LangChain结合LLM做私有化文档搜索
    需求管理手册-对需求管理者的要求(10)
    Nextcloud缩略图尺寸的文档
    GC-垃圾回收
    StrictMode卡顿与泄漏检测-StrictMode原理
    SQL语句该如何写?
    《增长黑客》思维导图
  • 原文地址:https://blog.csdn.net/lsdstone/article/details/126713581