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).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方法时,查找类的规则
- 检查该类是否已经被加载(在本类加载器中)
- 如果没有,调用上级类加载器loadClass方法(递归)
- 如果没有上级(Extension ClassLoader),则委派Bootstrap
- 如果都没有,本类加载器调用一个findClass方法(每个类加载器自己扩展)来加载
- 如果找到就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虚拟机学习笔记相关推荐
- Java虚拟机(JVM)学习笔记(不定时更新)
Java虚拟机(JVM)学习笔记 不少组织都曾开发过Java虚拟机: SUN公司曾经使用过3个虚拟机,Classic.Exact VM.Hotspot. 其中Hotspot虚拟机沿用至今,并已 ...
- Java 虚拟机学习笔记 | 类加载过程和对象的创建流程
前言 创建对象是 Java 语言绕不开的话题,那么对象是如何创建出来的呢?我们今天就来聊一聊.对象创建第一步就是检查类是否加载,而类的加载又牵扯到类的加载过程.如果单说对象的创建而绕开类的加载过程,感 ...
- JVM最佳学习笔记---总览
2019独角兽企业重金招聘Python工程师标准>>> 本笔记参照了周志明<深入理解Java虚拟机:JVM高级特性与最佳实践>第三版,读完之后受益匪浅,让我对Java虚拟 ...
- 深入理解JAVA虚拟机学习笔记(一)JVM内存模型
摘要: 上周末搬家后,家里的宽带一直没弄好,跟电信客服反映了N遍了终于约了个师傅明天早上来迁移宽带,可以结束一个多星期没网的痛苦日子了.这段时间也是各种忙,都一个星期没更新博客了,再不写之前那种状 ...
- Java虚拟机学习笔记(一)—Java虚拟机概述
一:编程语言兼容底层系统的方式大概分为两种 1.通过编译器实现兼容 例如C.C++等编程语言,既能运行与Linux系统,也能运行与Windows系统:既能运行于x86平台,也能运行于AMD平台.这种能 ...
- 狂神。JVM入门学习笔记。
JVM学习 JVM常见面试题: 请你谈谈你对jvm的理解?Java8虚拟机和之前的变化更新? 什么是OOM?什么是栈溢出StackOverFlowError?怎么分析? jvm的常见调优参数有哪些? ...
- java outofmemory_深入理解JAVA虚拟机学习笔记3——OutOfMemoryError异常
开门见山. 为了方便制造溢出,将JAVA堆的大小调整为10M. 本机用的是IntelliJ IDEA作为开发工具,进入到IDEA的安装目录,如D:\tools\IntelliJ IDEA 2017.1 ...
- JVM虚拟机学习 - JVM类加载,JVM内存模型,JVM性能分析工具
JVM虚拟机 二 JVM类加载 类的生命周期 加载: 加载class文件到二进制字节流,然后再将二进制字节流转化为方法区的运行时数据结构,生成一个对应的Class对象作为类各种数据的访问入口. 链 ...
- 深入理解Java虚拟机学习笔记-1.JVM内存模型
JVM内存模型 1.内存模型结构图 名称 特征 作用 配置参数 异常 程序计数器 占用内存小,线程私有, 生命周期与线程相同 大致为字节码行号指示器 无 无 虚拟机栈 线程私有,生命周期与线程相同,使 ...
最新文章
- Python使用matplotlib进行3D可视化分析:3d柱状图、3d直方图、3d线框图、3d曲面图、3d翼面图(莫比乌斯环)
- 计算机课堂教学改革培训心得体会,教学改革培训心得体会(精选3篇)
- java如何写安卓接口文档_android、java制作sdk以及自动生成文档
- project 打印的时候上面的表格和下面的图例中间有个很大的空白,这块东西怎么能去掉呢?
- 通过CN3口直接控制台达伺服电机A2-M(三)
- 伍德里奇计量经济学第六章计算机答案,伍德里奇计量经济学导论计算机习题第六章第13题c_6.13...
- maven常用打包命令
- LeetCode. 15 - 三数之和
- 电脑插入耳机检测不到没反应怎么办?
- Inferior 1 (process 663) exited with code 0177
- 工业质检-缺陷检测数据集
- 物联网技术练习题(二)——多选题与简答题
- docker容器下载vim
- SOLIDWORKS motion如何进行运动仿真
- 2022年全球市场胸腰椎板系统总体规模、主要生产商、主要地区、产品和应用细分研究报告
- DXP多引脚封装绘制方式
- 电商系统哪部分会用到接口测试_电商网站测试点 还需要整理
- Pulmonary nodule detection in CT scans with equivariant CNNs
- 电气隔离 电源模块 升压/充电 实测案例 150V 30W 带四个220UF电解电容并联 300ms
- Linux系统下安装 pycharm2022社区版 步骤记录