jvm虚拟机学习笔记

什么是jvm
  • 定义:java虚拟机,java二进制字节码运行的环境
  • 好处
    • 一次编译,到处运行
    • 自动内存管理,垃圾回收功能
    • 数组下标越界检查
    • 多态(虚方法表)
  • 比较jvm,jre,jdk
    • jvm:只是一个运行环境
    • jre(java运行环境):jvm+基础类库
    • jdk(java开发工具):jre+编译程序
实现路线
  • 类加载器
  • jvm内存结构
    • 方法区:类
    • 堆(Heap):类创建的实例对象
    • 虚拟机栈
    • 程序计数器
    • 本地方法栈
  • 执行引擎
    • 解释器:逐行解释运行(将字节码翻译成机器码)
    • 即时编译器:热点代码编译,优化后的执行
    • 垃圾回收(GC):回收类里不再用的实例对象
程序计数器(寄存器)
  • 记住下一条jvm指令的执行地址
  • 解释器去程序计数器里找到下一条指令
  • 通过cpu的寄存器实现
  • 特点
    • 线程私有:一个程序计数器属于一个线程
    • 不会存在内存溢出(唯一一个)
虚拟机栈
  • 线程运行需要的空间,一个线程一个栈
  • 每个栈由栈帧组成,一个栈帧对应着方法的调用,即每个方法运行需要的内存
  • 参数,局部变量,返回地址等都需要占用地址,预先分配好
  • 方法执行,进栈,执行完,出栈,方法一调用方法二,方法二进栈,方法二先出栈,方法一再出栈,方法二的返回地址就是方法1
  • 每个线程有一个活动帧,代表正在执行的方法
  • 问题
    • 垃圾回收不涉及栈内存,栈自己就弹出了
    • 栈画的太大,会让线程数变少
    • 方法内的局部变量线程安全:局部变量是每个线程私有的,不会相互影响
    • 方法内的局部变量是否线程安全?
      • 如果该局部变量没有逃离方法的作用范围,则线程安全
      • 若是传入的参数或返回值,引用了对象并逃离了方法范文,则不是安全的
      • 若是基本数据类型,则是线程安全的
  • 栈溢出
    • 方法一直调用,栈帧太多,不出栈(如递归没有退出条件)
    • 栈帧太大(少见)
本地方法栈
  • 给本地方法提供内存空间
  • 之后的部分都是线程共享的
  • 定义:通过new创建的对象都使用堆内存
  • 特点
    • 线程共享,堆中的对象都要考虑线程安全问题
    • 有垃圾回收机制
  • 堆内存溢出
方法区
  • 储存类的结构的成员信息:成员变量,成员方法数据,成员方法和构造器的代码
  • 方法区在jvm启动的时候被创建
  • 逻辑上是堆的组成部分
    ####运行时常量池
  • 二进制字节码(类基本信息,常量池,类方法定义,包含了虚拟机指令)
  • 常量池就是一张表,虚拟机根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息
  • 运行时常量池,常量池是*.class文件中的,当该类被加载,他的常量池就会被放入运行时常量池,并且把里面的符号变成真实地址
    ####StringTable(串池)
  • 是一个哈希表,加入进来(用到这行代码时)的时候看有没有重复的,不能扩容
 public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "ab";
//new StringBuilder().append(s1).append(s2).toString
        String s4 = s1 + s2;//首先创建一个新的StringBuilder
        //此时s3在串池中,而s4是new出来的,在堆里面
        System.out.println(s3==s4);//false
        //到常量池中找一个值为ab的结果,和s3的结果是一样的
        String s5 = "a" +"b";//javac的编译期优化,a和b都不是变量,在编译期已经稳定为ab
//        在常量池中新增"a","b",同时堆中增加两个new String 对象,而形成的新的new String("ab")只存在在堆中,没有存入常量池
        String s = new String("a")+new String("b");
        String s6 = s.intern();//将这个字符串放入串池,并返回串池中的对象
        System.out.println(s3==s6);//true
        //注意这个地方如果常量池中还没有ab,则放入的直接就是s这个对象,之后不管是s3=ab还是什么,都用的是常量池中的这个对象,即都和s相等
        // 而如果是1.6版本,则复制一个放入常量池,s和常量池中返回的不是一个


    }
  • toString 方法需要创建新的字符串对象
  • 常量池中的字符串仅是符号,第一次用才变为对象
  • 利用串池机构,避免重复创建字符串对象
  • 字符串变量拼接原理是StringBuilder
  • 字符串常量拼接原理是编译期优化
  • 可以使用intern方法,主动将串池中没有的字符串放入串池
  • 动态拼接的字符串不放入串池
