3. JMM:Java的内存模型

1 硬件层面保证数据一致性
  1. 存储器的层次结构:l0、l1等都是存储器,他们有些在cpu内部,有些在cpu外部,cpu使用其内存存储器中的数据,传输速度快,使用外部的传输速度慢
    在这里插入图片描述
  2. cpu执行读取读指令时,会先读取将该指令需要的数据,数据所在的位置不同,传输给cpu所需要的的时间也不同,它会最先向CPU中的一级缓存(L1 Cache)去寻找相关的数据,一级缓存是与CPU同频运行的,但是由于容量较小,所以不可能每次都命中,也就是没找到数据。这时CPU会继续向下一级的二级缓存(L2 Cache)寻找,同样的道理,当所需要的数据在二级缓存中也没有的话,会继续转向L3 Cache、内存(主存)和硬盘
    在这里插入图片描述
  3. cpu乱序执行:如果需要的数据恰好不在cpu的L0、L1、L2甚至L3中,那么cpu必须要通过内存总线到主存中读取,这个速度要远低于cpu处理指令的速度,此时cpu不会等待数据传输回来,而是会继续执行其他的与之前未处理完的指令无关的那些指令,这就是导致cpu乱序执行的根源
  4. 合并写(不重要)
    1. 简单解释:由于ALU速度太快,为了提高写的效率,CPU在写入L1同时,写入一个4字节的WC Buffers,满了之后直接更新到L2
    2. 详细解释:cpu执存储指令时,它会首先试图将数据写到离cpu最近的L1,如果此时cpu出现L1未命中,则会访问下一级缓存L2甚至L3,但L2的速度就已经比cpu慢20倍,又会造成cpu等待缓存的情况,因此现代cpu采取合并写技术,当L1未命中时,cpu会先写入一个64bit的缓冲区,注意不是缓存区,这个缓冲区允许cpu在从中读取,或写入数据的同时,执行其他操作,缓解了cpu写数据时cache miss时的性能影响。当后续的写操作需要修改相同的缓存行时,这些缓冲区在将后续的写操作提交到L2缓存之前,可以进行缓冲区写合并,缓冲区的数据会在某个延时的时刻更新到外部的缓存(L2_cache),如果我们可以在缓冲区被传输到L2之前能够填补这些缓冲区(buffers ),那么我们将大大提高传输总线的效率。对于intel的cpu,可以用于合并写的只有4个字节
  5. MESI缓存一致性协议
    1. 当从主存读取数据到cpu时,由于一个电脑有多个cpu,所以主存会为每个cpu都传一份数据,那么如果两个cpu都对这个数据进行了修改,会产生数据不一致
    2. 对于老cpu,cpu会将数据总线锁住,保证同时只有一个cpu可以访问这个数据,这种方式叫做总线锁,效率低
    3. 对于新cpu,每种cpu解决问题采用的协议不同,Intel采用MESI缓存一致性协议,是缓存锁的实现之一
    4. 所谓缓存一致性协议,就是给每个缓存行,做一个标记,表示这个缓存行状态,通过这些状态,让各cpu内缓存,保持一致
      1. M:Modified,被修改过
      2. E:Exclusive,独享的,即该缓存行只传给了一个cpu
      3. S:Shared,共享的,该缓存行传给了多个cpu
      4. I:Invalid,无效的,一个cpu中缓存行被改为M后,其他cpu内缓存行标记为I
    5. 有些无法被缓存的数据,或跨越多个缓存行的数据,依然必须使用总线锁
    6. 现代cpu的数据一致性实现 = 缓存锁(MESI)+总线锁
  6. 缓存行:可以简单的理解为CPU缓存中的最小缓存单位,大小为64字节,内存和高速缓存之间或高速缓存之间的数据移动不是以单个字节完成的,而是以缓存行为最小数据单元。例如当读取一个int型数据时,不会只将这4个字节读入到cpu,而是将其后面整个64字节,称为一个缓存行,一起读入
  7. 伪共享:位于同一缓存行的两个不同数据,被两个不同cpu锁定,产生相互影响的问题,称为伪共享,例如cpu1上有线程1,该线程1只需要使用变量x,而cpu2上的线程2,只需要变量y,但由于xy在同一缓存行,当cpu1读入x时,会把y也读入,cpu2读y时,x也被读入,那么当cpu1中x修改,虽然cpu2根本不用x,但由于缓存一致性协议,发现整个缓存行变为了Invalid,因此也需要从内存中重新读取。伪共享问题,可以通过缓存行对齐来解决,从而提升效率。
