深入理解java虚拟机笔记(下)

类加载

类加载是将外部.class 文件加载到jvm 内存中变为Class,其中流程包括:加载、链接(验证、准备、解析)、初始化、使用、卸载

加载

加载是在程序运行期间加载,类加载没有固定的时期

链接

  1. 验证Class 文件是否符合虚拟机要求
  2. 准备是为静态变量分配内存阶段

初始化

初始化时机,有明确的规定,

  1. new、getstatic、setstatic、invokestatic 没有初始化,则进行初始化
  2. java.lang.reflect 执行反射时
  3. 初始化子类,父类没有初始化,则先初始化父类
  4. 虚拟机初始化main 主类

类加载器

  1. 启动加载器(bootStrap classLoader)负责加载java_home/lib 下的类库,自定义类加载器时,返回null 默认走bootStrap classLoader
  2. 扩展类加载器(extension Classloader)java_home\lib/ext 中的类库
  3. 应用程序加载器(Application ClassLoader)负责加载用户路径上的指定类库

双亲委派

  1. 比较两个类是否相同的前提是,类加载器相同,否则即使同一个class 被不同类加载器加载,也不不相同(equal(),instanceof,isAssgionFrom())
  2. 双亲委派原则是:类加载器,优先调用父类的类加载器,父类无法加载才会子类加载
  3. 首次遭到破坏的双亲委派结构,是JNDI,可以让 运行时栈帧结构
  4. 局部变量表栈帧是存放方法变量及局部变量,主要包括:int、long(64位)、short、char、byte、float(64位)、reference(间接存放地址引用,直接存放存放对象信息)
  5. 局部变量表中 存放数据的基本单位是slot 32位,所以存放long和float 需要两个连续的slot,假如是64位系统首位填充

早起编译器优化(前端编译器)

早起编译时将java文件转化为虚拟机可以解析的class文件,主要分三个步骤(JavaCompiler类),JIT俗称后端编译器或及时编译器:

  1. 解析与填充符号过程,解析包括词法、语法分析
  2. 插入式注解处理器的注解处理过程
  3. 语义分析与字节码生成

Java语法糖

泛型与泛型擦除,java与c 的反应不太一样,java底层还是强转,C 则是不同的类型

java自动拆箱、装箱、遍历循环(foreach循环)

  1. java自动拆箱将基础对象类型 Integer、Long类型转化为对应的基础类型int long
  2. java装箱和拆箱相反,但是不能讲long转为Integer,只能从低位转向高位
  3. foreach 是一种新的语法糖,底层其实是for 循环+ iterator 迭代器,for循环和foreach 和 iterator 性能,iterator == foreach >= for
  4. 装拆箱的一些陷阱见如图
  5. 条件编译,会把不成立的条件在编译器删除,还有如在jdk1.7之后 “s”+“y”+“z”编译后会成功StringBuder.append(s) StringBuilder builder = StringBuilder.append(s);
    StringBuilder builder = StringBuilder.append(y);
    StringBuilder builder = StringBuilder.append(z);
  6. 还有switch、try resource、枚举类、内部类都是语法糖的实现

晚期编译器(后端编译器)

  1. 解释器(interpreter),及时的将class文件转化为和识别的机器码
  2. JIT(Just In timer compiler) 及时编译器,主要是优化热点代码
  3. 解释器和编译器默认是混合模式(1.7默认是该模式),及时编译器分两类:C1(client),C2(server),C2 是比较激进的编译器。混合编译 解释器–>c1–>c
  4. JIT触发条件
  • 方法被频繁调用,标准JIT及时编译
  • 循环体触发JIT编译,OSR(On Stack Replacement) 栈帧替换
  1. 热点代码检测:
  • 基于采样热点检测,某个时间点的快照,简单、高效
  • 基于计数器热点检测,需要维护计数器,精准、严瑾(hotSpot 缺省值)
    — 计数器分类 :方法计数器和回边计数器,方法计数器主要用于方法频繁调用,回边计数器用于循环

  1. 无论是否达到阈值,当次仍是interperter执行,client Compiler是简单高效的编译器,主要关注点是局部优化,自己码–>HIR–>方法内联、常量传播等、空置检查消除、范围检查消除–>LTR–>寄存器分配–>本地机器码
  2. serverCompiler 是充分优化过的高级编译器,无代码消除、循环展开、循环表达式外提、消除公共子表达式、常量穿欧巴、重排序、检查消除、空值检查消除、方法内联、逃逸分析。比较经典的是:
  • 公共子表达式消除
  • 数组范围检查技术
  • 最重要优化技术:方法内联,首先要说下方法执行:进入方法调用时需要创建新的栈帧,创建新栈帧需要申请资源存储-本地变量和参数,执行该方法,方法返回的时候,本地方法和参数会被销毁,栈顶被移除。方法内联,将两个方法合并,不会去申请栈帧资源
  • 最前沿的优化技术:逃逸分析:简单的说对象是否被其他方法或者对象引用,如果不存在,则可以作为其他(栈上替换-减少GC压力、同步消除-无需加锁,标量替换-将类中的变量拆分到栈中)优化的依据