StringTable的位置
  • 1.6储存在常量池中,而常量池在方法区,方法区在jvm内存中(永久代,回收效率低full GC)
  • 1.8中则放在堆中(但是和放New对象的不在一个区域)
StringTable 垃圾回收
  • 字符串常量在内存不足时也会被垃圾回收
StringTable性能调优
  • 哈希表的桶的个数多,碰撞较小,查找效率高
  • 可以让字符串入池减少字符串个数,提高效率
直接内存
  • 属于系统内存,不属于jvm内存
  • 常见于NIO操作,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受JVM内存回收管理
  • java本身不具有读写磁盘文件的能力,磁盘文件先加载到系统缓冲区,再加载到java缓冲区,才能用java读取
  • 而直接内存实在系统划分出了一块区域,java代码可以直接访问
  • 会导致内存溢出

垃圾回收

如何判断一个垃圾可以被回收
  • 引用计数法,被引用加一,不被引用-1,到0就会被回收,但是可能导致互相引用的问题,导致内存泄露
  • jvm采用的是可达性分析算法
    • 找到所用不会消失的对象,即根对象(GC root)
    • 扫描,如果一个对象被根对象直接或间接使用,则不被回收,反之则可以被回收
  • GC root 对象
    • 核心类库
    • 操作系统引用的对象
    • 被加锁的对象(syncronized)
    • 活动线程中的对象,栈帧内的引用对应的对象
    • 引用存在在活动栈帧里,引用的对象(new 出来的)存在在堆里面
四种引用
  • 强引用
    • 用等号把一个对象赋值给一个引用,是强引用,强引用可用GC root链找到,只用强引用都消失了,对象才能被回收
  • 软引用,弱引用
    • 软引用若垃圾回收且内存不足时,对象就会被回收
    • 弱引用只要垃圾回收对象就会被回收
    • 如果有引用队列,则软引用弱引用进入引用队列,配合引用队列来释放引用本身
  • 虚引用,终结器引用必须配合引用队列
    • 虚引用:直接内存的引用,由Reference Handler 线程调用虚引用方法释放内存
    • 终结器引用:有终结方法finalize,先加入引用队列,再回收,再引用引用队列的引用,才能执行终结方法,才能被回收(不推荐使用)
  • 软引用:一些资源可以不用一直放在内存里(如不重要的图片等)
//List 对SoftReference的引用是硬引用
//SoftReference对byte数组的引用是软引用
List<SoftReference<byte[]>> list = new ArrayList<>();
//软引用对象被回收之后,引用它的(null)仍旧存在于数组中,因此要使用引用队列
ReferenceQueue<byte[]> queue = new ReferenceQueue<>(new byte[],queue);
List<SoftReference<byte[]>> list = new ArrayList<>();
  • 弱引用:与软引用类似,WeakReference

垃圾回收算法

