类加载

类加载是将外部.class 文件加载到jvm 内存中变为Class,其中流程包括:加载、链接(验证、准备、解析)、初始化、使用、卸载
加载
加载是在程序运行期间加载,类加载没有固定的时期
链接
- 验证Class 文件是否符合虚拟机要求
- 准备是为静态变量分配内存阶段
初始化
初始化时机,有明确的规定,
- new、getstatic、setstatic、invokestatic 没有初始化,则进行初始化
- java.lang.reflect 执行反射时
- 初始化子类,父类没有初始化,则先初始化父类
- 虚拟机初始化main 主类
类加载器
- 启动加载器(bootStrap classLoader)负责加载java_home/lib 下的类库,自定义类加载器时,返回null 默认走bootStrap classLoader
- 扩展类加载器(extension Classloader)java_home\lib/ext 中的类库
- 应用程序加载器(Application ClassLoader)负责加载用户路径上的指定类库
双亲委派
- 比较两个类是否相同的前提是,类加载器相同,否则即使同一个class 被不同类加载器加载,也不不相同(equal(),instanceof,isAssgionFrom())
- 双亲委派原则是:类加载器,优先调用父类的类加载器,父类无法加载才会子类加载
- 首次遭到破坏的双亲委派结构,是JNDI,可以让 运行时栈帧结构
- 局部变量表栈帧是存放方法变量及局部变量,主要包括:int、long(64位)、short、char、byte、float(64位)、reference(间接存放地址引用,直接存放存放对象信息)
- 局部变量表中 存放数据的基本单位是slot 32位,所以存放long和float 需要两个连续的slot,假如是64位系统首位填充
早起编译器优化(前端编译器)
早起编译时将java文件转化为虚拟机可以解析的class文件,主要分三个步骤(JavaCompiler类),JIT俗称后端编译器或及时编译器:
- 解析与填充符号过程,解析包括词法、语法分析
- 插入式注解处理器的注解处理过程
- 语义分析与字节码生成
Java语法糖
泛型与泛型擦除,java与c 的反应不太一样,java底层还是强转,C 则是不同的类型
java自动拆箱、装箱、遍历循环(foreach循环)
- java自动拆箱将基础对象类型 Integer、Long类型转化为对应的基础类型int long
- java装箱和拆箱相反,但是不能讲long转为Integer,只能从低位转向高位
- foreach 是一种新的语法糖,底层其实是for 循环+ iterator 迭代器,for循环和foreach 和 iterator 性能,iterator == foreach >= for
- 装拆箱的一些陷阱见如图
- 条件编译,会把不成立的条件在编译器删除,还有如在jdk1.7之后 “s”+“y”+“z”编译后会成功StringBuder.append(s) StringBuilder builder = StringBuilder.append(s);
StringBuilder builder = StringBuilder.append(y);
StringBuilder builder = StringBuilder.append(z); - 还有switch、try resource、枚举类、内部类都是语法糖的实现
晚期编译器(后端编译器)
- 解释器(interpreter),及时的将class文件转化为和识别的机器码
- JIT(Just In timer compiler) 及时编译器,主要是优化热点代码
- 解释器和编译器默认是混合模式(1.7默认是该模式),及时编译器分两类:C1(client),C2(server),C2 是比较激进的编译器。混合编译 解释器–>c1–>c
- JIT触发条件
- 方法被频繁调用,标准JIT及时编译
- 循环体触发JIT编译,OSR(On Stack Replacement) 栈帧替换
- 热点代码检测:
- 基于采样热点检测,某个时间点的快照,简单、高效
- 基于计数器热点检测,需要维护计数器,精准、严瑾(hotSpot 缺省值)
— 计数器分类 :方法计数器和回边计数器,方法计数器主要用于方法频繁调用,回边计数器用于循环
- 无论是否达到阈值,当次仍是interperter执行,client Compiler是简单高效的编译器,主要关注点是局部优化,自己码–>HIR–>方法内联、常量传播等、空置检查消除、范围检查消除–>LTR–>寄存器分配–>本地机器码
- serverCompiler 是充分优化过的高级编译器,无代码消除、循环展开、循环表达式外提、消除公共子表达式、常量穿欧巴、重排序、检查消除、空值检查消除、方法内联、逃逸分析。比较经典的是:
- 公共子表达式消除
- 数组范围检查技术
- 最重要优化技术:方法内联,首先要说下方法执行:进入方法调用时需要创建新的栈帧,创建新栈帧需要申请资源存储-本地变量和参数,执行该方法,方法返回的时候,本地方法和参数会被销毁,栈顶被移除。方法内联,将两个方法合并,不会去申请栈帧资源
- 最前沿的优化技术:逃逸分析:简单的说对象是否被其他方法或者对象引用,如果不存在,则可以作为其他(栈上替换-减少GC压力、同步消除-无需加锁,标量替换-将类中的变量拆分到栈中)优化的依据
高并发
主内存和工作内存
- 主内存相当于堆空间,工作内存指的是栈中的内存空间,
- 内存间相互操作,内存从主内存copy到工作内存
- 原子操作
- lock(锁定) 作用于主内存,把一个变量标记为线程独占状态
- unlock(解锁)于lock相反
- read(读取) 作用于主内存,将主内存数据读取到工作内存
- load(载入) 将 read 读取到的数据,放入 工作内存的副本中
- use (使用)将工作内存中的变量传递给执行引擎
- assgin (赋值)作用于工作内存,将执行引擎计算后的值,赋值给工作内存
- store(保存)作用于工作内存,把工作内存的值传递到主内存中
- write(写入)将store 的值写入工作内存中
volatile
- volatile 保证了数据的可见性,没有保证原子性,所以需要同时保证原子性,才可以保证线程安全
- volatile 去除了代码重排序优化,保证代码顺序执行,所以变相保证了有序性
- 针对long和double 64位的情况下, 没有被volatile 定义的long和double load,write,read,store 不具有原子性,不过出现的概率很小,可以忽略
- 原子性、可见性、有序性
- 先行发生原则(判断线程是否安全)
- 程序次序原则:保证程序执行顺序,有序性
- 管城锁定规则:锁先后顺序
- volatile变量规则:对一个volatile变量的写操作优先发生于后面变量操作
- 线程启动规则:Thread start 方法先行发生于此线程的每一个动作
- 线程终止规则:线程所有操作先行于线程终止 操作
线程的实现
- 内核实现(Mutil-Threads Kernel),内核线程(Kernel-Level Thread,KLT),耗费资源,一个轻量级线程(Light Weight Process -LWP)需要一个内核线程支持。
- 用户级线程实现(User Thread -UT) 快速、低消耗 进程与用户线程关系: 1:N,实现难度大 (jdk1.2 之后放弃)
- 用户级线程和轻量级进程混合实现
- java 的实现,需要依据操作系统而定,sun JDK在 windows和linux线程实现,一个java Thread 对应一个LWP
java 线程调度
- 协同是调度(cooperative Threads-Scheduling) 占用cpu资源的线程执行完毕后,才把位置让出,这种情况容易造成阻塞浪费资源
- 抢占式调度(preemptive Threads-Scheduling)线程的执行时间由cpu分配时间,可以解决线程阻塞浪费资源,但是线程间的切换也会浪费资源
- java线程调度方式是抢占式+优先级的方式,优先级高的多分配时间,java 语言设置了10个优先级会映射到操作系统的线程优先级
线程的状态及转换
- 新建(New):创建后未启动
- 运行(Runable):有可能正在执行,也有可能正在等待CPU为它分配执行时间
- 无限期等待(Waiting):这种状态不会被分配CPU执行时间,要等待被其他线程显示唤醒,以下方法会让线程陷入无限期的等待状态。Object.wait()、Thread.join()、LockSupport.park()
- 限期等待(Timed Waiting):这种状态不会被分配CPU执行时间,不过无需等待被其他线程显示地唤醒,在一定时间之后他们会由系统自动唤醒,以下方法会让线程进入限期等待状态:Thread.sleep(xxx)、Object.wait(xxx)、Thread.join(xxx)、LockSupport.partNanos()、LockSupport.parkUntil()
- 阻塞(Blocked):线程被阻塞了,阻塞状态和等待状态的区别是:在等待着获取一个排它锁(Synchronized),而等待状态则是在等待一段时间或者唤醒的动作。
- 结束(Terminated):
线程安全及锁优化
java语言中的线程安全
- 不可变,不可变的对象一定是线程安全的,最简单的方式是定义为final,String是一个典型的final 对象,他的subString 和replace 都不会影响原来的值
- 绝对线程安全 java 中的大多数都不是绝对线程安全,例如vector(相对线程安全) 的 get 和 remove 都是synchronize,remove 同时去get 会出现异常
- 相对线程安全 对象单独的操作是线程安全的,对于一些特定顺序的连续调用,不能保证正确性(Vector,HashTable,Collections)
- 线程兼容 指对象本身不是线程安全的
- 线程对立 即使使用同步锁,都不能保证线程的安全性,两个对象同时调用Thread的suspend()和resume() 方法,会造成死锁。
线程安全的实现方法
- 互斥同步(悲观锁) 对象同时被多个线程访问,保证只有一个线程同时被访问,Java中有synchronize 和 reentrantLock (lock 和unlock 需要搭配try/finally使用),reentrantLock 可以实现公平锁(按顺序获取锁)、可中断锁(设置超时时间)、锁绑定条件(绑定多个Condition),jdk1.5 性能对比,reentrantLock 性能较稳定,不过jdk1.6之后 synchronize 已经和reentrantLock性能基本一致了。主要锁了优化
- 非阻塞式同步(乐观锁) 乐观的尝试的策略,出现冲突,则采用其他补偿措施(重试),常用的类型指令有:
- 测试并设置(Test and Set)
- 获取并增加(Fetch-and-increment)
- 交换(Swap)
- 比较并交换(Compare and Swap-CAS) 会出现ABA 问题,就是对比后,其实被其他数据修改过,但是值没有变,可以通过增加时间撮或者版本的方式
- 条件存储(Store Condition)
- 无同步方案 数据不同享、可重入代码、线程本地存储都可以实现线程安全
锁优化
自旋锁和自适应自选
- 在所等待的时候,不进行线程的切换,而是去循环获取锁,这样可以解决线程挂起和恢复线程消耗的资源(适合阻塞时间短),jdk.16默认开启 默认是10次
- 自适应的自旋锁,会根据历史自旋锁的时间进行优化,假如历史情况不佳,会不进行自旋
消除锁
- 消除锁是在JIT 编译时,针对同步代码,发现没有共享数据,默认会删除同步锁。消除锁的数据支持是逃逸分析
- 堆上所有数据都不会逃逸被其他线程访问,那就可以当做栈上数据对待
- 举个例子: “x” + “y” +”z” 频繁执行会被JIT 优化为StringBuffer.append(x).append(y).append(z)
锁粗化
1.StringBuild.append(x).append(y).append(z) 会进行三次锁操作,JIT 编译后会变为一个锁
轻量级锁
- jdk1.6 之后增加了新型锁机制,轻量级锁,轻量级锁不是代替重量级锁 而是减少获取锁消耗的资源
- 对象头信息中会保留一个轻量级锁标志,假如需要获取对象锁的话,通过CAS 方式来获取锁。存在竞争情况,该性能会弱于重量级锁
- 轻量锁操作流程,进入同步代码块中,在对象头Mark Work 信息中,判断是否已被锁定,假如未被锁定,拷贝锁信息到栈帧中,获取轻量级锁,并轻量级锁设置为01,假如更新操作失败,则判断当前线程的栈帧是否已拥有对象的锁。拥有继续执行, 否则执行重量级锁
偏向锁
在无竞争的情况下,会删除锁及CAS轻量级锁,进一步优化锁,锁竞争比较激烈的情况下,偏向锁是多余的。jdk1.6之后默认是开启