什么是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).toStringString 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代码块尽可能短,不要包裹太多
  • 减少锁的力度,将一个锁拆分成多个锁
  • 锁粗化,多个循环进入同步块不如同步块内多次循环
  • 锁消除,如果某个局部变量不会逃逸,就忽略锁
  • 读写分离,复制到一个新数组再读

jvm虚拟机学习笔记相关推荐

  1. Java虚拟机(JVM)学习笔记(不定时更新)

    Java虚拟机(JVM)学习笔记 不少组织都曾开发过Java虚拟机: SUN公司曾经使用过3个虚拟机,Classic.Exact VM.Hotspot.     其中Hotspot虚拟机沿用至今,并已 ...

  2. Java 虚拟机学习笔记 | 类加载过程和对象的创建流程

    前言 创建对象是 Java 语言绕不开的话题,那么对象是如何创建出来的呢?我们今天就来聊一聊.对象创建第一步就是检查类是否加载,而类的加载又牵扯到类的加载过程.如果单说对象的创建而绕开类的加载过程,感 ...

  3. JVM最佳学习笔记---总览

    2019独角兽企业重金招聘Python工程师标准>>> 本笔记参照了周志明<深入理解Java虚拟机:JVM高级特性与最佳实践>第三版,读完之后受益匪浅,让我对Java虚拟 ...

  4. 深入理解JAVA虚拟机学习笔记(一)JVM内存模型

    摘要:   上周末搬家后,家里的宽带一直没弄好,跟电信客服反映了N遍了终于约了个师傅明天早上来迁移宽带,可以结束一个多星期没网的痛苦日子了.这段时间也是各种忙,都一个星期没更新博客了,再不写之前那种状 ...

  5. Java虚拟机学习笔记(一)—Java虚拟机概述

    一:编程语言兼容底层系统的方式大概分为两种 1.通过编译器实现兼容 例如C.C++等编程语言,既能运行与Linux系统,也能运行与Windows系统:既能运行于x86平台,也能运行于AMD平台.这种能 ...

  6. 狂神。JVM入门学习笔记。

    JVM学习 JVM常见面试题: 请你谈谈你对jvm的理解?Java8虚拟机和之前的变化更新? 什么是OOM?什么是栈溢出StackOverFlowError?怎么分析? jvm的常见调优参数有哪些? ...

  7. java outofmemory_深入理解JAVA虚拟机学习笔记3——OutOfMemoryError异常

    开门见山. 为了方便制造溢出,将JAVA堆的大小调整为10M. 本机用的是IntelliJ IDEA作为开发工具,进入到IDEA的安装目录,如D:\tools\IntelliJ IDEA 2017.1 ...

  8. JVM虚拟机学习 - JVM类加载,JVM内存模型,JVM性能分析工具

    JVM虚拟机 二 JVM类加载 类的生命周期 加载: ​ 加载class文件到二进制字节流,然后再将二进制字节流转化为方法区的运行时数据结构,生成一个对应的Class对象作为类各种数据的访问入口. 链 ...

  9. 深入理解Java虚拟机学习笔记-1.JVM内存模型

    JVM内存模型 1.内存模型结构图 名称 特征 作用 配置参数 异常 程序计数器 占用内存小,线程私有, 生命周期与线程相同 大致为字节码行号指示器 无 无 虚拟机栈 线程私有,生命周期与线程相同,使 ...

最新文章

  1. Python使用matplotlib进行3D可视化分析:3d柱状图、3d直方图、3d线框图、3d曲面图、3d翼面图(莫比乌斯环)
  2. 计算机课堂教学改革培训心得体会,教学改革培训心得体会(精选3篇)
  3. java如何写安卓接口文档_android、java制作sdk以及自动生成文档
  4. project 打印的时候上面的表格和下面的图例中间有个很大的空白,这块东西怎么能去掉呢?
  5. 通过CN3口直接控制台达伺服电机A2-M(三)
  6. 伍德里奇计量经济学第六章计算机答案,伍德里奇计量经济学导论计算机习题第六章第13题c_6.13...
  7. maven常用打包命令
  8. LeetCode. 15 - 三数之和
  9. 电脑插入耳机检测不到没反应怎么办?
  10. Inferior 1 (process 663) exited with code 0177
  11. 工业质检-缺陷检测数据集
  12. 物联网技术练习题(二)——多选题与简答题
  13. docker容器下载vim
  14. SOLIDWORKS motion如何进行运动仿真
  15. 2022年全球市场胸腰椎板系统总体规模、主要生产商、主要地区、产品和应用细分研究报告
  16. DXP多引脚封装绘制方式
  17. 电商系统哪部分会用到接口测试_电商网站测试点 还需要整理
  18. Pulmonary nodule detection in CT scans with equivariant CNNs
  19. 电气隔离 电源模块 升压/充电 实测案例 150V 30W 带四个220UF电解电容并联 300ms
  20. Linux系统下安装 pycharm2022社区版 步骤记录

热门文章

  1. 2021年金属非金属矿井通风免费试题及金属非金属矿井通风考试总结
  2. LCD(一) TFT液晶时序图
  3. mysql事务 mysql事务回滚 MySQL事务死锁 如何解除死锁 资金出入账
  4. 大众点评CAT开源监控系统剖析
  5. linux系统日志怎么退出,linux系统日志的清除
  6. css改变鼠标图片大小,CSS实现鼠标经过图片上图片等比缩放效果(代码实例)
  7. win10系统还原被组策略关闭怎么解决
  8. 语法体系:揭秘同位语从句day9
  9. 视频千倍压缩背后的技术原理之环路滤波
  10. 请说说你对互联网行业的理解。