设计建筑的软件,上海百度移动关键词排名优化,管理咨询公司名字起名大全,伊春网站制作注意区分Java内存模型#xff08;Java Memory Model#xff0c;简称JMM#xff09;与Jvm内存结构#xff0c;前者与多线程相关#xff0c;后者与JVM内部存储相关。本文会对两者进行简单介绍。
一、JAVA内存模型(JMM)
1. 概念
说来话长#xff0c;由于在不同硬件厂商和…注意区分Java内存模型Java Memory Model简称JMM与Jvm内存结构前者与多线程相关后者与JVM内部存储相关。本文会对两者进行简单介绍。
一、JAVA内存模型(JMM)
1. 概念
说来话长由于在不同硬件厂商和不同操作系统之间内存访问有一定差异所以会使得相同代码在不同平台上运行结果可能不一致。为了使java程序在各种平台下达成一致的运行效果所以JMM屏蔽掉各种硬件和操作系统的内存访问差异。 JMM规定除局部变和方法参数以外的所有变量都存储在主内存中。从线程角度其基本工作方式是工作内存保存了线程用到的变量和主内存的副本只能修改工作内存的值然后刷回主存不能直接读写主内存中的变量。
一般问到Java内存模型都是想问多线程Java并发相关的问题。
2. 内存屏障
现代计算机CPU多为多核每核有自己的高速缓存易导致内存数据读写不一致产生指令乱序和不可见性问题。内存屏障确保指令顺序执行和内存操作的全局可见性防止重排序并即时更新和展示内存数据给其他CPU核解决读写延迟问题。读屏障清除缓存确保后续读取最新数据写屏障刷新缓存数据到内存使其对其他核可见。JMM针对读load写store提出了针对这两个操作的四种组合来覆盖度读写的所有情况。LoadLoad 屏障确保所有之前的读操作都完成后再执行之后的读操作。StoreStore 屏障确保所有之前的写操作都完成后再执行之后的写操作。LoadStore 屏障确保所有之前的读操作都完成后再执行之后的写操作。StoreLoad 屏障确保所有之前的写操作都完成并对其他处理器可见后再执行之后的读操作。
3.原子性 可见性 有序性
3.1原子性
原子性指的是一个操作是不可分割不可中断的一个线程在执行时不会被其他线程干扰。i不是原子操作因为它是先读取到i再加1是两步操作不保证原子性。代表性的是synchronized关键字该关键字修饰的方法或代码块可保证原子性。
3.2 可见性
可见性是指一个线程修改了某个变量的值这个改动能立即被其他线程感知。volatile关键字可以保证变量的可见性当变量被该关键字修饰时这个变量的改动会被立即刷新到内存其他线程会在主内存中读取该变量的新值。final和synchronized也可保证可见性。happens-before happens-before是指前一个操作的结果对后续操作是可见的并不是指前面一个操作一定发生在后面一个操作的前面。在不改变程序执行结果的前提下编译器和处理器可以自由优化程序执行顺序因为程序员只关心程序执行的语义是否正确。
3.3 有序性
在Java中volatile和synchronized都能维护多线程操作的有序性。volatile通过内存屏障禁止指令重排而synchronized则通过锁定机制确保同一时间只有一个线程可以执行被其保护的代码块从而实现有序性。
4. synchronezid volatile关键字
4.1 synchronezid
4.1.1 基本使用
synchronezid可以修饰方法、类和代码块。修饰实例方法锁住的是对象即对象锁修饰静态方法锁住的是类即类锁修饰代码块指定加锁对象对给定对象加锁也是对象锁。 对象锁可以有多个new几个对象就有几个对象锁但是类锁只有一把。
//修饰方法
public synchronized void add(){i;
}
//修饰类
public static synchronized void add(){i;
}
//修饰代码块
public void add() {synchronized (this) {i;}
}4.1.2 底层原理
查看上面代码的字节码
//修饰代码块
public void add();Code:0: aload_01: dup2: astore_13: monitorenter // synchronized关键字的入口4: getstatic #2 // Field i:I7: iconst_18: iadd9: putstatic #2 // Field i:I12: aload_113: monitorexit // synchronized关键字的出口14: goto 2217: astore_218: aload_119: monitorexit // synchronized关键字的出口20: aload_221: athrow22: return
通过字节码文件看出synchronized修饰代码块使用monitorenter和monitorexit指令。monitorenter指令指向同步代码块的开始位置monitorexit指令则指明同步代码块的结束位置。每个对象有一个监视器锁monitor。当monitor被占用时就会处于锁定状态线程执行monitorenter指令时尝试获取monitor的所有权设置计数器值为1。执行monitorexit指令将释放 monitor(锁)并设置计数器值为0。monitor存储于对象头信息中每个对象都存在一个monitor与之关联。
//修饰方法
public synchronized void add();descriptor: ()Vflags: (0x0021) ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack3, locals1, args_size10: aload_01: dup2: getfield #2 // Field i:I5: iconst_16: iadd7: putfield #2 // Field i:I10: returnLineNumberTable:line 5: 0line 6: 10
synchronized修饰实例方法对应的字节码没有 monitorenter和monitorexit 却额外多了 ACC_SYNCHRONIZED。因为整个方法都是同步代码因此就不需要标记同步代码的入口和出口了。当线程线程执行到这个方法时会判断是否有这个ACC_SYNCHRONIZED标志如果有的话则会尝试获取monitor对象锁。如果有异常发生线程自动释放锁。
4.2 volatile
能保证变量的可见性禁止指令重排序。可见性原理 每个线程都有一个Jvm栈栈内保存线程运行时的变量信息。当线程访问对象的属性时首先会找到堆内对象存的变量值再将其保存为栈内的一个副本之后会直接修改副本中属性的值。修改完后不会立即将修改的值更新到堆中这就导致某些线程读取到的还是旧值。volatile就是当副本中属性的值被修改后保证其能立即同步到堆中从而其他线程读取到该值也是新的值。
禁止指令重排序原理 通过插入内存屏障禁止指令重排序。插入内存屏障相当于告诉CPU和编译器先于这个命令的必须先执行后于这个命令的必须后执行。volatile写操作的前面插入一个StoreStore屏障后面插入一个SotreLoad屏障。
volatile不能保证线程安全可见性不能保证原子操作
二、JVM内存结构
1. 组成 JVM的内存划分为5部分Java栈本地方法栈堆程序计数器和方法区。 1-JAVA栈 即虚拟机栈 根据线程创建而创建所以每个线程都有一个虚拟机栈。虚拟机栈存储的是栈帧每个栈帧对应一个方法且都有自己的局部变量表操作数栈、动态链接和返回地址等。 局部变量表存放了编译器可知的各种基本数据类型int、short、byte、char、double、float、long、boolean、对象引用reference类型它不等同于对象本身可能是一个指向对象起始地址的引用指针也可能是指向一个代表对象的句柄或其他与此对象相关的位置和returnAddress类型指向了一跳字节码指令的地址。 JVM规范中Java虚拟机栈部分规定了两种异常StackOverflowError发生在递归调用过深时由于程序设计的错误如递归无终止条件OutOfMemoryError发生在JVM内存不足或设置过小导致无法为新线程分配栈空间。 2-本地方法栈 java虚拟机栈为虚拟机执行Java方法服务。本地方法栈则为虚拟机使用的native方法服务。native方法是用C语言实现的底层方法。 3-堆 生命周期与进程相同被所有线程所共享的内存区域。该区域存放的是对象实例。堆同时也是GC的主要区域。通常情况下它占用的空间是所有内存区域中最大的但如果无节制地创建大量对象也容易消耗完所有的空间堆的内存空间既可以固定大小也可运行时动态地调整通过参数-Xms设定初始值、-Xmx设定最大值。 4-程序计数器 它是一块极小的内存空间。记录了当前线程执行到的字节码行号。每个线程都有自己的程序计数器互不影响。native方法计数器为空。 5-方法区 被线程共享储存已被虚拟机加载的类信息、常量、静态变量、jit编译后的代码等数据。
Java源代码编译成Java Class文件后通过类加载器ClassLoader加载到JVM中 类存放在方法区中 类创建的对象存放在堆中 堆中对象的调用方法时会使用到虚拟机栈本地方法栈程序计数器 方法执行时每行代码由解释器逐行执行 热点代码由JIT编译器即时编译 垃圾回收机制回收堆中资源 和操作系统打交道需要调用本地方法接口
2. 类加载过程 2.1 加载
加载指的是将类的class文件读入到内存中并为之创建一个java.lang.Class对象。 类加载阶段可以使用系统提供的类加载器ClassLoader来完成也可以使用用户自定义的类加载器继承ClassLoader完成。
2.2 连接
2.2.1 验证
验证被加载的类文件符合JVM规范保证载入的类不会危害JVM。
文件格式验证→元数据验证→字节码验证→符号引用验证
2.2.1.1 文件格式验证 2.2.1.2 元数据验证 2.2.1.3 字节码验证 2.2.1.4 符号引用验证 2.2.2 准备
在方法区中为类变量(被static修饰的变量)分配内存并将其初始化为默认值。
对于 public static int value 123;变量value在准备阶段过后的初始值为0而不是123初始化时才会将value值赋为123。 如果类字段的字段属性表中存在ConnstantValue属性那在准备阶段value就会被初始化为ConstantValue属性所指定的值如public static final int value 123;编译时Javac将会为value生成ConstantValue属性在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
2.2.3 解析
将类中的符号引用转化为直接引用。编译的时候每个java类都会被编译成一个class文件但在编译的时候虚拟机并不知道所引用类的地址所以就用符号引用来代替。符号引用以一组符号来描述所引用的目标。直接引用可以是直接指向目标的指针。 2.3 初始化
执行类的初始化方法clinit()方法来初始化类的静态变量程序设置值和执行静态代码块。
2.4 使用
2.5 卸载
3. 类加载机制
1、全盘负责 类加载器加载某个类时该类所依赖和引用其它的类也由该类加载器载入。
2、双亲委派 先让父加载器加载该类父加载器无法加载时才考虑自己加载。 如果父加载器还存在其父加载器则进一步向上委托如果父类加载器可以完成父加载任务就成功返回如果父加载器无法完成加载任务子加载器才会尝试自己去加载可避免重复加载。
3、缓存机制 缓存机制保证所有加载过的class都会被缓存当程序中需要某个类时先从缓存区中搜索如果不存在才会读取该类对应的二进制数据并将其转换成class对象存入缓存区中。 这就是为什么修改了class后必须重启JVM程序所做的修改才会生效的原因。
4. 反射
Java 的反射机制是指在运行状态中对于任意一个类都能够知道这个类所有的属性和方法 并且对于任意一个对象都能够调用它的任意一个方法这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
4.1 实例化方式
Date datenew Date();
//方式1
Class? date Class.forName(java.util.Date);
//方式2
System.out.println(date.getClass());
//方式3
System.out.println(Date.class);
4.2 实例化对象
//通过反射机制获取Class通过Class来实例化对象
Class? clClass.forName(java.util.Date);
//newInstance() 这个方法会调用Date这个类的无参数构造方法完成对象的创建。
// 重点是newInstance()调用的是无参构造必须保证无参构造是存在的
Object objectcl.newInstance();
5.GC
(待施工)