package com.mashibing.jvm.c2_classloader;
public class T02_CacheLinePadding {
    private static class T {
    	//Disruptor源码中为了提升效率,解决为共享带来的问题,就是这样做的
    	//缓存行对齐会浪费一点空间,但会提升很高的效率,充分利用cpu
        //因为一个缓存行为64字节,所以p1..p7+x正好64字节,这样可以保证,同时创建多个T对象时,他们的x值一定不在同一个缓存行,这就叫做缓存行对齐
        public volatile long p1, p2, p3, p4, p5, p6, p7;
        public volatile long x = 0L;
        public volatile long p8, p9, p10, p11, p12, p13, p14;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

  1. cpu乱序执行的证明:证明方法有问题
public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    static Object obj1 = new Object();
    static Object obj2 = new Object();

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //老师说:如果没有指令重排序,x和y永远不可能同时为0,但实际上发生了
                    //但是我觉得这不一定是指令重排序的问题,很有可能是线程间不可见问题,当线程one进来,几个值都是0,然后x=b,x还是0
                    //第二个进程进来,很可能没有马上收到a的变化
                    a = 1;
                    //终于想到一个方案屏蔽线程间不可见的因素,进入synchronized方法,一定会将最新的b值读取到,此时有两种情况
                    //1. 下面b=1已经执行完,此时x=1
                    //2. 下面b=1未执行,此时x=0
                    synchronized(obj1){
                        x = b;
                    }
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    //与上面同理
                    //1. 上面a=1已经执行完,此时y=1
                    //2. 上面a=1未执行,此时y=0
                    //那么x=0,y=0表示,下面b=1未执行,且上面a=1也未执行,这种情况线程不会结束,所以线程结束,就不可能x、y同时为0
                    //经过试验,多久都不能打印结果,看来指令重排序并不是很容易出现,至少这种量级并发不会产生
                    synchronized (obj2){
                        y = a;
                    }
                }
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                //System.out.println(result);
            }
        }
    }

    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}
  1. cpu层面保证有序性:为两条不能重排序的指令中间加内存屏障来实现有序性,而内存屏障在intel生产的cpu中,是通过原语(sfence、lfence、mfence)或总线锁(lock)实现
  2. sfence:fence为屏障,s为save,表示存屏障,表示屏障两侧的写指令不允许重排
  3. lfence:l为load,读屏障,lfence两侧的读指令,不允许重排
  4. mfence:屏障两侧的读写操作,都不允许重排
  5. lock[指令1]的语义
    1. 会加一个总线锁,其他CPU对内存的读写都会被阻塞
    2. 指令1执行完毕后,将缓存中的数据写回内存
    3. 禁止该指令与之前和之后的读和写指令重排序
    4. 对volatile修饰的变量进行写操作时,其实就是lock[写操作]
  6. JVM层面保证有序性:JVM制定了一些规范来保证有序性,但LoadLoad等屏障的底层实际上是通过cpu的lock指令实现的。Load为读,Store为写
    1. LoadLoad:Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕
    2. StoreStore:Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见
    3. LoadStore:Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕
    4. StoreLoad:Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见
  7. volatile的实现方式
    1. 字节码层面(Jclasslib查看):ACC_VOLATILE
    2. JVM层面:由于volatile写完才能volatile读,因此volatile读之前没必要再加StoreLoad,同理volatile读之后才能volatile写,因此volatile写之前不用再加LoadStore
      1. StoreStore–对volatile变量执行写操作–StoreLoad
        1. 如果对一个volatile变量进行写操作,那么该写操作,不能与其前面的任何写操作互换,也不能与后面任何读操作互换
        2. volatile变量的写操作,可以与前面的读操作换位置,因为之前无论怎么读,都不会改变我写入volatile的结果,但如果与其前面的写操作更换位置,就有可能影响到写入的值,例如int x=3;int volatilevar = x+5;一旦x=3这个写操作和volatile修饰的volatilevar变量的写操作互换位置,就会导致,volatilevar得到的不是最新的值
      2. LoadLoad–对volatile变量执行读操作–LoadStore
    3. OS(操作系统)和硬件层面:使用hsdis工具(hotspot的反汇编),就是观察虚拟机编译好的字节码在cpu级别用什么汇编指令完成的
      1. lock汇编指令实现,具体指令为为lock addl,addl表示向esp这个寄存器上,加一个0,而加0对寄存器没有任何作用,所以该lock指令主要作用就是lock
      2. 这是因为只有intel的cpu有sfence、lfence、mfence这三个原语,而java虚拟机可能跑在不同cpu上,因此只好使用了一个对大部分cpu都通用的lock指令来实现
  8. synchronized实现细节
    1. 字节码层面:
      1. 同步方法:ACC_SYNCHRONIZED
      2. 同步代码块:monitorenter(进入同步块),monitorexit(退出同步块)
    2. JVM层面:
      1. C、C++调用了操作系统级别的同步机制
    3. OS和硬件层面
      1. X86:lock指令
  9. happens-before原则:JVM规定的重排序必须遵守的规则
  10. as if serial:不管如何重排序,单线程执行结果一定不会变,只有在多线程中,重排序才会造成结果混乱