标记清除
  • 按GC root链查找后,标记没有被引用的对象
  • 清除这些被标记的部分(将其其实和结尾坐标放到空闲地址列表里,之后可以分配给别的对象
  • 优点:速度快
  • 缺点:会产生内存碎片,没有对释放的空间整合,不连续
标记整理
  • 先标记
  • 清理的过程中将可用的对象向前移动,空出连续的空间
  • 缺点:效率下降
  • 优点:有连续空间
复制
  • 将内存区划分成大小相等的两部分
  • 先标记
  • 将FROM区的可用对象复制到TO区
  • 直接清除FROM区
  • 交换FROM和TO的位置
  • 优点:没有碎片
  • 缺点:占用额外的空间
实际:分代回收算法,将上面三种算法结合在一起
  • 分为新生代和老年代
  • 新的对象诞生在伊甸园中,逐渐被占满
  • 触发新生代拉架回收(Minor GC),去标记要被回收的对象,采用复制算法复制没被标记的到TO,,清除伊甸园,幸存对象寿命+1
  • 交换TO和FROM
  • minor GC会引发stop the world,暂停其他用户线程(防止地址改变导致混乱),时间非常短
  • 第二次垃圾回收,标记伊甸园和幸存区,重复上面的操作
  • 幸存区的对象寿命超过一定阈值,晋升到老年代
  • 老年代快放满了,先再尝试minor GC,如果还不行,做老年代垃圾回收(full GC),从新生代到老年代,整个垃圾回收,STW更长
  • 老年代采用标记清除或者标记整理
  • 如果仍不足,就内存溢出了

垃圾回收器

串行垃圾回收器
  • 单线程,回收时暂停其他线程
  • 堆内存较小,适合个人电脑(CPU个数少)
  • 新生代+老年代
  • 触发垃圾回收后,让所有的线程在安全点停下来,开启一个垃圾回收线程,其他阻塞,结束后其他再运行
吞吐量优先(必行垃圾回收器)
  • 多线程,堆内存较大,多核CPU
  • 让单位时间内,STW的时间最短
  • 1.8下使用它
  • 垃圾回收在安全点停下来,开启多个垃圾回收线程,回收结束恢复运行
  • 所有核都去垃圾回收了,占用率达到100%
  • 调整堆增大,是垃圾回收次数下降,但是单次回收时间增加,需要取折中值
响应时间优先(CMS)
  • CMS针对是老生代
  • 多线程,堆内存较大,多核CPU
  • 单次STW的时间(暂停线程)时间最少
  • 老年代并发的标记回收,并发能尽量减少STW,有些时候不需要STW,有的时候需要,新生代还是复制
  • 并发:用户线程和用户线程同时运行;并行:多个垃圾回收线程同时运行,但是用户线程要堵塞
  • 内存不足,线程到达一个安全点,CMS发生STW,进行一个初始标记(只标记root的直接关联对象,所以很快),
    之后用户线程恢复运行,同时标记的线程并发标记。再次STW,进行重新标记(因为前面运行的一段时间线程可能引用了对象),
    多核同时重新标记,恢复用户线程,同时回收线程进行并发清理
  • 但是CMS占用了一部分核,会导致用户线程响应时间变长
  • 用户线程与垃圾回收线程并发时,会导致浮动垃圾,需要预留空间
  • 如果重新标记时间更长,重新标记前,再对新生代进行垃圾回收,以防止新生代引用老生代,扫描整个堆
  • 标记清除会导致碎片增多,此时退化为串行回收器,减少碎片,再恢复
  • 初始标记
    • 标记老年代中所有GC root对象和新生代中活着的对象直接引用老年代的对象
  • 并发标记
    • 从初始标记标记的对象找出所有标记的对象
  • 预清理阶段
    • 标记并发导致的新的引用
  • 重新标记
    • 标记整个老年代所有的存活对象,扫描整个堆,包括新生代和老生代,找到所有存在于老生代中的存活对象
G1(Garbage First)
  • 同时注意吞吐量和低延迟,默认暂停目标是200ms,是并发的
  • 超大堆内存,会将堆内存划分为多个大小相等的region(1248M)
  • 整体是标记整理算法,两个区域间是复制算法
  • 内存较小时G1和CMS差不多,堆很大时G1更快
G1的回收阶段
  • 新生代垃圾收集
    • 每个region都可以作为伊甸园,幸存区,和老年代
    • 新的对象先分配伊甸园,伊甸园放不下了发生新生代垃圾回收,发生STW,拷贝幸存的对象进入幸存区,清除伊甸园
    • 幸存区快慢了触发新的新生代垃圾回收,一部分进入老年代,一部分进入新的幸存区,当前的伊甸园也进入新的幸存区
  • 新生代垃圾收集+并发的标记(老年代)
    • 在进行Young GC的同时会进行GC Root的初始标记(初始标记不会占用并发标记的时间)
    • 老年代占比达到阈值的时候,进行并发标记
  • 混合收集
    • 此阶段对三个区域都进行全面的垃圾回收
    • 新生代发生新生代垃圾回收
    • 发生STW,进行最终标记
    • 发生STW,老年代根据并发标记,将幸存的复制到新的老年代(有选择的,不一定都清除,选择回收价值最高的,已达到短暂停),复制可以整理内存,减少碎片)
  • 三者循环进行
