JVM内存结构、Java对象模型、Java内存模型(JMM)

JVM内存结构

JVM内存结构:java代码是运行在虚拟机上,虚拟机会将内存分为不同的区域,每个区域又有不同的作用。
在这里插入图片描述
class文件经过类加载器转换后会到达运行数据区,即Runtime Data Area。绿色的(方法区和堆)是线程共享的,黄色的(java栈,本地方法栈和程序计数器)是线程私有的。

  • 堆(heap):最大的一块,也占用内存最多。里面主要是new出来的以及其他指令创建的实例对象,并且这些实例对象不再有引用的话会被垃圾回收,包括数组,因为数组也是对象。
  • 虚拟机栈(JVM stack):也就是上图里的java栈,保存了各个基本的数据类型,以及对于对象的引用,特点就是在编译的时候就确定了大小,在运行时大小不会改变。
  • 方法区(methed):存储的是已经加载static,类信息以及常量信息,还包含着永久引用。普通的引用在虚拟机栈中,但是像有些引用是static对象的引用,也就是永久引用,会放在方法区中。
  • 本地方法栈:保存和本地方法相关的信息,本地方法指的是native方法。
  • 程序计数器:它所占的区域是最小的,主要保存当前线程执行程序字节码的行号数,也就是在上下文切换时的数据会保存下来,还包括下个程序需要执行的指令、分支、循环等异常处理,这些是依赖程序计数器的。

Java对象模型

Java对象模型:每个对象在JVM中存储是有一定结构的,这个结构模型称之为Java对象模型。
在这里插入图片描述

  • Java对象模型是Java对象的存储模型。
  • JVM会给每个类创建一个instanceKlass,保存在方法区中,用来在JVM层表示该Java类。
  • 在Java代码中,使用new创建对象时,在栈中对对象进行赋值,JVM会在堆创建instanceOopDesc对象,这个对象包含了对象头和示例数据。

JMM(Java Memory Model):Java内存模型

1.JMM 是一组规范,需要各个JVM共同来实现遵守JMM规范,以便于开发者可以利用这些规范,更方便的开发多线程程序。
如果没有这样一个JMM内存模型,那么很可能经过不同的JVM的不同规则的重排序,导致不同的虚拟机上运行的结果不一样,这是很大的问题。
2.JMM除了是一种规范,还是工具类和关键字原理,

  • volatile、synchronized、Lock等的原理都是JMM。
  • 如果没有JMM,那需要我们自己制定什么时候用内存栅栏等(内存栅栏可以简单的认为它是工作内存和主内存之间的拷贝和同步),那是相当麻烦的;因为有了JMM我们只需要用同步工具和关键字就可以开发并发程序。
  • 最最重要的三点内容:重排序、可见性、原子性(说起JMM一定要能想到这三点)

重排序:

  • 定义:代码实际执行顺序和代码在java文件里的顺序不一致,代码指令并不是严格按照代码语句顺序执行,他们的顺序被改变了,这就是重排序。
  • 好处:提高处理速度(重排序会对整个代码进行指令的优化、)
  • 重排序的三种情况:编译器优化、CPU指令重排序、内存的“重排序”
/**
 * 演示重排序的现象
 * 因为不是每次都出现,直到某个条件才停止
 */
public class OutOfOrderExecution {
    private static int x = 0,y = 0;
    private static int a = 0,b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for (; ; ){
            i++;
            x = 0;y=0;a=0;b=0;
            //CountDownLatch 工具类可以起到闸门的作用,后面的数字代表几次倒计时,为了演示下面结果里的第三个,否则就不需要加这个工具类
            CountDownLatch latch = new CountDownLatch(1);
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //等待放开闸门,为了能出现两个线程能同时执行第一个赋值语句;
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    a = 1;
                    x = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //等待放开闸门,为了能出现两个线程能同时执行第一个赋值语句
                        latch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    b = 1;
                    y = a;
                }
            });
            one.start();
            two.start();
            //放开闸门,
            latch.countDown();
            one.join();
            two.join();

            String result = "第"+i+"次 ("+ x + "," + y+")";
            if(x == 1 && y == 1){
            //下面的if是演示第四种情况,也就是重排序
//            if(x == 0 && y == 0){
                System.out.println(result);
                break;
            }else{
                System.out.println(result);
            }
        }
    }
}
  • 上方代码执行顺序决定了x和y的值,正常情况下按我们分析一共三种情况,正常,那肯定有不正常情况,就是第四种出现重排序
  1. a=1;x=b(0);b=1;y=a(1);结果x=0,y=1
  2. b=1;y=a(0);a=1;x=b(1);结果x=1,y=0
  3. b=1;a=1;x=b(1);y=a(1);结果x=1,y=1(这个结果得多次执行才能出现,或者将代码全放进for循环里,就不用多次运行了)
  4. 出现了 x=0,y=0,正常意义上是不可能出现的,能出现就说明经过了重排序,线程2的代码的执行逻辑被改变成了,y=a;a=1;x=b;b=1; 我运行时第291539次出现了(……第291538次 (0,1) 第291539次 (0,0))