2 对象的内存布局
2.1 对象的创建过程
  1. 加载.class文件
    1. loading
    2. linking
    3. initializing
  2. 创建对象
    1. 为对象分配内存
    2. 成员变量默认值
    3. 调用构造器
      1. 成员变量顺序赋初始值
      2. 执行构造器中语句
2.2 对象在内存中的存储布局
  1. 与虚拟机的实现、虚拟机的设置有关
  2. 观察虚拟机配置
C:\Users\含低调>java -XX:+PrintCommandLineFlags -version
//注意这里的+号,表示开启,如果关闭,可以配置为-
-XX:InitialHeapSize=261755840 -XX:MaxHeapSize=4188093440 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
  1. 普通对象
    1. 对象头:Hotspot中叫markword,8位,用于标记对象状态
    2. ClassPointer指针:指向该对象属于的类的Class对象,-XX:+UseCompressedClassPointers开启为4字节,不开启为8字节
    3. 实例数据
      1. 基本类型:直接存值
      2. 引用类型:存放其引用的对象的地址,-XX:+UseCompressedOops开启为4位,不开启为8位
        1. Oops:ordinary object pointers
    4. Padding对齐:将整个对象的大小,对齐为8的倍数,因为对64位操作系统,是按块读而不是按接读,直接读取8的倍数个字节,速度反而更快
  2. 数组对象
    1. 对象头:markword,8位
    2. ClassPointer指针
    3. 数组长度:4字节
    4. 数组数据
    5. 对齐成8的倍数
2.3 获取对象的大小
  1. javaagent:主要用于在加载.class文件之前做拦截,对字节码进行修改
  2. 创建包含指定方法签名的premain方法的类
import java.lang.instrument.Instrumentation;

public class ObjectSizeAgent {
    private static Instrumentation inst;
    //类必须有premain方法,运行在main函数之前
    public static void premain(String agentArgs, Instrumentation _inst) {
        inst = _inst;
    }

    public static long sizeOf(Object o) {
        return inst.getObjectSize(o);
    }
}
  1. src下建立文件夹META-INF,其内建立文件MANIFEST.MF,注意Premain-Class必须独自放一行,注意写完intellij不能报错