区分四种垃圾回收器
  • 四者的新生代都是minor GC
  • 串行和并行的老年代都是full GC
  • CMS和G1老年代内存不足(达到设定的阈值),如果清除的速度大于产生新的垃圾的速度,还处在并发标记阶段。
  • 如果垃圾回收速度跟不上产生新垃圾的速度,就会退化回串行收集,产生full GC,更长时间的STW
新生代垃圾回收的跨代引用(老年代引用新生代)
  • 去老年代里找引用新生代的引用效率非常低
  • 将老年代分成很多card,如果一个card引用了新生代,则标记为dirty card
  • 这样将来回收新生代不需要遍历整个老年代
  • 每次引用变更都要重新标记dirty card,是异步操作,不会立刻完成
    ####重新标记阶段
  • 并发标记阶段,如果用户线程改变引用,会增加一个写屏障,把被引用对象加入队列
  • 重新标记阶段会检查队列里的对象
G1字符串去重
  • 将所有新创建的字符串数组放入一个队列
  • 新生代回收时,G1并发检查是否有字符号串重复
  • 如果有,让两个引用指向一个char数组
    ##类加载和字节码文件
  • 解释+编译
类文件结构
  • 魔数:表示文件类型(ca fe bs be)四个字节
  • 版本,4个字节
javap
  • 反编译工具
类加载流程
  • 常量池数据放入运行时常量池(在方法区中)
    • 小数字在方法区,和字节码指令存在一起,大数字在常量池
  • 方法的字节码在方法区
  • 给main方法分配栈帧(局部变量表,操作数栈)
  • 执行引擎读取方法区中的字节码文件,开始执行
  • 将字节码中读取到局部变量压入操作数栈
  • 操作数栈中的局部变量弹出进入局部变量表
  • 如果字节码指向常量池中的数,让常量池中的数进入操作数栈
  • 发生操作时,将局部变量表中操作的变量读入操作数栈,发生操作,再弹出变量,结果存入局部变量表
  • 运行时常量池中找到局部变量的引用,指向堆中的System.out对象,将其引用放入操作数栈
  • 操作数栈读入要打印的变量
  • 找到常量池中的println,找到方法区中的该方法,生成新的栈帧
  • 将要打印的局部变量传递给新的栈帧
  • 方法执行完毕,弹出栈帧,清除之前操作数栈的引用和变量
  • byte,short,char都按int比较
构造方法
  • 编译期按照从上到下的顺序收集所有静态代码块和静态代码,合并成一个方法,在类加载的初始化阶段调用(inite)
  • 只有public方法因为能被重写,所以调用期间并不知道是哪个方法(动态绑定),需要运行时确定
  • new一个对象,将对象放入堆中,将对象的引用放入操作数栈,再复制一份放入操作数栈,调用结束从栈顶清除一个
  • 把剩余的引用放到局部变量表中
  • 该引用调用方法,入操作数栈,执行结束后出栈
  • 主义public方法直接就入栈出栈了
多态调用原理
  • public方法进入一个叫vtable的虚方法表中,在类结构中的最后
  • 先通过栈帧中的对象引用找到对象
  • 分析对象头,找到对象实际的class
  • class结构中有vtable,在类加载阶段已经根据方法重写规则生成了
  • 得到方法的具体地址
  • 执行该方法的字节码
异常处理
  • 异常表(Exception table)结构,from to 是前闭后开的检测范围,一旦这个范围内字节码执行出现异常,通过type匹配字节类型,如果一致,进入target指示行号
  • finally的工作方式,就是复制这一块字节码,复制到try块后面和catch后面
  • 如果异常没有被catch块捕获到,就会存储一个这个异常到一个没有名字的槽位,然后再进入后面的finally的字节码
  • finally中的代码被复制了三份,分别放入try流程,catch流程,和catch剩余的异常类型流程
  • 在finally代码中写return 会吞掉异常
  • 如果在try块中有return 语句,会先把return 的值存在这个槽位里,如果finally没有return,则这个值不会变,finally之后执行return,返回之前的值
  • 如果finally里面有return,则新的return的值替换那个槽位,返回新的return的值