可见性

/**
 * 演示可见性带来的问题
 */
public class FieldVisibility {
    int a = 1;
    int b = 2;
    private void print() {
        System.out.println("a = "+a+",b = "+b);
    }
    private void change() {
        a = 3;
        b = a;
    }
    public static void main(String[] args) {
        while (true){
            FieldVisibility test = new FieldVisibility();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();
        }
    }
}

分析结果里的四种情况:
a = 3,b = 2
a = 1,b = 2
a = 3,b = 3
a = 1,b = 3
前三种很好理解,最后一个a = 3,b = 1出现的非常罕见,原因是第一个线程将a和b改成了3,但是两个线程不能直接通讯,需要通过主存,因为主存同步,第一个线程在自己的本地内存里进行的修改,并将b=3同步到主内存中,a=3还没同步过去,线程2就进行了打印。

解决方法:为了避免上面的第四种情况,使用volatile关健字,这个关健字就会强制线程每次读取的都是被修改过的,也就是最新的值。

为什么会出现可见性:
在这里插入图片描述

  • CPU有多级缓存,导致读的数据过期。
  1. 高速缓存的容量比主存小,但是速度仅次于寄存器,所以在CPU和主存之间就多了Cache层
  2. 线程间的对于共享的可见性问题不是由多核引起的,而是由多级缓存引起的。
  3. 如果所有核心都只有一个缓存,就不存在内存可见性问题了,因为大家看到的都是同一缓存的数据。
  4. 每一个核心都会将自己需要数据读到独占缓存中(每一个核心都有自己独占的缓存),数据修改后也是写入到自己独占缓存中,然后等待刷入到内存中。所以会导致有些核心读取的值是一个过期的值

我们的主存(RAM)是最大的,但运行速度却是非常的慢,而越往上的缓存越小,但是速度却越快,而CPU的运行速度非常快。如果每次从RAM里拿数据,严重影响CPU效率,所以CPU直接从寄存器(registers)或者L1里面拿,是相对而言比较快的,但是由于不是直接从RAM里获取的数据,就可能出现可见性问题。例如核心4改写了值并且同步到了右边的L2,而左边的L2是从L3获取的,L3此时还不知道右侧的L2变化了,就导致核心1和核心4看到同一个变量在同一时刻的值不一样。

什么是主内存和本地内存?

  • JMM定义了一套读写内存数据的规范,我们不需要关心一级缓存二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
  • 这里的本地内存并不是一块给每个线程分配的内存,而是JMM的一个抽象,是对寄存器、一级缓存、二级缓存等的抽象。
    主存和本地内存
  • JMM有以下两个规定
  1. 所有变量都存储在内存中,同时每个线程也有自己独立的工作内存,每个工作内存中的变量内容是主内存的拷贝
  2. 线程不能直接读取主内存中的变量,而是只能操作自己内存中的变量,然后再同步到主内存中。
    主内存多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。
  • 总结:所有的共享变量存在主内存中,每个线程都有自己的本地内存,而且线程读写共享数据也是通过主内存交换的,所以才导致了可见性问题

* happens-before

什么是happens-before?

  • happends-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看到A,这就是happens-before。
  • 两个操作可以用happens-before来确定他们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。