高并发

主内存和工作内存

  1. 主内存相当于堆空间,工作内存指的是栈中的内存空间,
  2. 内存间相互操作,内存从主内存copy到工作内存
  3. 原子操作
  • lock(锁定) 作用于主内存,把一个变量标记为线程独占状态
  • unlock(解锁)于lock相反
  • read(读取) 作用于主内存,将主内存数据读取到工作内存
  • load(载入) 将 read 读取到的数据,放入 工作内存的副本中
  • use (使用)将工作内存中的变量传递给执行引擎
  • assgin (赋值)作用于工作内存,将执行引擎计算后的值,赋值给工作内存
  • store(保存)作用于工作内存,把工作内存的值传递到主内存中
  • write(写入)将store 的值写入工作内存中

volatile

  1. volatile 保证了数据的可见性,没有保证原子性,所以需要同时保证原子性,才可以保证线程安全
  2. volatile 去除了代码重排序优化,保证代码顺序执行,所以变相保证了有序性
  3. 针对long和double 64位的情况下, 没有被volatile 定义的long和double load,write,read,store 不具有原子性,不过出现的概率很小,可以忽略
  • 原子性、可见性、有序性
  • 先行发生原则(判断线程是否安全)
  1. 程序次序原则:保证程序执行顺序,有序性
  2. 管城锁定规则:锁先后顺序
  3. volatile变量规则:对一个volatile变量的写操作优先发生于后面变量操作
  4. 线程启动规则:Thread start 方法先行发生于此线程的每一个动作
  5. 线程终止规则:线程所有操作先行于线程终止 操作

线程的实现

  1. 内核实现(Mutil-Threads Kernel),内核线程(Kernel-Level Thread,KLT),耗费资源,一个轻量级线程(Light Weight Process -LWP)需要一个内核线程支持。
  2. 用户级线程实现(User Thread -UT) 快速、低消耗 进程与用户线程关系: 1:N,实现难度大 (jdk1.2 之后放弃)
  3. 用户级线程和轻量级进程混合实现
  4. java 的实现,需要依据操作系统而定,sun JDK在 windows和linux线程实现,一个java Thread 对应一个LWP

java 线程调度

  1. 协同是调度(cooperative Threads-Scheduling) 占用cpu资源的线程执行完毕后,才把位置让出,这种情况容易造成阻塞浪费资源
  2. 抢占式调度(preemptive Threads-Scheduling)线程的执行时间由cpu分配时间,可以解决线程阻塞浪费资源,但是线程间的切换也会浪费资源
  3. java线程调度方式是抢占式+优先级的方式,优先级高的多分配时间,java 语言设置了10个优先级会映射到操作系统的线程优先级

线程的状态及转换

  1. 新建(New):创建后未启动
  2. 运行(Runable):有可能正在执行,也有可能正在等待CPU为它分配执行时间
  3. 无限期等待(Waiting):这种状态不会被分配CPU执行时间,要等待被其他线程显示唤醒,以下方法会让线程陷入无限期的等待状态。Object.wait()、Thread.join()、LockSupport.park()
  4. 限期等待(Timed Waiting):这种状态不会被分配CPU执行时间,不过无需等待被其他线程显示地唤醒,在一定时间之后他们会由系统自动唤醒,以下方法会让线程进入限期等待状态:Thread.sleep(xxx)、Object.wait(xxx)、Thread.join(xxx)、LockSupport.partNanos()、LockSupport.parkUntil()
  5. 阻塞(Blocked):线程被阻塞了,阻塞状态和等待状态的区别是:在等待着获取一个排它锁(Synchronized),而等待状态则是在等待一段时间或者唤醒的动作。
  6. 结束(Terminated):