synchronized
  • new一个对象要把引用放入栈顶两次,一次用于构造方法调用
  • lock对象的引用也要复制两份,一份用于加锁,一份用于解锁
  • 使用异常表,如果没有异常,将还剩一个的lock引用加载,执行解锁
  • 如果出现异常,就跳转到第二个解锁操作的地方,并把异常抛出

语法糖

  • 编译期优化和处理,自动生成一些字节码
默认构造器
  • 自动生成无参构造器,调用父类无参构造器
自动拆装箱
  • 基础类型和包装类型转换
泛型集合取值
  • 泛型编译成字节码会被擦除掉,按object操作,基础类型变为包装类型,包装类型再变为object
  • 取出值的时候要进行类型转换(checkcast)
  • 一部分泛型信息会被保留(如果作为局部变量),局部操作类型表会记录类型,但是操作还用object操作,这个信息不能反射拿到,只有get和return的值才能拿到
可变参数
  • String…等价于String[] args,无参创建新数组
foreach循环(增强for循环)
  • 对数组而言等价于for循环按下标遍历
  • 对集合相当于迭代器(因为继承了collection继承的Iterator)
switch
  • jdk7可配合字符串和枚举类
  • String转换成两个switch
  • hashcode在编译前确定,第一个switch和hashcode匹配,之后匹配equals比较String,然后定义一个byte值,不同哈希码byte值不同
  • 第二个switch匹配这个byte值
  • 有一些hashcode相同,但字符串内容不同(Hashcode冲突)
  • 枚举类会在当前类生成静态内部类,让枚举类作为index对应数组map里的值
  • switch时先从map里拿到值,在进行匹配
枚举类
  • 是final类继承了Enum
  • 枚举对象在静态代码块里被创建,同时创建数组存放枚举对象
  • 构造方法是私有的,防止在外部创建
    ####方法重写桥接方法
  • 子类重写的方法的返回值可以是父类方法返回值的子类
  • 生成了合成方法,返回值是父类返回值,实际返回的是子类返回值类型
    ####匿名内部类
  • 生成额外的类,实现了匿名内部类继承的方法
  • 方法内匿名内部类引用方法的局部变量必须是final的
  • jvm储存了这个参数,用匿名内部类的参数传了进去
  • 在匿名内部类的位置new了一个这个生成的类

类加载

加载
  • 将类的字节码加载到方法区中,采用c++的instanceKlass描述java类
    • java mirror即java的类镜像,如String 的klass的镜像为String.class,把klass对象暴露给java使用
    • super即父类
    • fields即成员变量
    • methods方法
    • constants常量池
    • class loader类加载器
    • vtable虚方法表
    • itable接口方法表
    • 先加载父类,再加载这个类
    • instanceKlass元数据存储在方法区中,class数据存储在堆中,创建的对象通过对象头链接到class数据
    • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
    • 在这里插入图片描述
链接
  • 验证,验证字节码是否符合规范
  • 准备:为静态变量分配空间,并设置默认值。静态变量存储在class数据里
    • 静态变量分配空间在准备阶段完成,赋值在初始化阶段完成
    • 如果静态变量是final的,则准备阶段就赋值,String 也是一样
    • 如果final变量是new的对象,则在初始化阶段赋值
  • 解析:将常量池的符号解析为直接引用,如类中引用了另一个类,则解析后就可以通过引用找到那个类了,而解析前只是一个那个类的名称的符号
初始化
  • 初始化调用()V方法,虚拟机会保证这个类的构造方法线程安全
  • 触发初始化的情况
    • 首先初始化main方法所在的类
    • 首次访问这个类的静态变量或静态方法时
    • 子类初始化会初始化父类
    • 如果子类访问父类的静态变量,只触发父类初始化
    • Class.forName
    • new 会导致初始化
  • 不会触发初始化的情况
    • 访问static final静态常量(基本数据类型和字符串)(包装类型会初始化,Integer.valueOf(20))
    • 类对象.class
    • 创建该类的数组
    • 类加载器的loadCLass方法
    • Class.forName的参数2为false时
  • 看类里的静态代码块有没有执行来判断有没有初始化
  • 单例懒惰模式,只有调用getInstance才会调用类的初始化

class Singleton{
    private Singleton(){}
    //内部静态代码块
    private static class LazyHolder{
        private static final Singleton SINGLETON = new Singleton();
    }
    public Singleton getInstance(){
        return LazyHolder.SINGLETON;
    }
}
  • 类的初始化
    • 编译的时候,会将静态代码收集,并编译成static方法{}
    • 初始化阶段会调用这个static方法