Premain-Class: ObjectSizeAgent
  1. 将整个项目打成一个jar包
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 新建项目,导入该jar包,并创建测试类
public class T03_SizeOfAnObject {
    public static void main(String[] args) {
        System.out.println(ObjectSizeAgent.sizeOf(new Object()));
        System.out.println(ObjectSizeAgent.sizeOf(new int[] {}));
        System.out.println(ObjectSizeAgent.sizeOf(new P()));
    }
    private static class P {
        //8 _markword
        //4 _oop指针
        int id;         //4
        String name;    //4
        int age;        //4
        byte b1;        //1
        byte b2;        //1
        Object o;       //4
        byte b3;        //1
    }
}
  1. 配值jvm启动参数,增加代理
-javaagent:E:\IdeaProjects\untitled1\out\artifacts\untitled1_jar\untitled1.jar
  1. 执行
2.4 Hotspot开启内存压缩的规则(64位机 )
  1. 4G以下:无需压缩
  2. 4G - 32G:默认开启内存压缩 ClassPointers Oops
  3. 32G以上:压缩无效,使用64位
2.5 对象头具体包括的内容

在这里插入图片描述

  1. 对象状态不同,虚拟机位数不同,其内容格式不同,图中为64位Hotspot对象头中的内容
  2. unsed:未利用
  3. hashcode:对象哈希值,只有调用对象的hashCode方法,才放入
    1. 如果重写了hashCode,存放重写的方法的返回值的
    2. 如果没重写,存放根据对象的不同状态,算出的一个hashCode值,也称为identityHashCode,可以用System.identityHashCode(Object x)得到该值
    3. 当一个对象计算过identityHashCode,就无法进入偏向锁状态是对的,因为偏向锁前几位要记录线程id,此时没法记录了
  4. age:分代年龄,也就是被回收了多少次,4字节最大值为15,所以年轻代到老年代的gc年龄为15
  5. thread:线程ID
  6. ptr_to_lock_record:指向栈中锁记录的指针
  7. ptr_to_heavyweight_monitor:指向重量级锁的指针
2.6 对象如何定位
  1. 句柄池:相当于t先指向一个内存,这个内存分别存放两个地址,一个地址指向对象,另一个地址指向这个对象属于的Class对象,访问效率低,垃圾回收时效率高
  2. 直接指针:直接指向对象,Hotspot用的就是这种
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
图像识别技术在病虫害检测中的应用是一个快速发展的领域,它结合了计算机视觉和机器学习算法来自动识别和分类植物上的病虫害。以下是这一技术的一些关键步骤和组成部分: 1. **数据收集**:首先需要收集大量的植物图像数据,这些数据包括健康植物的图像以及受不同病虫害影响的植物图像。 2. **图像预处理**:对收集到的图像进行处理,以提高后续分析的准确性。这可能包括调整亮度、对比度、去噪、裁剪、缩放等。 3. **特征提取**:从图像中提取有助于识别病虫害的特征。这些特征可能包括颜色、纹理、形状、边缘等。 4. **模型训练**:使用机器学习算法(如支持向量机、随机森林、卷积神经网络等)来训练模型。训练过程中,算法会学习如何根据提取的特征来识别不同的病虫害。 5. **模型验证和测试**:在独立的测试集上验证模型的性能,以确保其准确性和泛化能力。 6. **部署和应用**:将训练好的模型部署到实际的病虫害检测系统中,可以是移动应用、网页服务或集成到智能农业设备中。 7. **实时监测**:在实际应用中,系统可以实时接收植物图像,并快速给出病虫害的检测结果。 8. **持续学习**:随着时间的推移,系统可以不断学习新的病虫害样本,以提高其识别能力。 9. **用户界面**:为了方便用户使用,通常会有一个用户友好的界面,显示检测结果,并提供进一步的指导或建议。 这项技术的优势在于它可以快速、准确地识别出病虫害,甚至在早期阶段就能发现问题,从而及时采取措施。此外,它还可以减少对化学农药的依赖,支持可持续农业发展。随着技术的不断进步,图像识别在病虫害检测中的应用将越来越广泛。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值