线程安全及锁优化

java语言中的线程安全

  1. 不可变,不可变的对象一定是线程安全的,最简单的方式是定义为final,String是一个典型的final 对象,他的subString 和replace 都不会影响原来的值
  2. 绝对线程安全 java 中的大多数都不是绝对线程安全,例如vector(相对线程安全) 的 get 和 remove 都是synchronize,remove 同时去get 会出现异常
  3. 相对线程安全 对象单独的操作是线程安全的,对于一些特定顺序的连续调用,不能保证正确性(Vector,HashTable,Collections)
  4. 线程兼容 指对象本身不是线程安全的
  5. 线程对立 即使使用同步锁,都不能保证线程的安全性,两个对象同时调用Thread的suspend()和resume() 方法,会造成死锁。

线程安全的实现方法

  1. 互斥同步(悲观锁) 对象同时被多个线程访问,保证只有一个线程同时被访问,Java中有synchronize 和 reentrantLock (lock 和unlock 需要搭配try/finally使用),reentrantLock 可以实现公平锁(按顺序获取锁)、可中断锁(设置超时时间)、锁绑定条件(绑定多个Condition),jdk1.5 性能对比,reentrantLock 性能较稳定,不过jdk1.6之后 synchronize 已经和reentrantLock性能基本一致了。主要锁了优化
  2. 非阻塞式同步(乐观锁) 乐观的尝试的策略,出现冲突,则采用其他补偿措施(重试),常用的类型指令有:
  • 测试并设置(Test and Set)
  • 获取并增加(Fetch-and-increment)
  • 交换(Swap)
  • 比较并交换(Compare and Swap-CAS) 会出现ABA 问题,就是对比后,其实被其他数据修改过,但是值没有变,可以通过增加时间撮或者版本的方式
  • 条件存储(Store Condition)
  1. 无同步方案 数据不同享、可重入代码、线程本地存储都可以实现线程安全

锁优化

自旋锁和自适应自选

  1. 在所等待的时候,不进行线程的切换,而是去循环获取锁,这样可以解决线程挂起和恢复线程消耗的资源(适合阻塞时间短),jdk.16默认开启 默认是10次
  2. 自适应的自旋锁,会根据历史自旋锁的时间进行优化,假如历史情况不佳,会不进行自旋

消除锁

  1. 消除锁是在JIT 编译时,针对同步代码,发现没有共享数据,默认会删除同步锁。消除锁的数据支持是逃逸分析
  2. 堆上所有数据都不会逃逸被其他线程访问,那就可以当做栈上数据对待
  3. 举个例子: “x” + “y” +”z” 频繁执行会被JIT 优化为StringBuffer.append(x).append(y).append(z)

锁粗化

1.StringBuild.append(x).append(y).append(z) 会进行三次锁操作,JIT 编译后会变为一个锁

轻量级锁

  1. jdk1.6 之后增加了新型锁机制,轻量级锁,轻量级锁不是代替重量级锁 而是减少获取锁消耗的资源
  2. 对象头信息中会保留一个轻量级锁标志,假如需要获取对象锁的话,通过CAS 方式来获取锁。存在竞争情况,该性能会弱于重量级锁
  3. 轻量锁操作流程,进入同步代码块中,在对象头Mark Work 信息中,判断是否已被锁定,假如未被锁定,拷贝锁信息到栈帧中,获取轻量级锁,并轻量级锁设置为01,假如更新操作失败,则判断当前线程的栈帧是否已拥有对象的锁。拥有继续执行, 否则执行重量级锁

偏向锁

在无竞争的情况下,会删除锁及CAS轻量级锁,进一步优化锁,锁竞争比较激烈的情况下,偏向锁是多余的。jdk1.6之后默认是开启