类加载器

启动类加载器(Bootstrap ClassLoader)(JAVA_HOME/jre/lib)
  • 无法直接访问
拓展类加载器(Extension ClassLoader)(JAVA_HOME/jre/lib/ext)
  • 上级为Bootstrap,显示为null
应用程序类加载器(Application ClassLoader)(classpath)
  • 上级为extension
自定义类加载器
  • 上级为Application
双亲委派
  • 双亲委派机制就是调用类加载器loadClass方法时,查找类的规则
  1. 检查该类是否已经被加载(在本类加载器中)
  2. 如果没有,调用上级类加载器loadClass方法(递归)
  3. 如果没有上级(Extension ClassLoader),则委派Bootstrap
  4. 如果都没有,本类加载器调用一个findClass方法(每个类加载器自己扩展)来加载
  5. 如果找到就return,找不到就返回classNotFound,注意如果上级返回了classNotFound,在递归的时候被下级try catch了
  • 在这里插入图片描述

  • 注意递过只到Extension ClassLoader,Bootstrap调用的c++的方法

  • 是线程安全的

线程上下文类加载器
  • 使用ServiceLoader机制加载驱动,即SPI

  • 根据接口找到文件,文件内容是要加载的类的类名

  • 使用ServiceLoader.load方法

  • 在这里插入图片描述

  • 每个线程启动的时候,默认把应用程序类加载器给它

  • 破坏了双亲委派机制(按照双亲委派机制jdbc应该由Bootstrap调用,实际上是Application

自定义类加载器
  • 加载任意路径的类
  • 框架设计中通过接口来实现,希望解耦
  • 隔离这些类,不同应用的同名类都可以加载,常见于tomcat
  • 步骤
    • 集成ClassLoader父类
    • 遵从双亲委派机制,重写findClass方法
    • 读取类文件的字节码
    • 调用父类的defineClass方法加载类
    • 使用者调用该类加载器的loadClass方法
  • 用不同的类加载器会被加载两次,得到不同的类对象

运行期优化

  • 第0层,解释执行

  • 1层,使用C1编译期编译执行(不带profiling)

  • 2层,使用C1编译期编译执行(带基本profiling)

  • 3层,使用C1编译期编译执行(带完全profiling)

  • 4层,使用C2编译期编译执行

  • 在这里插入图片描述

  • profiling是指在运行过程中收集一些程序执行状态的数据,如方法调用次数,循环的回边次数

  • C2使用了逃逸分析,没有逃逸(没有在外部使用)的对象就不会生成

方法内联
  • 将方法内的代码拷贝黏贴到调用者的位置(直接调用调用者给的参数)
  • 可以进行常量折叠优化,即一个变量一直是同一个值,就直接将变量变成这个值
字段优化
  • 如果允许方法内联,将首次读到的外部的数组长度,数组等对象缓存为一个局部变量
反射优化
  • 一开始调用本地方法访问器
  • 达到阈值之后,使用一个运行期间动态生成的新的方法访问器,替换原本方法访问器,变成了直接正常访问,不是反射执行
  • 相当于生成了虚拟类,直接调用
    #内存模型
  • JMM定义了一套在多线程读写共享数据时(成员变量,数组)时,对数据的可见性,有序性,和原子性的规则和保障
    ####使用synchronized关键字
原子性
  • 同一时刻只能有一个线程进入被synchronized修饰的对象
  • 一个线程进入监视器(monitor),直接进入owner,第二个线程进入无法进入owner,只能进入EntryLIST
  • 第一个线程释放离开owner,entryList中的线程争抢owner
  • synchronized范围尽量大,减少加锁解锁线程
  • 加锁必须是同一个对象
  • 静态变量的自增和自减不是原子性,如下面的代码输出结果就不是0
public class test2 {
    static int i = 0;

    public static void main(String[] args) throws InterruptedException{
        Thread t1 = new Thread(() ->{
            for(int j = 0;j<50000;j++){
                i++;
            }
        });
        Thread t2 = new Thread(() ->{
            for(int j = 0;j<50000;j++){
                i++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
可见性
  • 由于jvm把成员变量放到了高速缓存内,所以修改成员变量不会影响线程
  • 解决方法,volatile关键字,修饰成员变量和静态变量,可以避免线程从自己的工作缓存中查找这个值,而必须到主存中获取它的值,线程中操作这个值也会直接影响主存
  • 这样可以保证可见性,即一个线程能看到另一个线程对它的修改,不能保证原子性
  • 只能用于一个线程写,多个线程读的情况
  • synchronized语句既能保证原子性,也能保证可见性,但是属于重量级操作,性能较低
  • 在线程循环内加println就可以导致可见性,因为println是synchronized修饰的
    ####有序性
  • 指令重排会破坏原本的顺序
  • 给变量加volatile修饰就能避免指令重排
  • 可见性规则总结
    • 线程解锁m前对变量的写,对接下来对m加锁的其他线程对该变量的读乐可见
    • 线程对volatile变量的写,对其他线程对该变量的读可见
    • 线程start前对变量的写,在该线程开始后对该线程可见、
    • 线程结束前对变量的写,对其他变量得知它结束后的读可见(如调用t1.isAlive()或者t1.join())
    • 线程t1打断t2前t2对变量的写,对其他线程得知t2被打断后的读对变量可见(通过t2.interrupted和t2.isInterrupted)

CAS与原子类

CAS
  • 是一种乐观锁的思想
  • 不加锁,对volatile变量操作,在一开始先储存共享变量的旧值
  • 结束时用compareAndSwap对旧值和共享变量现在的值进行比较
  • 如果与预期相同,就返回true,成功退出
  • 如果不同,返回false,重新进入while循环,重新判断
  • 因为没有synchronized所以不会阻塞,效率高
  • 但是如果竞争激烈,重试就会频繁发生,影响效率
  • 适合多核CPU场景,竞争不太激烈
乐观锁与悲观锁
  • 乐观锁:乐观估计别人不会来就该我的线程,如果被修改了,再重新来一次
  • 悲观锁:悲观估计别人会来修改我的线程,给线程上锁,解锁后别人才能操作
原子操作类
  • 在这里插入图片描述

synchronized优化

  • 每个对象都有对象头,包括指向class文件的指针和Mark Word
  • Mark Word平时储存对象的哈希码,分代年龄(新生代,老年代)
  • 加锁时,这些信息就根据情况被替换为标记位,线程锁记录指针,重量级锁指针,线程ID等内容
轻量级锁
  • 多线程的访问时间是交错的
  • 第二个线程进入时会提醒第一个线程,第一个线程就把锁升级为重量级锁
  • 每个线程的栈帧中都会有锁记录的结构,内部储存锁定对象的Mark Word,而原本的Mark Word存储锁的地址,解锁的时候再交换回去
锁膨胀
  • 线程2尝试加轻量级锁,发现CAS无法操作成功,是因为线程1已经为它加上了轻量级锁,这时候进行锁膨胀,将线程1轻量级锁变为重量级锁,线程2进入阻塞
  • 此时Mark Word的数据变成重量锁的指针,线程一解锁的时候就会唤醒阻塞中的线程争夺锁
重量级锁
  • 自旋优化,线程二加锁失败不会马上阻塞,先不停,不断自选重试,看那个锁有没有被放开,如果放开了线程2就直接加锁
  • 自旋多次失败,进入阻塞
  • 自适应,动态的,会占用CPU,需要多核CPU
偏向锁
  • 偏向锁在锁重铸的时候不会重新加锁,而轻量级锁会重新加锁
  • 对象头里存在的是线程id,如果发现这个id就是自己,就不会重新加锁
  • 如果有其它锁竞争就会将偏向锁升级为轻量级锁,会触发STW
  • 访问hashcode,因为hashcode被交换到栈帧里了,所以需要撤销偏向锁
  • 可以重新偏向另一个线程
  • 批量进行,以类为单位
  • 如果改变次数太多,自动禁用偏向,改为不可偏向锁
其他锁优化
  • synchronized代码块尽可能短,不要包裹太多
  • 减少锁的力度,将一个锁拆分成多个锁
  • 锁粗化,多个循环进入同步块不如同步块内多次循环
  • 锁消除,如果某个局部变量不会逃逸,就忽略锁
  • 读写分离,复制到一个新数组再读
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值