1、JVM
1.1 定义
JVM是双亲委派机制,自上向下加载,启动类加载器、扩展类加载器、应用类加载器,在JDK1.8开始新增了自定义类加载器
启动类加载器:主要加载java核心库
扩展类加载器:加载扩展卡,如classpath中的jre
应用类加载器:加载程序所在目录
自定义类加载器:加载用户自定义的文件
1.2 内存模型
一共分为两类:线程共享的和线程私有的
线程共享:静态方法区(在JDK1.8后被元空间替代)、堆
线程私有:虚拟方法栈、本地方法栈、程序计数器
虚拟方法栈:每个线程私有的区域,每个方法执行时,都会创建出一个栈帧,用于存储局部变量、动态链接、方法出口等信息;每个方法从调用到结束的过程,代表一个栈帧从入栈到出栈的过程。
静态方案区:常量、静态变量
1.3 垃圾回收
1、算法:
垃圾回收器主要有以下算法:标记清除、标记整理、复制算法、分代回收
2、垃圾回收器:
串行回收器:最原始的垃圾回收器,通过单线程执行垃圾回收,执行过程中,应用线程会停掉等待;包含:Serial、Serial Old垃圾回收器
并行回收器:多个线程通过执行垃圾回收器,但是在执行的过程中,应用线程也是需要停掉等待的;包含:PreNew、Paralle Old
并发回收器:CMS、G1、ZGC
CMS(JDK5):并发标记算法,主要目的是减少应用程序的停顿时间;该回收器采用的标记清除算法,主要分为四步操作:
1》初始标记:标记出所有可达的根对象,这步是STW(Stop-The-World)操作;
2》并发标记:标记出所有可达的对象,这步是并发执行的;
3》重新标记:标记出所有在并发阶段被修改的对象,确保在并发阶段结束时,没有新增对象或被标识的对象被修改;这步会STW;
4》并发清除:这步是在重新标记完成之后,会启动并发清除,垃圾回收器与应用程序并发执行;
优点:减少停顿时间、提高吞吐量
缺点:垃圾碎片太多、停顿时间不确定
G1(JDK9):分代清除算法,主要是将内存划分为年轻代和老年代;由于年轻代生命周期比较短,所以采用复制算法;老年代声明周期比较长,采用标记整理算法;该回收器还有一个更大的优势,是可预测模型算法,会根据之前的回收记录,评估平均需要多长时间,尽量满足设置的预期值;
优点:低停顿(因为分代)、可预测的停顿时间、内存碎片整理
缺点:性能波动(可能出现在应用程序刚启动或者堆内存分配太小)
ZGC(JDK11):是一个采用region(分区)布局,不分代的垃圾回收器,使用了读屏障、染色指针和内存多重映射来实现可并发的标记-整理算法,以低延迟为首要目标;在对吞吐量影响不大的情况下,实现任意堆内存大小下可以把垃圾回收停顿时间限制在10ms内
优点:极低停顿、对大堆内存的支持、高并发性能(采用了可中断的并发处理方式,可以在不影响应用程序响应的情况下完成回收)
缺点:性能成本比较高(会有一些额外开销,内存开销:因为他需要维护额外的数据结构来支持并发标记、整理、清理的过程;线程开销:因为他是可中断的处理方式,所以需要创建和管理一些额外的线程来操作垃圾回收)
3、四种引用:
强引用:即使内存不足时,也不会回收该对象;例如:new Object();
软引用:当系统内存不足时,会尝试回收该引用的对象;例如:使用java.lang.ref.SoftReference创建的对象
使用场景:缓存机制
弱引用:当垃圾回收器扫描到该引用时,不管内存是否充足,都会被回收;例如:使用java.lang.ref.WeakReference;
使用场景:对对象声明周期不敏感的缓存
虚引用:不会对对象生命周期产生影响,只是在对象被回收时被调用;
使用场景:当对象被回收后,如果需要做清理操作、记录日志或其他操作可以调用该引用;
2、数据结构
2.1 数组(Array)
定义:是一种线性数据结构,由一串连续的内存单元组成,用来存储相同类型的数据元素;
优点:1、查询快,可以根据索引快速访问任意元素,时间复杂度是O(1);
缺点:1、插入和删除慢;2、扩容代价比较大,如果确定大小了就无法扩容了;
底层使用的类:ArrayList、Vector;
2.2 队列(Queue)
定义:是一种线性数据结构,具有先进先出的特性,每次添加元素到队尾从队首开始消费
优点:1、先进先出,保证顺序;2、线程安全,在并发情况下可以使用ConcurrentLinkedQueue保证安全;
缺点:只能从队尾或者队首访问,无法随意访问;
底层使用的类:Linked List实现了Queue接口,因此,可以用作队列的实现;
2.3 链表(Linked List)
定义:是一种线性数据结构,由一系列节点组成,每个节点都有本节点元素及指向下一节点的指针;
优点:1、具有动态性,不需要提前分配内存;2、插入和删除效率高,只需要移动指针就可以;
缺点:1、查询慢,链表中的节点不是联系存储的,时间复杂度O(n);2、额外空间开销,每个节点都需要有一个指针指向下一个节点;
底层使用的类:Linked List
2.4 哈希表(Hash)
定义:是以键值对的形式存储,由数组和哈希函数组成,哈希函数将键映射到数组索引位置上,从而查询接近于常数;
优点:1、快速查找;2、快速增删;3、适用于大规模数据;
缺点:1、易出现哈希冲突;2、不适用有序操作;
底层使用的类:HashMap
2.5 栈(Stack)
定义:是一个先进后出的数据类型,一般有两种操作方式,压栈、出栈;
优点:操作简单,都是在栈顶操作
缺点:不灵活,只能在栈顶操作
底层使用的类:Stack
2.6 堆(Heap)
定义:堆是一个特殊的树形结构,通常由一个数组来表示;堆分为最大堆和最小堆,最大堆的父节点大于等于子节点,最小堆的父节点小于等于子节点;
优点:1、快速插入和删除;2、快速查找最值;3、不要求整体数据集有序,只要局部有序
缺点:1、不支持在常数范围内查看数据集,只支持查看最大值或最小值;
底层使用的类:java.util.PriorityQueue
2.7 图(Graph)
定义:是一个抽象的数据结构,由点和边组成;通常分为有向图和无向图;
优点:1、可以很好的表示关系
缺点:1、比较复杂,由于图中点和边的关系,让算法相对较复杂;
底层使用的类:Java 标准库中的 Graph 接口和相关类,如 ArrayList 来表示邻接表
3、数据库
3.1 索引
主键索引:逐渐索引,数据库帮我我们创建,不能有空值;
普通索引:基本索引类型,没有限制,允许在定义索引列中插入重复值和空值;
唯一索引:索引列中的值必须是唯一的;
组合索引:组合多个字段创建的索引,需要遵守最左原则;
全文索引:只有MYSUM引擎支持,并且只支持CHAR、VARCHAR、TEXT几种类型;
缺点:1、空间损耗,索引也需要一定的物理空间,尤其是联合索引占用会更大;2、性能影响,在插入修改和删除时,会更新索引;
3.2 存储引擎
MYSQL有两种存储引擎,Mysum和InnoDB,默认使用的是InnoDB,支持事务;数据结构有两种hash和Btree;并且使用Btree的话,它底层是属树结构,支持排序,像hash结构的话,不支持排序,只是查询快;
3.3 事务
1、四大特性
原子性(Atomicity):一次事务操作,要么全部成功,要么全部失败;
数据在写数据库时,会先写undo log文件中,当提交事务之后,会建undo log文件中的日志提交,如果要回滚,直接回滚该文件的日志即可;
一致性(Consistency):当一次完成的事务操作,事务前后状态保证一致
这个特性主要是靠约束或者业务逻辑保证;
隔离性(Isolation):事物与事物之间互不影响
主要是靠mvcc和锁保证事务隔离
锁:通过行级锁,可以防止下修改数据的过程中被其他事务修改
mvcc:通过本身事务默认的隔离级别“可重复读”,可以解决不可重复读和幻读的问题
持久性(Durability):将数据持久化到磁盘
主要通过预写日志的方式,将事务提交的内容先记入redo log文件中,然后redo log会定期刷盘,达到持久化的作用;
2、事务并发问题
脏读:A事务读取了B事务未提交的数据
不可重复读:在A事务内同一个查询,读到了两个不一样的结果,这是因为读取了B事务修改完并提交的数据
幻读:在同一个事务内,同样的两次查询,返回的结果不一样,这是因为有其他事务写入数据并提交了事务
3、事务隔离级别
读未提交:select语句不加锁,事务中修改的语句没有提交,对其他事务也都是可见的;
读已提交:可以读取到其他事务已经提交的数据,但是多次读取的数据可能不一致;这种级别可以避免脏读;
可重复读:保证了在同一事务中,多次读取同一个记录的结果是一致的;解决了不可重复读和脏读;
串行化:强制事务串行执行,在读取每一行数据时,加行锁;解决了脏读、不可重复读、幻读;
3.4 MVCC机制
多版本并发控制,是一种无锁的机制保证高并发;主要目的是提高数据库并发性能,降低死锁风险;
提高并发性能:读写操作之间互不影响;
降低死锁风险:无需使用显示锁进行并发控制;
应用事务:读已提交、重复读;
读未提交:直接写表
串行化:加表锁
组成:1、隐藏列 2、undo log 3、read view
三个隐藏列:1、当前数据操作事务ID 2、老数据列 3、事务ID列
3.5 锁
乐观锁:一般是用户自己实现的,例如使用版本号、时间戳等方式;
悲观锁:InnoDB支持表锁、行锁、页锁,并发能力强,但是容易死锁;Mysum支持表锁,不会出现死锁,但是并发能力差;
3.6 场景案例
场景一:
如果在同一个事务中,首先读取了表 T 的 A 字段为 1,然后在事务中修改了 A 字段为 2,继续在同一个事务中查询这张表,那么在查询时应该能够看到修改后的结果,即 A 字段的值为 2。
这是因为在数据库的隔离级别中,通常情况下,在同一个事务中所做的修改对该事务中的后续查询是可见的。即使在未提交的情况下,事务内部的修改对后续查询也是可见的。所以在这个场景下,事务内部先读取了 A 字段为 1,然后修改为 2,后续查询应该能够看到 A 字段被修改为 2 的值。
场景二:
如果在 AA 事务中首先读取了表 T 的 A 字段为 1,然后在另一个 BB 事务中修改了 A 字段为 2 并且 BB 事务提交了事务,之后在 AA 事务中继续查询这张表,那么在 AA 事务中的查询结果应该是 A 字段的值为 1。
这是因为 AA 事务在自己开始时读取了 A 字段为 1,之后 BB 事务修改了 A 字段为 2 并提交了事务。在数据库默认的隔离级别下,其他事务在提交后所做的修改对当前事务不可见。因此,尽管 BB 事务修改了 A 字段为 2 并提交了事务,但是在 AA 事务中查询时,AA 事务只能看到自己开始时的快照,即 A 字段的值为 1。