Posts
1.Java基础
1.1 Java程序从源代码到运行的过程 JVM可以理解的代码就是字节码(.class),面向Java虚拟机,Java语言通过字节码的方式,在一定程度上解决传统解释性语言执行效率低的问题,同时又保留了解释性语言可移植的特性。 .class文件到机器码这一步,JVM加载器会首先加载字节码文件,然后通过解释器逐行执行,这种执行速度比较慢,并且有些代码和方法会被经常调用,所以引进了JIT(Just in Time Compilation),属于运行时便意,当JIT完成第一次遍以后,会将对应的机器码保存下来,下次直接使用。所以说Java是编译与解释共存的语言。 1.2 AOT v.s. JIT AOT(Ahead of Time Compilation),在程序执行前就进行编译,属于静态便意,可以提高Java程序启动速度,避免预热时间过长,减少内存占用,增加程序安全性(AOT遍以后的代码不容易被反编译和修改),适合云原生场景。 但是AOT不支持反射、动态代理、动态加载和JNI(Java Native Interface),所以很多框架和库都无法使用。 1.3 Java v.s. C++ Java不提供指针直接访问内存 Java类是单继承,C++可以多继承,但是Java的接口可以多继承 Java有自动内存管理垃圾回收机制,不需要程序员手动释放无用内存。 C++同时支持方法重载和操作符重载,但是Java只支持方法重载。 1.4 基本数据类型 byte(8位)、short(16位)、int(32位)、long(64位):默认值0 float(32位)、double(64位):默认值0.0 char(16位):默认值u0000 boolean(1位):默认值false **包装类型:**包装类型变量不赋值则为null,对于包装类型,==比较的是内存地址,而不是值,所以需要用equals方法比较。占用空间包装类型更大一些,除了定义一些常量和局部变量之外,我们在其他地方比如方法参数、对象属性中很少会使用基本类型来定义变量。并且,包装类型可用于泛型,而基本类型不可以。 包装类型的缓存: 非浮点数缓存范围是[-128, 127],Character缓存范围位[0, 127],Boolean则是true和false。 包装类型的自动拆箱与装箱: 装箱:将基本类型用对应的引用类型包装起来,本质调用包装类的ValutOf方法; 拆箱:将包装类型转换为基本数据类型,本质调用xxxValue方法; 为什么说是几乎所有对象实例都存在于堆中呢? 这是因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析,如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。 **浮点数运算精度丢失:**使用BigDecimal进行浮点运算。 **超过Long整数类型的数据如何表示:**使用BigInteger进行表示,内部使用int[]数组进行存储任意大小的整形数据。 1.5 深拷贝和浅拷贝 浅拷贝:在堆上创建一个新的对象,但是原对象内部属性如果是引用类型,则浅拷贝会直接复制原对象内部的引用地址。 深拷贝:完全复制整个对象。 引用拷贝:复制一个引用,指向原对象。 1.5 Objects 1.5.1 ==和equals方法 ==:如果是对象,则比较地址是否相同,基本类型则比较值。 object.equals():没有重写的情况下,效果与==相同,String类型中的equals方法被重写过,会比较内部值是否相同。 1.5.2 hashCode()有什么用 获取int类型哈希码,hashCode相同时,两个对象不一定相等(哈希冲突),如果两个对象hashCode相等,并且equals返回true,才认为两个对象相等,如果两个对象hashCode不相等,则直接认为两个对象不相等。 ...
2.Index
1 索引基础 1.1 索引的分类 1.1.1 按数据结构分:B+Tree索引、Hash索引、Full-text索引 InnoDB支持B+Tree索引和Full-Text索引,不支持Hash索引但是内存结构中有一个自适应hash索引; MyISAM支持B+Tree索引和Full-Text索引,但是不支持Hash索引; Memory支持B+Tree索引和Hash索引,但是不支持Full-Text索引。 InnoDB存储引擎在创建表时:如果有主键,会默认使用主键作为聚簇索引的索引键;如果没有主键,就会选择第一个不包含NULL值的唯一列所谓聚簇索引的索引键;如果都没有则会自动生成一个隐式自增ID作为聚簇索引键。 其他索引都属于辅助索引,也称为二级索引或非聚簇索引。创建的主键索引和二级索引默认使用的都是B+Tree索引。B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。 B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。 主键索引的 B+Tree 和二级索引的 B+Tree 区别如下: 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里; 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。 通过二级索引查询会先检二级索引中的 B+Tree 的索引值,找到对应的叶子节点,然后获取主键值,然后再通过主键索引中的 B+Tree 树查询到对应的叶子节点,然后获取整行数据。这个过程叫「回表」,也就是说要查两个 B+Tree 才能查到数据。当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查。这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据。 为什么InnoDB选择B+Tree作为索引的数据结构? B+Tree vs B Tree B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的磁盘 I/O 次数下,就能查询更多的节点。另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的基于范围的顺序查找,而 B 树无法做到这一点。 B+Tree vs 二叉树 对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。 ...
2.Java集合
2.1 集合(容器) 主要由两大借口派生而来:一个是Collection、一个是Map,而Collection下还有List、Set、Queue。 List:存储的元素是有序的,可重复的 Set:存储的元素是不可重复的 Queue:存储元素有序、可重复,按照特定排队规则确定先后顺序 Map:使用键值对存储,key无序、不可重复,value无序、可重复。 2.2 集合框架底层数据结构 List ArrayList:Objects数组 Vector:Objects数组 LinkedList:双向链表 Set HashSet:基于HashMap实现 LinkedHashSet:基于LinkedHashMap实现 TreeSet:基于红黑树实现 Queue PriorityQueue:Object数组实现的小顶堆 DelayQueue:基于Priority实现 ArrayDeque:可扩容双向数组 Map HashMap:JDK1.8之前由数组+链表组成,JDK1.8后,链表长度大于8时,转换为红黑树。 LinkedHashMap:继承自HashMap,底层与HashMap相同,不同的是增加了双向链表,使得上面的接口可以保持键值对插入的顺序。 HashTable:数组+链表组成,链表主要为了解决Hash冲突 TreeMap:红黑树
3.JUC
3.1 线程 3.1.1 生命周期和状态 线程的上下文切换: 主动让出CPU:调用sleep、wait函数 时间片用完 调用了阻塞类型的系统中断 被终止或者结束运行 Thread#sleep()方法和Object#wait()方法对比: 两者都可以暂停线程的运行 sleep方法没有释放锁,而wait方法释放了锁 wait用于线程之间交互通信,而sleep主要用于暂停执行 wait方法执行后,线程不会主动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep方法会主动苏醒,或者使用wait(long timeout)超时后也会主动苏醒。 sleep()是Thread类的静态本地方法,而wait是Object类的本地方法。 为什么wait方法不定义在Thread中? wait方法是为了让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁,每个对象都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入等待状态,自然应该操作对应的对象,而不是当前线程。 3.2 Volatile和Synchronized关键字 3.2.1 volatile关键字 volatile关键字可以保证变量的可见性,指示JVM这个共享变量是不稳定的,每次使用时都需要去主存中进行读取,但是volatile关键字不保证数据的原子性。 volatile关键字还可以防止JVM 的指令重拍,对修饰的变量进行读写时,会插入特定的内存屏障禁止指令重排序。 3.2.2 synchronized关键字 synchronized关键字早期属于重量级锁,因为其底层monitor(监视器锁)基于操作系统的Mutex Lock实现,需要进行用户态和内核态的切换,Java 6之后对synchronized引入了大量的优化来减少锁操作的开销。 synchronized可以对对象加锁,修饰静态方法,修饰代码块。构造方法不能使用synchronized关键字,因为构造方法本身是线程安全的,但是如果构造方法中使用了共享资源,可以在构造方法内使用synchronized关键字修饰代码块。 3.2.3 synchronized和violatile的区别 volatile关键字是线程同步的轻量级实现,volatile关键字只能用于变量,而synchronized关键字可以修饰方法和代码块。 volatile关键字能保证数据的可见性,但是无法保证数据的原子性,synchronized两者都可以保证。 【可见性】避免读取变量在缓存中的旧值,而是强制读取主存中的新值。 3.3 乐观锁和悲观锁 3.3.1 悲观锁 假设最坏情况,访问共享资源时,先获取锁。 synchronized和ReentrantLock等独占锁都是悲观锁思想的实现。 3.3.2 乐观锁 假设最好情况,访问访问共享资源时不需要获取锁,提交修改时验证资源是否发生了冲突。 可以通过CAS机制或版本号机制实现乐观锁。 CAS机制依赖于CPU的原子指令,但是无法避免ABA问题。 3.3.3 ReentrantLock ReentrantLock实现了Lock接口,有一个内部类Sync,Sync继承AQS(AbstractQueuedSynchronizer),而Sync有公平锁和非公平锁两个子类。ReentrantLock默认使用非公平锁。 公平锁保证按照申请顺序获取锁。 非公平锁不保证按照申请顺序获取锁,可能会出现饿死的情况。 3.3.4 synchronized和ReentrantLock有什么区别? 两者都是可重入锁 synchronized依赖于JVM,而ReentrantLock依赖于API ReentrantLock比synchronized增加了一些高级功能:等待可中断、可实现公平锁,可实现选择性通知。 写锁可以降级为读锁,但是读锁不能升级为写锁,为了避免死锁的情况。 3.4 TreadLocal 3.5 线程池 3.6 Future 3.7 AQS
3.Transaction
3.1 事务的特性 原子性:一个事务中的操作要么全部完成,要么全部不完成。 一致性:事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。 隔离性:数据库允许多个并发事务的同时,防止多个事务并发时由于交叉执行而导致数据不一致。 持久性:事务处理结束后,对数据的修改是永久的。 InnoDB通过redo log保证持久性,通过undo log保证原子性,隔离性通过MVCC机制或锁机制保证,一致性则通过持久性+原子性+隔离性来保证。 3.2 并发事务会引发什么问题 脏读 如果一个事务读到了另一个事务还没有提交的修改数据,就意味着发生了脏读。 不可重复度 在一次事务内多次读取同一个数据,如果出现两次读到的数据不一致的情况,就意味着发生了不可重复读现象。 幻读 在一个事务内多次查询某个符合查询条件的记录数量,如果出现前后两次查询到的记录数量不一致的情况,就意味着发生了幻读现象。 3.3 事务的隔离级别 读未提交(*read uncommitted*),指一个事务还没提交时,它做的变更就能被其他事务看到; 读提交(*read committed*),指一个事务提交之后,它做的变更才能被其他事务看到; 可重复读(*repeatable read*),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别; 串行化(*serializable* );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行; MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象(并不是完全解决了: 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。 3.4 Read View在MVCC中如何工作? Read View中的四个字段 m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。 min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。 max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1; creator_trx_id :指的是创建该 Read View 的事务的事务 id。 聚簇索引记录的两个隐藏列 trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里; roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。 一个事务去访问记录时,除了自己更新的记录总是可见的之外: ...
4.JVM
4.1 内存区域 线程私有 程序计数器:可以看做当前线程所执行的字节码的行号指示器,每个线程都有一个独立的程序计数器。JVM通过改变程序计数器依次读取指令,在多线程情况下,程序计数器用于记录当前线程执行的位置。 虚拟机栈:线程私有,生命周期和线程相同,除了Native方法外,其他所有Java方法的调用都是通过栈实现的。方法嗲用的数据需要通过栈进行传递,每次方法调用会有一个对应的栈帧被压入栈中,结束调用则弹出。 栈帧内部有:局部变量表、操作数栈、动态链接、方法返回地址。 局部变量表:存放编译期间可知的各种数据类型、对象引用。 操作数栈:主要作为方法调用的中转站,存放方法执行过程中的中间计算结果。 动态链接:主要服务一个方法需要调用其他方法的场景。 本地方法栈:为虚拟机使用到的Native方法服务,在HotSpot虚拟机中,和Java虚拟机栈合二为一。 StackOverFlowError:如果栈的内存不允许动态扩张,那么当线程请求栈的深度超过虚拟机栈的最大深度,则会报出此错误。 OutOfMemoryError:如果栈的内存可以动态扩张,如果虚拟机在动态扩张栈时无法申请到足够的内存空间,则报出此错误。 线程共享 堆:存放对象实例,几乎所有的对象实例都在此分配内存。垃圾收集器管理的主要区域,也被称为GC堆。 字符串常量池:是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。 方法区:方法区会存储已经被JVM加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。(永久代和元空间实际上是方法区的具体实现,JDK1.8之前是永久代,而之后是元空间,永久代受到JVM内存的上限,而元空间受到本机可用内存限制,相对来说溢出可能性变小,并且永久代会给GC带来不必要的复杂度) 运行时常量池:Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有存放编译期生成的各种字面量和符号引用的常量池表。常量池表户会在类加载后放到方法区的运行时常量池中。 直接内存(非运行时数据区的一部分):直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI 的方式在本地内存上分配的。 4.2 垃圾回收 新生代内存(Eden, S0, S1) 老生代内存(Tenured) 永久代内存(PermGen[JDK1.7],MetaSpace[JDK1.8]) 4.2.1 内存分配和回收原则 内存分配 对象优先在Eden区分配内存 大对象直接进入老年代 长期存活对象进入老年代(经历过一次GC后仍然存活,如果能被Survivor空间收纳,则进入s0或s1,并将对象年龄设置为1,之后每经历一次GC,年龄+1,默认到达15岁后进入老年代)【HotSpot虚拟机对对象按照年龄从小到大累计内存,当内存大小超过Survivor区一半时,重新设置晋升老年代年龄阈值为当前年龄和阈值年龄中的较小值】 回收原则 部分收集: 新生代收集:只对新生代进行收集 老年代收集:只对老年代收集 混合收集:对整个新生代和部分老年代进行收集 整堆收集:收集整个Java堆和方法区 4.2.2 空间分配担保 为了确保Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间。 发生Minor GC之前,JVM判断老年代最大可用的连续空间是否大于新生代所有对象的总空间,如果成立则证明Minor GC是安全的,否则查看是否允许担保失败的参数,如果允许,则检查老年代连续空间是否大于历次晋升到老年代的平均大小,如果大于则进行Minor GC,否则改为Full GC。 4.2.3 死亡对象判断方法 引用计数法(难以解决循环引用问题) 可达性分析算法: GC ROOT:JVM虚拟机栈(局部变量表)中引用的对象、本地方法栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象,所有被同步锁持有的对象,被JNI引用的对象。 对象可回收不一定会被回收,第一次会被标记,被标记的对象会被放在一个队列中进行第二次标记,除非这个对象与引用链上任何一个对象建立链接,否则就会被回收。 引用类型: 强引用:具有强引用的对象不会被回收 软引用:只具有软引用的对象如果内存空间不足,则会被回收 弱引用:只有弱引用的对象,一旦被发现,则会回收 虚引用:不会决定是否被回收,主要用于跟踪对象被垃圾回收的活动 4.2.4 垃圾收集算法 标记-清楚算法:效率不高,会有内存碎片 标记-复制算法:内存缩小为一般,如果存活对象过大,复制性能会变差,不适合老年代 标记-整理算法:标记后,让所有对象移动到一端。 分带收集算法:新生代采用“标记-复制算法”,老年代采用“标记-清楚算法”或“标记-整理算法” 4.2.5 垃圾收集器 Serial收集器 ParNew收集器 Parallel Scavenge收集器 Serial Old收集器 Parallel Old收集器 CMS收集器 G1收集器 ZGC收集器 4.3 类加载 加载(通过类名获取此类的二进制字节流,将字节流所代表的静态存储结构转换为方法区的运行时数据,内存中生成一个代表该类的Class对象,作为方法区数据的访问入口)【由类加载器完成】 验证(确保Class文件的字节流包含的信息符合约束) 准备(为类变量分配内存并设置变量初始值) 解析(将常量池内的符号引用替换为直接引用) 初始化(执行clinit) 卸载(所有实例对象被GC,没有任何地方被引用,该类的类加载器已被GC后,类会才可能被卸载) 4.3.1 类加载器 用于加载Java类的字节码 ...
4.Lock
【!MyISAM不支持行级锁】 4.1 锁的分类 全局锁 表级锁 表锁 元数据锁 意向锁 AUTO-INC锁 行级锁 Record Lock Gap Lock Next-Key Lock 插入意向锁 4.2 MySQL是怎么加锁的? 4.2.1 什么SQL语句会加行级锁 普通的select语句是不会对记录加锁的(除了串行化隔离级别),因为都属于快照读,是通过MVCC实现的。 如果要在查询记录时加行级锁,可以使用: # S型锁 SELECT ... LOCK IN SHARE MODE; # X型锁 SELECT ... FOR UPDATE; 上面这两句必须在一个事务中,因为当事务提交了,锁就会释放,所以使用这两条语句时,要加上begin或者start transaction。 除上面这两句锁定读语句会加行级锁之外,update和delete操作都会加行级锁,并且所得类型都是独占锁(X型)。 UPDATE TABLE ... WHERE ...; DELETE FROM TABLE WHERE ... 共享锁之间读读共享,读写互斥。独占锁满足写写互斥,读写互斥。 4.2.2 行级锁有哪些种类 在读已提交隔离级别下,行级锁的种类只有记录锁。 在可重复读隔离级别下,行级锁除了有记录锁,还有间隙锁(避免幻读)、临键锁。 Record Lock,记录锁,仅对一条记录上锁; Gap Lock,间隙锁,锁定一个范围,但是不包括记录本身; Next-Key Lock,Record Lock + Gap Lock,锁定一恶搞范围,并且锁定记录本身。 4.2.2.1 Record Lock 记录锁有X锁和S锁之分。 4.2.2.2 Gap Lock 只存在于可重复度隔离级别,目的是为了解决可重复度隔离级别下幻读的现象。 ...
5.Log
5.1 Undo Log 用于保证事务ACID中的原子性。 事务没提交时,会将记录更新前的数据记录到undo log日志文件中。 插入:记录主键值 删除:记录这条记录全部内容 更新:记录这条记录旧值 一条记录的每一次更新操作产生的undo log格式都有一个roll_pointer和trx_id指针,分别用于将undo log串成链表和记录被哪个事务所修改。 所以undo log还可以和ReadView配合实现MVCC。 作用: 实现事务回滚,保障事务的原子性。事务处理过程中,如果出现了错误或者用户执 行了 ROLLBACK 语句,MySQL 可以利用 undo log 中的历史数据将数据恢复到事务开始之前的状态。 实现 MVCC(多版本并发控制)关键因素之一。MVCC 是通过 ReadView + undo log 实现的。undo log 为每条记录保存多份历史数据,MySQL 在执行快照读(普通 select 语句)的时候,会根据事务的 Read View 里的信息,顺着 undo log 的版本链找到满足其可见性的记录。 5.2 Redo Log redo log用于保证ACID中的持久性。 由于更新数据会先写入Buffer Pool,而内存是不可靠的,所以为了防止断电导致数据丢失,InnoDB会先更新内存,然后将本次对这个页的修改以redo log的形式记录,后续在适当的时候,后台线程再将Buffer Pool中的脏页刷到磁盘中,这就是WAL(Write-Ahead Logging)技术。 WAL 技术指的是, MySQL 的写操作并不是立刻写到磁盘上,而是先写日志,然后在合适的时间再写到磁盘上。 redo log是物理日志,记录某个数据页做了什么修改,每执行一个事务就会产生这样一条或者多条物理日志,事务提交时,只需要将redo log持久化到磁盘即可,不需要等待buffer pool中的脏页持久化。 这样即使系统崩溃,虽然脏页没有持久化,但是MySQL可以根据redo log的内容,将数据恢复到最新状态。 被修改的Undo页面,需要记录对应的redo log。在内存修改该 Undo 页面后,需要记录对应的 redo log。 redo log 和 undo log区别: 两种日志都是存储引擎日志 redo log 记录了此次事务「完成后」的状态,记录的是更新之后的值。 undo log 记录了此次事务「开始前」的状态,记录的是更新之前的值。 写入 redo log 的方式使用了追加操作, 所以磁盘操作是顺序写,而写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。 ...