笔记
1.类加载
1.1什么是类加载
类加载是一种机制,总共分为五个步骤:加载、验证、准备、解析、初始化。
验证、准备、解析统称为连接
1.2类加载的过程
加载
- 根据类的全限定名获取此类的二进制字节流
- 根据字节流所代表的静态数据结构转化为方法区的运行时数据结构
- 在虚拟机堆中创建Class对象作为此类数据的访问入口
验证
验证阶段的工作量在虚拟机的类加载子系统中占了很大一部分。因为类的二进制字节流的来源多样,所以需要验证上是否符合当前虚拟机的规范
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
准备
- 为类变量在方法区分配内存
- 为类变量赋予零值
- final修饰的类变量赋值指定的值
解析
将常量池中的符号引用替换为直接引用的过程。
解析的时机:类加载阶段就执行,或者等待需要使用到符号引用时才执行
解析的目标:类或接口、字段、类的方法、接口的方法。对应常量池的CONSTANT_class_info、CONSTANT_fieldref_info、CONSTANT_methodref_info、CONSTANT_interfaceMethodref_info
- 类或接口的解析:在类D中符合引用N解析为一类或者接口C的直接引用包括3个步骤
- 非数组 类型,由D的类加载器加载类C根据代表N的全限定名,完成加载阶段
- 数组类型,先加载数据元素类型,接着由虚拟机生成一个代表次数据维度和元素的数组对象
- 虚拟机已经有类/接口C的对象的,进行符合验证,是否有访问权限。
- 字段解析
- 字段的简单名称和字段描述符相同则匹配
- 没有则接口中找
- 从上往下递归搜索父类是否包含该字段
- 还是没有则抛出异常
- 类方法和接口方法的解析与字段解析大同小异
初始化
初始化阶段就是执行类的clinit方法,此方法由编译期收集
- clinit方法由类的静态代码块和类变量的赋值语句组成
- clinit方法中的语句按类中定义的先后顺序执行
- 静态代码块不能访问定义在后面语句的类变量,只能赋值
- jvm虚拟机保证无需显式调用,父类的clinit先执行与子类。object方法最先执行
- jvm保证clinit方法执行同步加锁,正确执行一次
- 一个类可以没有clinit方法
- 接口中没有静态代码块,但是依旧可以有类变量赋值语句
1.3类加载的时机
类“初始化”(加载、验证、准备自然在此之前)的时机:有且仅包括以下四种情况
- 遇到new,getstatic,putstatic,invokestatic字节码指定时(这四种字节码常见的java代码为:new 一个对象、读取和设置类变量(常量除外,编译期已经放入常量池)、调用类的静态方法)
- 子类加载时,父类必须先加载
- main方法的类触发加载
- java.lang.reflect包下调用到了类
- methodhandler类调用到了类
- 类与接口的区别在于,这一条(子类加载时,父类必须先加载)父接口不需要完全加载
2.双亲委派模型
下面这种类的层次关系就称为类加载器的双亲委派模型
2.1类加载器
类加载负责根据类的全限定名获取类的二进制文件,在方法区分配运行时数据结构,在堆中生成class对象以供访问。
一个类相同需要他们的class文件相同并且加载class文件的类加载器也相同。相同的定义包括:equals、isAssignableFrom、isInstance和关键字instanceof
2.2 jvm类加载器类型
从jvm看只有启动类加载器和其他类加载器。
从java角度看包括以下几种:
- 启动类加载器(加载$java_home/lib目录下的类)
- 扩展类加载器(加载$java_home/lib/ext目录下的类)
- 系统类加载器(加载classpath下的类)
2.3工作流程
类加载收到加载类的请求时,首先委托由父类加载器去完成,每个层次的类加载器都是如此,因此所有类加载请求首先由启动类加载器执行,当父类加载器无法加载时,才有本层类加载器执行加载。
这种模式的好处是:可以保证java类的优先级层次关系,比如Object类放在rt.jar中,无论哪个类加载都是由启动类加载器完成加载。保证了object是同一个类。
3.volatile
3.1语义
保证线程对共享变量的修改对其他线程可见,线程读取共享变量时每次从主内存读取
3.2实现原理
- 对volatile修饰的共享变量每次从都从主内存读取,操作完立即写会主内存
- 使用
内存屏障
避免指令重排序 - 加入lock指令
4.synchronized
4.1语义
synchronized锁住的代码块,多线程是串行执行的。
4.2 实现原理
总的来说是jvm通过进入和退出monitor对象来实现方法同步和代码块的同步的。
- 通过在代码块前后分别加monitorenter和monitorexit指令实现代码块的同步
- 方法的同步在jvm中没有说明是怎么实现的
monitorenter是获取对象锁的指令,获取的对象锁位于对象头中
4.3锁升级过程
a. 无锁到偏向锁到轻量级锁
- 1号线程修改mark word cas更新对象的mark word,成功获取到锁
- 2号线程检查mark word中是否为自己的线程id,使用cas替换mark word,失败执行撤销偏向锁
- 如果一号线程处于不活跃状态,则置为无锁状态,否则等1号线程进入安全点后,判断是否还持有该锁,如果没有,则重新指定2号线程为偏向锁,如果持有则升级为偏向锁,将mark word的状态改为1号线程栈的指针
b.轻量级锁升级为重量级锁
- 1号线程继续持有锁,当2号线程自旋多次后还是无法将mark word的指针修改为自己栈帧上的,那么修改mark word的指针为互斥量,2号线程阻塞
- 1号线程释放锁后唤醒等待的线程
注意:锁升级后不能降级
5.Java对象头
5.1结构
长度 | 内容 | 说明 |
---|---|---|
32bit/64bit | mark word | hashcode和锁信息 |
32bit/64bit | class metadata address | 存储对象类型数据的指针 |
32bit/64bit | array length | 如果是数组,表示数组长度 |
5.2Mark Word数据结构
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashcode | 对象分代年龄 | 0 | 01 |
5.3Mark Word的状态变化
6.GC算法
6.1标记清除
分为标记和清除两个阶段。
先标记所有需要回收的对象,在标记完成后统一回收标记的对象。或者标记存活的对象,然后统一回收未被标记的对象。对象是否存活使用可达性分析算法。(tracing gc)
存在两个问题
- 性能问题:标记和清除都效率不高
- 空间问题:清除后存在较多内存碎片
6.2 复制算法
复制算法是标记清除的改进版本:将可用内存相同大小的两块,每次只使用一块,当一块快用完时,将存活对象复制到保留区域。
这有解决了清除阶段的效率和内存碎片问题。
Hotspot的jvm在新生代才有这种收集算法。分为Eden区和两个survivor区,每次只使用eden区和一个survivor区,空间利用率是90%
6.3 标记整理算法
常用于老年代的收集。
是标记清除算法的改进版本。标记阶段相同,在清理阶段:把存活的对象向一端移动,再清理存活对象边界外内存。
解决了内存碎片的问题,清理操作也是顺访问更高效。
6.4分代收集算法
根据对象的存活周期的不同将内存划分为几块。java堆划分为新生代和老年代,根据每个代的不同特点采用适当的收集算法。
新生代存活对象少,适合使用复制算法。
老年代没有空间担保,对象存活率高,适合标记清除和标记整理算法。
7.对象存活判断算法
7.1 引用计数
每当A被B引用一次 ,A的引用计数+1
当 引用次数达到0时,判定为死亡
存在问题:A,B两个对象互相引用,其他对象对 它们无引用 。
7.2 可达性分析
从gc roots触发,对gc roots的整个引用链进行图便利,标记所有遍历到的对象为存活。
gc roots是:
- 常量池的对象
- 活跃线程的堆栈中的局部变量表引用
- 使用分代收集时,另一代也称为了gc roots。 由于跨代引用假说,hotspot虚拟机使用remember set数据结构维护老年代对新生代存在引用的内存地址。 remember set的维护的地址会作为gc roots的一部分.hotspot使用卡表作为remember set的实现
8.GC类型
分为部分收集和整堆收集
部分收集:
- 新生代收集(minor gc/young gc):对新生代进行垃圾回收
- 老年代收集(major gc/old gc):对老年代进行垃圾收集。 major gc有时也指整堆收集
- 混合收集(mixed gc):对新生代收集以及部分老年代收集。此行为目前只有g1有
整堆收集:
- 整堆收集(full gc):对整个java堆和方法区(常量和类信息)进行收集
9 jvm内存区域划分
9.1 jdk7
- 堆
- 虚拟机方法栈
- 本地方法栈
- 程序计数器
- 方法区
9.2 jdk8
- 堆
- 虚拟机方法栈
- 本地方法栈
- 程序计数器
- 元空间(占用内存不算在Xmx参数 内)(有独立的垃圾回收器)
- 直接内存(占用内存不算在Xmx参数 内)(有独立的垃圾回收器)
10.垃圾收集器实现细节
10.1根节点
概念上讲:根节点包括方法区的常量,类变量和活跃线程的局部变量表
10.2 OopMap的数据结构
但是在现代Java应用中,方法区上至几百兆已经是非常常见的。想从几百兆内存中快速定位到gc roots的耗时依旧是无法接受的。
根节点的引用关系需要在某个时刻确定下来,所有收集器的实现都是通过STW来实现的。
通过OopMap的数据结构来记录这些GC ROOTS。在类加载完成后即可确定类的常量,记录在OopMap中。
JIT也会在特定的位置记录下栈里和寄存器哪些位置是引用
10.3 安全点
随时线程的执行OopMap中的数据会改变,那么需要统计某一刻的记录,需要暂停线程。
商用虚拟机是通过在耗时的指令前增加一条轮询汇编指令实现。
让所有线程停顿下来有两种实现思想:抢先式中断和主动式中断。
抢先式中断:强制停顿所有线程,让没有达到安全点的线程继续执行到安全点后挂起。
主动式中断:当垃圾收集需要中断线程时,设置标识,让线程主动轮询标识,自动挂起
10.4 安全区域
安全区域是执行某个代码片段不会引用关系变化。那么在线程执行此区域的时候是可以进行根节点枚举的,线程离开安全区域时会校验是否要求线程中断。
扩展了安全点
10.5 记忆集和卡表
是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构
用来缩减GC ROOTS的扫描范围
卡表是hotspot记忆集的实现。
卡表是一个字节数组。每个元素都对应着一个内存块,标识是否存在跨代指针。
使用512字节作为一个卡页,也就是卡表的每一项表示对应的512个字节是否存在跨代指针
CARD_TABLE[this address>>9]=0
10.6 写屏障
可以想到虚拟机会在改变对象引用关系的时候去维护卡表。
存在问题:如果是解释执行的字节码,虚拟机还有维护的办法,如果是JIT后的机器码,那么虚拟机是如何修改的呢。
hotspot使用写屏障来实现,写屏障可以看做是引用类型字段赋值的AOP切面,在对引用类型字段赋值时会产生一个环绕通知。一般GC收集器会使用写后屏障
卡表存在伪共享问题(Contended注解解决)。64个卡表元素组成一个64字节缓存行,当多线程对64字节代表的32kb内存地址修改指针时,会存在写回无效或者同步导致性能低下。提供-XX:UseCondCardMark参数,开启后每次先判断是否脏页再决定是否写。
10.7 并发的可达性分析
GC ROOTS确定的情况下就要进行可达性分析。理论上堆的空间越大可容量的对象越多,可达性分析的对象就越多。那么并发的可达性分析是减少STW收益最大的部分。
要理解为什么要暂停线程进行可达性分析才能解决STW的问题?
三色标记:
- 白色:未被收集器访问过的对象,最终为可回收对象
- 黑色:被收集器访问过的对象,并且所有引用类型已经扫描过了。存活对象
- 灰色:被收集器访问过的对象,但是引用类型还未扫描完全。存活对象
那么当收集线程和用户线程并发时,存在以下两个问题:
- 原本消亡的对象标记为存活(浮动垃圾)
- 原本存活的对象标记为消亡(不能接受,需要解决)
什么情况下存活的对象会被标记为消亡呢? 理论证明同时满足以下两个条件:
- 赋值器新增了黑色对象对白色对象的引用
- 复制器删除了灰色对象对白色对象的间接或直接引用
那么虚拟机通过破坏两个条件其中的一个出现了两种解决办法:
- 增量更新:记录黑色对象对白色对象的新引用。标记完成后,再次以黑色对象为GC roots进行可达性分析(暂停线程)cms使用方式
- 原始快照:记录灰色对象删除对白色对象的引用。以灰色对象作为GC roots再次进行可达性分析(G1使用)
11.垃圾收集器
hotspot虚拟机针对不同代有多种垃圾收集器。
新生代:serial、parnew、parallel scavenge
老年代:serial old、cmd、parallel old
全代:G1、ZGC
11.1 serial收集器
新生代收集器
单线程收集
收集期间stw
是jvm在client模式下默认选择
标记复制算法
11.2 parnew收集器
新生代垃圾收集器。
是serial收集器的多线程版本。
收集期间stw。
搭配cms使用
标记复制算法
11.3 parallel scavenge收集器
新生代垃圾收集器。
并行收集器
使用复制算法
具备gc自适应条件策略,设置MaxGCPauseMillis(停顿时间)参数或者GCTimeRatio参数(吞吐量)
11.4 serial old收集器
老年代收集器,client模式下默认
标记整理
单线程
11.5 parallel old收集器
老年代收集器
多线程标记整理
是parallel scavenge的老年代版本,搭配使用,适合在吞吐量care场景下
11.6 cms收集器
cms(concurrent mark sweep)作为老年代 收集器,只能搭配parnew和serial使用。
是获取最短停顿时间为目标的收集器
使用标记清除算法
运作过程分为四个步骤:
- 初始标记(STW):标记与GC ROOTS直接相关联的对象
- 并发标记:对直接相关联的对象使用三色标记进行并发可达性分析
- 重写标记(STW):使用增量更新解决黑色对象对白色对象新增的引用。
- 并发清除:直接清除死亡对象,不需要移动对象。可以与用户线程并发
存在问题:
- 无法处理并发标记和并发清除阶段产生的浮动垃圾。可能导致full gc
- 使用标记-清除算法,产生内存碎片。不得不通过full gc进行内存碎片整理(在G1之前移动对象是不能并发的)
11.7 G1收集器
概述
JDK9的默认垃圾收集器。JDK8为parallel scavenge+parallel old
开创了面向局部回收和基于region的内存布局形式
改进CMS的回收算法,使用复制算法,避免内存碎片。
可预测的停顿模型:在M毫秒的时间内,垃圾回收时间不超过N毫秒
回收范围
面向堆内存任何部分来组成回收集(collection set 简称 CSet),基于哪块内存存放垃圾多,回收收益大。这就是mixed gc模式
region
细分有四种region类型
- region的大小由-XX:G1HeapRegionSize设定,取值为1MB-32MB之间
- 超过半个region大小的大对象会存储到humongous区块
- region作为单次回收的最小单位
- g1会收集每个region回收价值,维护优先级列表,根据设置的回收目标(-XX:MaxGCPauseMills)优先回收价值高的。这种以region划分内存区域,以及具有优先级的区域回收方式是g1在有限时间内尽可能高效回收的保证
12.Java8时间类
// instant > localdatetime
LocalDateTime a = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime();
// localdatetime > instant
Instant b = LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant();
// date > instant
Instant instant = new Date().toInstant().minusMillis(1);
Date date = Date.from(instant);
// instant > date
LocalDateTime localDateTime = LocalDateTime.now(ZoneId.systemDefault()).minusHours(5);
Date from = Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
13.网络
13.1HTTP、HTTPS
https://blog.csdn.net/wengfuying5308/article/details/107851922
14.数据结构
14.1堆
- 使用数组存储二叉树,根节点放在数组首个,左节点的index是2 * 根 +1,右节点的index是2 * 根 + 2,根节点下标是 左或右节点的下标-1 /2
- 调整堆:从最后一个非叶子节点(下标:2/n -1)开始从右至左,从下至上的调整节点,父节点小于左右节点。
- 加入节点:判断是否小于根节点,小于就替换然后把根节点放到数据末尾,大于就放到数组末尾
- 删除节点:根节点与末位节点更换位置,再n-1。执行调整堆,返回最后一个节点。
14.2 B树
- 讨论b树需要先确定阶。M阶表是非叶子节点 最多能有m个子树
- 关键字和数据共同存在索引中。
- 关键字:所有节点的关键字最多m-1个,根节点最少可以是1个, 非根节点最少m/2个
- 子树:最多m颗子树
- 左子节点关键字小于根节点关键字,右子节点关键字大于根节点关键字。
- 分裂时,按中间关键字分割,左边独立成为中间关键字的左节点,右半边独立成为中间关键字的右子节点 。中间关键字放入父节点
- 关键字唯一
插入
插入后节点的关键字小于m直接完成。
插入后节点的关键字等于m此时需要分裂,按中间关键字(Math.ceil(m/2))分割,左边独立成为中间关键字的左节点,右半边独立成为中间关键字的右子节点 。中间关键字放入父节点
删除
14.3 B+树
- 讨论b+树需要先确定阶。M阶表是非叶子节点 最多能有m个子节点
- 非叶子节点只存储关键字,不存储数据 。分为索引节点和叶子节点
- 叶子节点包含所有关键字,所有叶子节点组成一条双向链表
- 关键字:所有节点的关键字最多m-1个,根节点最少可以是1个, 非根节点最少m/2个
- 子树:最多m颗子树
- 左子节点小于根节点,右子节点大于等于根节点
插入
1.插入后节点的关键字小于m直接完成。
2.插入后节点的关键字等于m此时需要分裂,按中间关键字(Math.ceil(m/2))分割,左边独立成为中间关键字的左节点,右半边+关键字独立成为中间关键字的右子节点 。中间关键字放入父节点
删除节点
B+树的删除只删除叶子节点,保留索引节点,因此有以下这种情况:索引存在但是记录不存在
15.排序算法
15.1 堆排序
- 第一步:执行调整堆的代码
- 第二步:把根节点与末位节点交换位置,并且n - 1;
- 第三步:执行调整堆的代码
- 第四步:二三步代码执行n边,堆排序完成
- 时间复杂度:o(nlogn)
- 空间复杂度:o(1)
15.2 快速排序
- 理论基础:确定一个基数,按基数把数组
- 分成两组,使得左边都小于基数,右边大于基数。再把左右也做相同处理
- 取lowIndex作为基数,左右指针开工,右指针先走,找到小于基数的暂停,左指针再走,找到大于基数的暂停。交换两m者。直至 左指针大于右指针
- 临界条件: low > high结束, left < right 才能交换。 low与left交换。子排序的范围是low left-1和left+1和hight
- 时间复杂度:o(nlogn)
- 空间复杂度:o(1)
15.3 归并排序
-
归并排序是分治的思想,先分解问题,再解决子问题
-
分:先二分数组,直至数组有序(数组只含有一个节点就是有序的)
-
治:合并两个有序的数组。
-
时间复杂度:O(n log n)
-
空间复杂度:o(n)
16. MySQL
16.1 InnoDB引擎
16.1.1 事务
- 原子性:事务中的全部SQL只会全部执行或者全部回滚
- 一致性:事务开始和结束,数据库的状态是从一个状态到另一个状态是一致的。
- 持久性:事务提交后无论宕机还是异常,修改结果都不会丢失
- 隔离性:事务之间是互不影响的,四种隔离级别,RU RC RR S
16.1.2 binlog
mysql的二进制文件,用于恢复和复制。二进制文件的存储有三种模式
- row:记录数据修改后的结果
- statement:记录写操作的sql语句
- mixed:对于不同的sql选择使用row或者statement来记录
使用row的复制模式,会增加一个的开销(网络和日志量),但是数据一致性好
使用mix/statement的模式,开销小于row,使用存储过程,或者主从表结构不一致时会有问题。
16.1.3 事务隔离级别
- 读未提交:会产生脏读问题。一个事务会读取到另一个事务未提交的数据,这个数据是可能被回滚的
- 读已提交:解决了脏读问题。会产生不可重复读的问题。一个事务会读取到另一个事务已提交的数据。通过MVCC解决脏读问题。oracle的默认级别
- 可重复读:解决了不可重复读的问题。会产生幻读的问题。一个事务第一次读取的数据和第二次读取的数据不一致。通过MVCC解决不可重读问题。mysql的默认级别
- 串行化:所有事务都是串行执行的,所有数据是最稳定的。但是性能也是最差的
16.1.4 MVCC多版本并发控制
实现了读写并发
innodb通过在每行增加两列来实现的。一列是保存了行的事务id,一列是保存了行的回滚指针。
a.可重复下的事务隔离级别
- select(快照读)
- 只查找行版本号小于等于当前事务的记录
- 可以读取到删除标识列大于自己的行,不能读取删除标识列小于等于自己的行
- insert
- 保存行的创建标识列为自己的事务id
- delete
- 对修改行的删除标识列更新为自己的事务id
- update
- 插入一条修改后的记录,行的创建列标识为自己的事务id
- 修改更新行的记录,行的删除标识为自己事务的id
purge thread会定期把无效的undo log回收再利用
简单的描述:
0时刻: A事务插入了一条记录,trx_id是 1
name | age | row_id | trx_id | rollback_pointer |
---|---|---|---|---|
mary | 18 | 1 | 1 | null |
1时刻:B事务修改age为20,trx_id是2
- 获取行的锁
- 记录到redo log
- copy 当前记录到undo log
- 修改当前的记录的值,并把trx_id修改的2,回滚指针指向回滚段的undo log记录
b.ReadView
结论:读已提交隔离级别下,每一次快照读都会创建ReadView,这就实现了读已提交的隔离级别
可重复读隔离级别下,每一次快照读都会沿用第一次快照读时生成的ReadView
什么是readview?
就是当前活跃事务id的集合
事务可以读取到什么版本的数据呢?
事务id要小于ReadView中最小的事务id,并且是所有版本中的最大值
事务提交和回滚时,undo log需要做什么修改?
由于更新时获取了排他锁,记录已经被修改,commit不需要做什么。回滚时只需要回复回滚指针指向记录的数据
16.1.3 redo log
- 事务中每条sql修改后的物理页都会放在redo log,来保证事务的持久性
16.1.4 锁
-
按锁的粒度分:支持表锁,行锁,范围锁和next-key lock
-
按锁定用途分为:读锁和写锁,意向读和意向写
使用了next-key lock解决了幻读问题。