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

/ java / 0 条评论 / 41浏览

类加载

类加载是将外部.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,可以让

运行时栈帧结构

栈帧结构

  1. 局部变量表栈帧是存放方法变量及局部变量,主要包括:int、long(64位)、short、char、byte、float(64位)、reference(间接存放地址引用,直接存放存放对象信息)
  2. 局部变量表中 存放数据的基本单位是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触发条件
  1. 热点代码检测:
  1. 无论是否达到阈值,当次仍是interperter执行,client Compiler是简单高效的编译器,主要关注点是局部优化,自己码-->HIR-->方法内联、常量传播等、空置检查消除、范围检查消除-->LTR-->寄存器分配-->本地机器码
  2. serverCompiler 是充分优化过的高级编译器,无代码消除、循环展开、循环表达式外提、消除公共子表达式、常量穿欧巴、重排序、检查消除、空值检查消除、方法内联、逃逸分析。比较经典的是:

高并发

主内存和工作内存

  1. 主内存相当于堆空间,工作内存指的是栈中的内存空间,
  2. 内存间相互操作,内存从主内存copy到工作内存
  3. 原子操作

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. 非阻塞式同步(乐观锁) 乐观的尝试的策略,出现冲突,则采用其他补偿措施(重试),常用的类型指令有:
  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之后默认是开启