什么场景符合happends-before

  1. 单线程原则:一个线程运行一段逻辑,比如上方的change()方法a=3;b=a;,如果a=3先运行,那给b赋值就一定能看到a为3,注意,说的是a=3先运行,因为他可能出现重排序的,导致a=3后运行。happends-before并不影响重排序
  2. 锁操作(Synchronized和Lock):一个线程解锁了,另一个线程加锁,它一定能看到解锁的线程的操作。
  3. volatile变量:只要写线程对volatile修饰的变量完成了写操作,读线程就一定能看到。而且它不仅能保证自己的可见性,它还能对它之前的变量操作保证可见性,这就是volatile的近朱者赤原则。
  4. 线程启动:主线程启动子线程,子线程一定能看到主线程启动它之前所有的操作。
  5. 线程join:join后面的语句一定能看到刚才我等待的线程所有语句的执行。
  6. 传递性:如果hb(A,B)而且hb(B,C),那么可以推出hb(A,C).
  7. 中断:一个线程被其他线程interrupt时,那么检测中断(isInterrupted)或抛出InterruptedException一定能看到。
  8. 构造方法:对象构造方法的最后一行指令happens-before于finalize()方法的第一行指令。
  9. 工具类的Happens-Before原则
    1. 线程安全的容器get一定能看到在此之前的put等存入动作。
    2. CountDownLatch:上面用到了,这个工具类可以起到闸门的作用,后面的数字代表几次倒计时。
    3. Semaphore:信号量,和CountDownLatch一样都是控制线程流程的。它是一个计数信号量,必须由获取它的线程释放。 常用于限制可以访问某些资源的线程数量,例如通过 Semaphore 限流。
    4. Future:可以后台执行,并且可以拿到线程执行结果的类,它的get方法就是拿到执行的结果。get方法对它执行逻辑是可见的,所以get时不会出现拿到了执行了一半的结果。
    5. 线程池:我们会给线程池提交很多任务,用submit等方法,而在提交的任务中,每一个任务,它在提交前都能看到提交前的所有执行结果。
    6. CyclicBarrier:和CountDownLatch一样也是起到一个栅栏的作用,之前也用到过。这个工具类可以让线程根据我们需要在某一个地方等待,直到要等待的人员都就绪,然后一起出发。

* volatile关键字

  • volatile是什么?
    volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。
    如果一个变量被修饰成volatile,那么JVM就知道这个变量可能会被并发修改,JVM就会进行禁止重排序等操作。
    由于开销小,相应能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchronized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。
  • volatile的使用场景
  1. 不适用:a++
  2. 使用场合1:boolean(布尔值)或者flag(标记位),如果一个共享变量自始至终是只被各个线程直接赋值(赋值不取决于之前的值,比如boolean = !boolean ,这种加volatile也没用),而没有其他读改等操作,那么就可以被volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以就足以保证线程安全。
  3. 适用场合2:作为刷新之前变量的触发器
  • volatile的作用:可见性、禁止重排序
  1. 可见性:读一个volatile变量之前,需要先使响应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立刻刷入到主内存
  2. 禁止指令重排序优化:解决单例双重锁乱序问题。
  • volatile和synchronized的关系
    volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程直接赋值而没有其他操作,那么就可以被volatile来代替synchronized或者代替原子变量,因为赋值自身具有原子性,而volatile又保证了可见性,所以就足以保证线程安全。
  • 用volitile修正重排序问题
/**
 * 演示volatile禁止重排序,给上面演示重排序代码OutOfOrderExecution,变量加上volatile,就不会出现x == 0 && y == 0
 */
`public class OutOfOrderExecution {
    private volatile static int x = 0,y = 0;
    private volatile static int a = 0,b = 0;``
    …………
    …………
  • volatile可以使得long 和 double 的赋值是原子的,因为这两个赋值本身不是原子的,后面会说。

能保证可见性的措施

  • 除了volatile可以让变量保证可见性外,synchronized、Lock、并发集合、Thread.join()和Thread.start()等都可以保证可见性。

synchronized

  • synchronized不仅保证了原子性,还保证了可见性
  • synchronized不仅让被保护的代码安全,还近朱者赤

原子性

  • 什么是原子性
    一系列操作,要么全部执行成功,要么全部不执行,不会出现执行部分的情况,是不可分割的。
    ATM取钱或转账 就是很好的例子
    i++ 不是原子性
    用synchronized实现原子性
  • Java 原子操作有哪些
  1. 除了long和double之外的基本类型(int,byte,boolean,short,char,float)的赋值操作
  2. 所有引用reference的赋值操作,不管是32位的机器还是64位的机器
  3. java.concurrent.Atomic.*包中所有类的原子操作
  • long 和 double的原子性
    问题描述:官方文档、long和double是64位值,对其写入会视为两个32位单独的写入操作、读取错误、使用volatile解决。
    结论:在32位的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的。
    实际开发中:商用Java虚拟机中不会出现这种问题,因为在32位机器上自动的保证了long 和 double的写入的原子性
  • 原子操作+原子操作 != 原子操作
    简单的把两个原子操作组合在一起,并不能保证整体依然具有原子性。
    全同步的HashMap也不完全安全。单独操作没有多大的问题,但是将多个操作组合起来,可能会出现问题。
    上篇:多线程带来的性能问题
    下篇:单例模式的8种写法
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值