Java进阶
1、谈谈对ArrayList的理解
-
定位:有序集合
-
数据结构:数组
-
扩容原理
-
如果没有指定初始长度,初始数组长度为0;在第一次添加元素时会将数组长度初始化为10;
-
添加元素时先判断长度够不够用,不够才会扩容;每次扩容后的长度为扩容前的1.5倍,
-
如果扩容后的长度不够,比如1乘1.5,取整还是1;这个时候会直接让 新数组长度=能满足新增元素的最小长度
-
2、ArrayList和LinkedList区别
-
数据结构:ArrayList是数组实现的,地址在内存中是连续的;LinkedList是双向链表实现的,地址是不连续的
-
操作数据的效率
-
查找:ArrayList对于已知下标的元素查找效率为O(1),未知的为O(n);LinkedList为O(n)
-
插入和删除:ArrayList插入和删除需要挪动数组,为O(n);LinkedList需要遍历找到位置也是O(n)
-
3、List和Set的区别
-
是否有序:List是有序的,通过数组下标的顺序储存元素;Set是无序的,通过对象的哈希值作为key访问元素
-
是否可重复:List可以重复;Set不可重复,通过equals方法比较两个对象是否重复,
4、请你说说BIO、NIO、AIO
BIO(Blocking I/O,阻塞式I/O):当应用程序发起一个I/O操作时,线程会被阻塞,期间无法执行其他任务,直到该操作完成。
NIO(Non-blocking I/O,非阻塞式I/O,Tomcat、Redis):引入了缓冲区(Buffer)、通道(Channel)和选择器(selector)的概念;发起I/O操作时如果没有准备好数据会直接返回,不需要等待。
AIO(Asynchronous I/O,异步I/O):在AIO模式下,当发出一个I/O操作后,直接注册一个回调函数,当I/O操作完成时,系统会自动调用这个回调函数通知应用。
5、解释一下IO多路复用
-
单线程监听多个文件描述符(如socket连接),当存在某个Socket可读可写的时候得到通知;
-
常用的有epoll方式实现的IO多路复用,底层是用的红黑树来管理文件描述符,相比poll方式的链表结构更快,是Linux特有的实现方式;
6、请你讲一下Java NIO
结构
-
Channels:缓冲区与其他数据源进行数据传输的媒介,它是双向的
-
Buffers:缓冲区是与通道交互的中转站,所有数据都通过缓冲区处理
-
Selectors:选择器,用于管理多个通道的事件,使得单个线程能够监听多个通道上的I/O事件。
理解:在发起IO请求之后通过选择器去监听通道中的IO操作是否完成,发请求的线程不会被阻塞,会在IO完成之后得到通知。
Java的非阻塞式IO用途很广泛,像Tomcat、Redis的网络模型中都有用到;
7、简单说下你对JVM的了解
java虚拟机,负责将字节码文件通过类加载器加载到内存中,通过解释器或即时编译器将字节码文件转化成机器码,然后交给操作系统执行来实现系统调用、内存管理和线程管理
分为三个子系统:
-
类加载器:分为启动类加载器(BootStrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(AppClassLoader),通过双亲委派机制来实现类的加载(向上委派,向下加载;确保核心类库被启动类加载器加载,避免用户通过下层类加载器篡改核心类库)
-
执行引擎:实现JVM与操作系统的交互
-
运行时数据区
-
线程私有区域:
-
程序计数器:记录程序执行行号,不会引起OOM
-
本地方法栈:包含线程调用的native方法信息(native方法表示不是用java实现的方法)
-
虚拟机栈
-
局部变量表:实例方法的this对象(只保存引用,对象都放在堆区),方法参数,方法体中的局部变量
-
操作数栈:临时保存变量
-
帧数据
动态链接:保存了字节码中符号引用的编号到内存地址的映射关系,比如getstatic #10,这里的#10对应内存 中的一个地址,根据这个地址找到对应的值
方法出口:方法结束时会告诉程序计数器调用该方法的代码中下一条指令的地址,被称为方法出口
异常表:保存了异常捕获的生效范围以及异常发生后跳转到的字节码指令的位置
-
-
-
共享区域
-
堆内存:字符串常量池(优化字符串的管理),实例对象
-
方法区:JDK8之后被定义为元空间,独立于堆存在于本地内存中,不属于JVM内存规范中的一部分了,包含了类的元信息(.class文件信息)、运行时常量池(管理基本类型的变量)
-
-
8、聊聊JVM内存模型(JVM运行时数据区)
线程私有区
-
程序计数器:保存程序执行的行号,占用小
-
本地方法栈:保存线程本地的native方法信息
-
JVM栈:
-
局部变量表:保存实例方法的this对象,方法参数,方法中的局部变量
-
操作数栈:临时保存变量
-
栈帧:
-
动态链接:字节码中符号引用的编号到内存地址的映射关系
-
方法出口:该方法执行结束后要跳转到的下一条指令的地址
-
异常表:异常捕获的生效范围和发生异常后程序要跳转的指令的位置
-
-
共享内存区
-
堆区:字符串常量池(管理字符串),引用类型的对象实例(new 出来的对象,包括数组)以及里面的变量(基本类型和引用类型)
-
方法区:JDK8之后被定义为元空间,独立于堆存在于本地内存中,不属于JVM内存规范中的一部分了。保存类的运行时常量池(管理常量),静态变量和静态方法,类的元信息(.class文件信息)
9、说说JVM的垃圾回收机制
垃圾回收主要是针对堆内存进行的,堆内存是JVM占用最大的内存区
解决三个问题
a、什么时候进行回收?
内存不够的时候;手动gc不一定会直接进行垃圾回收,只是给JVM一个通知,建议JVM进行gc,但是不一定会被执行,就是说执行权还在JVM手上
b、哪些内存需要被回收?
没有被引用的对象,提供了两种找到这些对象的方式
-
引用计数法:记录每个对象被引用的次数,初始为0,每被引用一次就加一,断开引用就减一,进行gc时检查每个对象被引用的次数是否等于0,等于0表示可以被回收
-
可达性分析法:在JVM中定义了很多根对象,比如Thread对象,被锁关联的对象(Synchronized(object)中的object),这些对象不会被清除;进行gc的时候,会经过这些根对象的引用关系,找到被关联的其他对象,标记为不可清除的对象
c、怎么回收?
垃圾回收算法
-
标记清除算法:先标记再清理;效率高,但是会产生内存碎片(小的内存无法被利用起来)
-
复制算法:将内存空间分为两个大小相同的区域称为From区和To区,只使用From区储存数据,进行回收时将From区存活的对象转移到To区,然后完全删除From区的数据,最后将两个区的名称互换。这样就避免了内存碎片的产生;效率高;但是内存使用率低(只能使用一半的内存)
-
标记整理算法:java中使用可达性分析法标记对象,将存活对象整理到堆的一端,清理掉非存活对象,效率低,但是没有内存碎片
-
分代收集:基于一个理论——大部分对象的生存周期很短。将堆内存分为新生代和老年代,结合使用了多种垃圾回收算法
-
新生代分为两个区域,eden区,两个幸存区(分为s0和s1),其中一个幸存区用来储存对象,另一个空闲
-
对于新创建的对象会被放入eden区;如果eden区满了,会触发新生代的垃圾回收:将eden区和存放对象的幸存区存活对象转移到空闲的幸存区,然后清空eden区和原幸存区,让存活对象的年龄加1
-
如果清理后eden区的大小依然不能满足要求,部分对象会被转移到老年代;
-
如果幸存区有对象的年龄达到阈值(默认为15),会被放入老年代;如果老年代满了,会触发整个堆内存的垃圾回收(包含老年代和新生代,时间会很长);如果依然无法满足要求,就OOM了
-
10、说说JVM中都有哪些垃圾回收器
分为老年代垃圾回收器(只负责老年代的回收)、新生代垃圾回收器(只负责新生代的回收)
-
新生代:serial(复制算法,串行);parNew(复制算法,并行);parallel Scavenge(复制算法,并行)
-
老年代:serial Old(标记整理算法,串行);CMS(标记清除算法,串行);parallel Old(标记整理算法,并行)
除此之外还有一个G1垃圾回收器,通常新生代和老年代需要组合使用;G1可以单独使用
常见的组合方式
-
serial+serial Old:串行垃圾回收器
-
parNew+CMS:关注暂停时间;新生代是并行,对serial做了多线程的改造;老年代是串行并且使用标记清除算法,效率高,暂停时间短。
-
parallel Scavenge + parallel Old:关注系统的吞吐量;并行垃圾回收器;具备自动调控堆内存大小的特点,JDK8中默认使用的垃圾回收器
G1垃圾回收器(这个放到了后面T14)
11、说说GC的可达性分析
在JVM中定义了很多GC Root对象(根对象),比如锁关联的对象、Thread对象,JVM栈中的实例对象,根对象不可被清除;通过这些根对象与其他对象的引用,以及被引用的对象与其他对象的引用,形成一些以根对象为头结点的引用关系链;根据这些引用关系链,就可以找到被引用的对象并将他们标记为不可被清理的对象。
12、请你说说Java的四种引用方式
-
强引用:正常通过new出来的对象都是强引用关系,被强引用关联的对象无法被GC清理掉,会造成内存泄漏
-
软引用:通过softRefence对象包装的对象,只有在内存不足的时候会被GC清理掉,内存充足的时候进行GC不会被清理
-
弱引用:通过WeakRefence对象包装的对象,只要进行GC,该对象就会被清理
-
虚引用:虚引用是最弱的一种引用关系,主要用于跟踪对象被垃圾回收的活动。虚引用必须和引用队列(
ReferenceQueue
)一起使用,当垃圾回收器准备回收对象时,会将虚引用加入到关联的引用队列中。程序可以通过检查引用队列来了解对象是否已经被回收,但不能通过虚引用来获取对象实例。
13、说说CMS垃圾回收器
-
定位:老年代,标记清除算法;关注暂停时间,在某些步骤可以与用户线程并发进行
-
执行步骤
-
初始标记:用极短的时间关联到GC Root直接关联的对象
-
并发标记:并发标记所有可以关联到的对象,用户线程不用暂停
-
重新标记:并发阶段对象可能会有变化,需要重新标记
-
并发清理:并发清理没有被标记的对象,用户线程不用暂停
-
-
优点:效率高,暂停时间短
-
缺点:
-
会产生内存碎片
-
会产生浮动垃圾;在两次并发过程中用户线程可能会创建一些新对象,这些对象不会参与到这次GC中
-
如果浮动垃圾过多,可能导致老年代空间迅速被填满,进而引发“Concurrent Mode Failure”,这时JVM不得不启动备用计划,比如暂停用户线程,使用Serial Old收集器进行全量GC,这将带来较长的停顿时间
-
14、请你讲下G1垃圾回收器
分区:
-
将堆内存等分为若干份大小相等的区域(称为Region),这些Region被分为eden区、幸存区和老年区(Old区);
-
他们的位置是随机的;Region的大小通过 堆空间/2048得到,必须是2的指数幂,通常为1m-32m之间(1、2、4、8、16、32);
-
分区的意义就是为了宏观调控垃圾回收的时间,可以通过参数 -XX:MaxGCPauseMills=毫秒数设置最大暂停时间;
-
G1会通过这个最大暂停时间来调整策略来尽量的保证在这个时间内完成GC
新生代回收:
-
流程
-
新创建的对象会被放入eden区,当G1判断eden区不足时,会执行新生代的垃圾回收
-
标记出eden区和幸存区存活的对象
-
根据配置的最大暂停时间,来选择某些区域将存活的对象复制到一个新的幸存区(这些对象的年龄+1;其他没有经历清理的Region中的对象年龄是不变的),然后清除选择的那些区域
-
每次清理一个eden区和幸存区的时候都会去记录耗时,作为下次回收时选择多少个区域的参考依据
-
当对象的年龄达到阈值(默认为15)时,会被放入老年代
-
如果对象的大小太大了,比如超过了Region的一半,就会直接被放入老年代,甚至可以横跨多个Region保存,这些大对象被称为Humongous(巨大的);
-
混合回收:当老年代占用达到阈值时(默认为堆内存的45%)触发混合回收(回收所有新生代和部分老年代),老年代采用复制算法,过程和CMS基本一致
-
流程
-
初始标记
-
并发标记
-
最终标记
-
并发清理
-
-
G1对老年代的清理会选择存活度最低的区域来进行回收,这样可以保证回收效率最高
-
如果清理过程中发现没有足够的Region存放转移的对象,会执行Full GC:单线程执行标记-整理算法对整个堆内存进行回收,此时会导致用户线程较长时间的暂停。
优点:暂停时间可控,不会产生内存碎片
15、说说类的加载机制(类的生命周期)
五个阶段
-
加载:类加载器会通过本地文件、动态代理、网络传输等方式以二进制流的形式获取类的字节码信息(类的元信息),然后将这些元信息生成与类对应的InstanceKlass对象,保存在方法区中;在堆中生成与类对应的Class对象,用于动态获取类信息;并从方法区复制部分需要被访问的方法到堆区,实现只从堆中访问类,保证访问类的安全性。
-
连接
-
验证
-
验证文件格式:文件头是否是cafebabe(java文件头格式)
-
元信息验证:例如类必须有一个父类(Object)
-
验证程序执行指令的语义
-
符号引用验证
-
-
准备:给静态变量赋初始值;int-0;byte-0;long-0l;boolean-false;short-0;double-0.0;char-‘\u0000’;引用类型为null;对于使用final修饰的静态变量,在这一步会直接赋值为代码中指定的值;
-
解析:将常量池中的符号引用替换成指向内存的直接引用
-
-
初始化:设置静态成员(静态变量的赋值,静态代码块的执行)
-
触发初始化的几种场景
-
访问一个类的静态变量或者静态方法
-
调用Class.forName方法
-
new一个实例对象:这个还会执行实例化代码块和构造函数
-
执行一个类的main方法
-
-
有父类的类的加载顺序:
-
父类--静态变量
-
父类--静态初始化块
-
子类--静态变量
-
子类--静态初始化块
-
子类main方法
-
父类--变量
-
父类--初始化块
-
父类--构造器
-
子类--变量
-
子类--初始化块
-
子类--构造器
-
-
-
使用:在代码中调用
-
卸载
16、解释一下双亲委派机制
在一个Java程序中存在三种类加载器
-
启动类加载器:加载jre/lib下的文件,比如rt.jar包
-
扩展类加载器:加载jre/lib/ext下的文件
-
应用程序类加载器:加载用户自定义的类
启动类加载器是扩展类加载器的父亲,扩展类加载器是应用程序类加载器的父亲
加载规则为:自底向上查找(是否加载过),自顶向下加载(是否可以加载);
-
当一个类需要加载的时候,会检查应用程序类加载器是否加载过
-
加载过就直接返回;没有加载过就检查扩展类加载器是否加载过
-
同理,加载过就直接返回;没有加载过就检查启动类加载器是否加载过
-
加载过就直接返回;没有加载过就检查启动类加载器能不能加载;能就加载并返回;不能就向下检查;直到被加载
-
-
简单说就是:向上查找父类加载器是否加载过这个类,如果加载过就直接返回;都没有加载过;就向下检查子类是否可以加载,可以加载就加载然后返回;直到该类被加载;
-
检索依据的是全类名(包名和类名);
-
如果用户自定义的类的全类名和核心类库里某个类的全类名一致的话,通过这个机制,会直接返回核心类库中的类(由启动类加载器加载),也就是说实际上用户自定义的类是没有被加载的。
-
换句话说,用户使用的Java.lang.String类只能是核心类库中的java.lang.String类,自定义的java.lang.String类在语法层面不会报错,但是在逻辑层面是没有任何意义的(不会被加载,不能被使用)。
-
在JVM中对于相同类的定义是:全类名+类加载器,两个都得相同;也就是说两个自定义加载器加载相同的类在JVM看来是不冲突的;但是两个全类名相同的类,只会被同一个加载器加载一次!
-
-
保证了核心类的一致性和安全性,避免重复加载。
17、类的实例化过程(对象的创建过程)
-
加载类:先检查这个类有没有被JVM加载,没有的话就按照类的加载流程加载、连接、初始化
-
分配内存:在堆中为对象分配内存,包括对象头和对象本身所占的空间
-
初始化:给实例变量赋初值,静态变量在类加载的时候已经初始化了
-
执行构造方法
-
返回引用地址
18、请你说说内存溢出
指JVM用来运行程序的内存不够用了,JVM内存分为五个部分
线程私有区
-
程序计数器:占用小,不会溢出
-
本地方法栈:保存Native方法,一般情况下不会溢出,在方法中使用递归可能导致溢出
-
JVM栈:保存了实例方法信息,一般情况下不会溢出,在方法中使用递归可能导致溢出
共享区域
-
堆内存:保存实例对象,字符串常量池;一般情况下发生内存溢出都指得是堆内存溢出
-
方法区:保存运行时常量池,类的元信息;大量加载类,或者使用动态代理大量生产类会导致方法区溢出
19、说说内存泄漏
不再使用的对象仍然被引用,导致gc无法将它回收;比如集合中的对象,如果使用完不主动清理会越来越多,最终导致OOM
解决:具体代码要具体分析,不过核心思路是将不再使用的对象释放掉;比如集合使用完就手动清空,对象使用完就取消引用等等
20、进程间的通信方式
-
管道(Pipe)与无名管道(Anonymous Pipe):
-
管道是一种半双工(单向数据流)的通信方式,通常用于具有亲缘关系的父子进程间通信。
-
无名管道是匿名的、临时的,只存在于内存中,由系统自动创建和销毁。
-
-
命名管道(Named Pipe):
-
存在于文件系统中管道
-
-
信号(Signal):
-
用于通知接收进程某个事件已经发生,常用于异常处理,如终止进程、挂起等。
-
-
消息队列(Message Queue):
-
消息队列允许进程异步地发送和接收数据块(消息)。可以实现进程间的同步和数据传递。
-
-
共享内存(Shared Memory):
-
允许多个进程访问同一块内存区域。为了同步对这块内存的访问,通常还需要配合信号量(Semaphore)或其他同步机制。
-
-
信号量(Semaphore):
-
信号量是一个计数器,用于控制多个进程对公共资源的访问。
-
-
套接字(Socket):
-
虽然最初是为了实现网络通信而设计的,但在同一台机器上的进程也可以使用套接字进行通信,支持TCP(面向连接)和UDP(无连接)通信。
-
21、进程跟线程的区别(为什么已经有了多进程,还要引入多线程?)
定义
-
进程是资源分配的最小单位,线程是CPU调度的最小单位;
-
线程依赖于进程而存在,一个进程可以包含多个线程。
独立性
-
进程间是独立的,没有资源共享(不相互调用的情况),可以独立的运行或结束,对其他进程没有任何影响。
-
同一个进程的线程之间存在资源共享,并且可以相互影响(一个线程出异常可能导致整个程序崩溃)。
上下文切换成本
-
进程的并发粒度高,上下文切换成本更高(占用资源多)。
-
线程更轻便,粒度低,更适合进行上下文切换。
通信效率
-
进程需要通过特殊的机制(管道,消息队列,共享内存等)来实现通信,实现复杂并且效率低。
-
进程内的线程共享同一内存空间,可以通过简单的锁机制和原子操作就实现同步和通信,效率高。
22、为什么多线程比单线程更快?
多线程可以并发执行,错开响应等待时间,充分利用CPU;
比如要执行多个相同任务:先要执行IO操作,然后运算得到结果。如果用单线程,按顺序一个一个的执行;但是多线程可以在一个任务进行IO的时候,处理上一次IO完成的任务的运算结果。
23、死锁的产生条件,如何避免
死锁是建立在并发之上的,有并发才有死锁
产生条件
1、互斥:资源的访问必须是互斥的
2、请求与保持:对于已获得的资源不会主动释放,对于未获得的资源会请求,如果没有,则阻塞
3、不剥夺:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
4、环路等待:发生死锁的时候必然存在资源请求的环路关系,比如:A需要B的资源,B需要C的资源,C需要A的资源
避免
对于互斥、请求与保持和不剥夺这三个条件是并发的必要条件,我们只能破坏环路等待去避免死锁。
24、事务的四大特性
ACID:
-
原子性:事务是最小的任务单元,不可分割
-
一致性:事务内的操作结果最终都必须和事务的结果保持一致
-
隔离性:事务在未提交之前所做的任何操作对其他事务是不可见的
-
持久性:事务在提交之后会对所做的操作在Mysql永久保存
25、Mysql并发事务会带来哪些问题
-
如果事务的四大特性全部满足的话会导致并发事务失去意义(并行执行和串行执行完全一致),降低了执行效率;因此Mysql破坏了事务的隔离性(其他的不好破坏),建立了一系列隔离级别来提升事务的执行效率
-
破坏了隔离性,会导致以下的问题
-
脏读:一个事务读到别的事务还没有提交的数据
-
不可重复读:在两次读数据之间,有其他事务提交导致两次读的结果不一样。
-
幻读:幻读的关键点在于查询结果集的不稳定性,它使得基于初次查询结果做出的后续操作决策变得不可靠。比如:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
-
-
Mysql的事务隔离级别
-
未提交读:什么都没解决
-
已提交读:解决了脏读
-
可重复读:解决了不可重复读和脏读
-
串行化:都解决了
-
26、说说Java里的锁升级机制
三种锁分别是重量级锁,轻量级锁和偏向锁。
JDK1.6之前Synchronized一直是重量级锁,在1.6之后引入了偏向锁和轻量级锁。
-
偏向锁:不存在锁竞争的时候,成功获取锁的线程会在偏向锁的对象头中使用CAS操作记录线程ID,线程获取这个偏向锁的只需检查ID是否一致,一致就直接获取成功,无需再进行CAS操作;不一致就升级为轻量级锁。(无竞争)
-
轻量级锁:偏向锁被另一个线程访问时,偏向锁升级为轻量级锁,没有成功获取锁的线程会自旋(一直尝试获取,直到成功)通过CAS操作尝试将对象头的标记字段设置为指向当前线程的锁记录。如果多个线程同时竞争且CAS失败,轻量级锁会进一步升级为重量级锁。(低竞争)
-
重量级锁:多个线程同时访问同一个轻量级锁时,轻量级锁会升级为重量级锁;阻塞获取失败的线程(高竞争)
JDK1.6以后默认情况下使用的是偏向锁,有竞争的时候会先升级为轻量级锁,如果竞争激烈会升级为重量级锁;轻量级锁和偏向锁都是为了解决在多线程环境下,减少不必要的同步开销,提高锁的获取与释放效率。这些锁状态的转换是自动进行的,并且一旦升级到重量级锁,就不会再降级回轻量级锁或偏向锁,以保持系统的稳定性和一致性。
27、讲讲Mysql中的索引
定义:Mysql中一中有序的,便于查询的数据结构
分类
-
按逻辑功能上划分
-
普通索引:针对要索引的字段没有任何约束
-
唯一索引:索引的字段值必须唯一,可以是null
-
主键索引:索引的字段必须为主键,只能存在一个主键索引
-
全文索引:通过倒排索引来对整个表的数据划分,高效解决字段是否包含问题,可以极大的提升搜索速度
-
-
按物理实现上划分
-
聚簇索引:索引的结果包含了整列数据,主键索引是天然的聚簇索引
-
非聚簇索引:索引的结果没有包含整列数据,需要二次索引出结果
-
-
字段个数划分
-
单列索引:索引的字段只有一个
-
组合索引:索引的字段有多个
-
结构
哈希索引:用哈希表来储存数据,查询效率为O(1),但是不便于范围查询
B+树索引:innodb引擎中使用的是B+树索引,底层结构是B+树,
-
有序的,自平衡的,多叉的,矮胖的树结构;减少磁盘IO
-
非叶子节点只保存指针,叶子节点保存实际的数据;读写代价更低
-
叶子节点间使用双向链表链接;便于范围查询和扫库
28、索引存在的问题
创建索引的原则
-
针对数据量特别大的表中经常查询、作为条件、分组、排序的字段才去创建
-
尽量使用联合索引
-
要控制索引的数量,对有索引的表每次插入和删除操作都需要进行索引重排,浪费资源,其次索引本身也占用空间
使用索引失效
-
违反最左前缀法则:使用联合索引中某个字段的时候不能越过这个字段之前的字段
-
范围查询右边的字段,索引会失效
-
在索引字段上使用算术运算
-
以百分号开头的模糊查询
-
索引字段如果是字符串,对应的值没加单引号
29、为什么mysql索引使用B+树而不用红黑树?
红黑树本质是一个二叉树,随着数据量的增加,层数也会迅速增加;B+树是多叉树,层数很稳定,一般在2-4左右;每访问一层会进行一次磁盘IO,层数越多,意味着磁盘IO次数越多。其次B+树的叶子节点使用双向链表连接,在做范围查询和扫库的时候可以进一步减少磁盘IO,同时效率更高。
30、Mysql主从同步是怎么实现的?
Mysql主从集群:主节点负责写操作,从节点负责读操作
主节点会将所有写操作的命令存放在binlog中,从库会开启一个线程来监听binlog的变化,一有变化就写入到从库的relay_log(中继日志)中,从库会执行relay_log的命令,然后清除relay_log中已经执行的命令。
31、Mysql中常用的存储引擎
Innodb:5.7之后的默认存储引擎;支持行级锁,事务,外键约束
MyISAM:早期的存储引擎;不支持事务和行级锁,多线程下进行读写操作会出问题;但是读写速度很快,适用于读数据较多的场景(多线程读数据不会出问题)
memory:数据都放在内存中,读写速度非常快,但是不支持数据持久化,Mysql关闭后数据全部丢失;适用于需要快速访问并且不要求持久化数据的小型表
32、解释一下Mysql中的redo_log和undo_log
-
redo_log:事务持久化的保证,记录所有已经提交的事务要进行的操作;在事务提交之后,会记录在内存中的专门开辟的一块redo_log_buffer(重做日志缓冲区)中,这块区域只负责写入数据到磁盘中的redo_log_file,避免来不及做持久化(因为内存中的公共缓存区的IO操作可能被其他线程占用了),然后发生问题导致数据丢失。
-
undo_log:回滚日志,事务一致性的保证;上面记录着与实际进行的操作完全相反的操作(比如:实际执行了插入操作,这上面会记录相应数据的删除操作;用于数据回滚),以及每一个操作之前的数据版本信息(MVCC数据版本链表)
33、解释一下MVCC
MVCC:多版本并发控制,解决Mysql在不同隔离级别下读数据时数据版本选择的问题
-
隐藏字段:上一个对数据进行修改的事务id;上一个数据版本的地址
-
undo_log版本链表:一条数据每次进行修改都会记录这两个隐藏字段,形成了一个版本链表。
-
readView:
-
Mysql中是根据readView的匹配规则(主要根据当前行的事务id和当前正在活跃的一些事务id)来决定读哪个版本的数据的。
-
不同隔离级别下生成readView的策略不同,RC级别下每次读数据都生成一次readView;RR级别下只在第一次读的时候生成readView,后续复用
-
34、Mysql中的锁有哪些?
-
锁的属性分类
-
共享锁(读锁):加共享锁的记录在释放所有共享锁之前只能加共享锁
-
排他锁(写锁):加排他锁的记录在释放该排他锁之前无法被加其他锁
-
-
锁的状态分类
-
意向共享锁:
-
意向排他锁
-
-
锁的粒度分类
-
表锁:锁住整张表,并发能力弱
-
页锁:锁住一些行,并发能力鉴于表锁和行锁之间
-
行锁:锁住一行或多行,并发能力强
-
记录锁:锁住事务中修改的某条记录
-
间隙锁:锁住表中下一条数据与该数据之间的记录,不包含该记录;如果没命中记录,会向上找到最近的一条记录加间隙锁
-
临建锁:锁住表中上一条数据与该数据之间的记录,包含该记录;如果没有命中记录,会向下找到最近的一条记录加临建锁
-
35、怎么定位慢查询?怎么优化的?
在Mysql配置文件中可以开启慢查询功能,默认是执行时间大于10秒的查询会被记录为慢查询,可以在日志文件中查看;
优化的思路:
-
一般先使用explain字段对查询语句解析;注意里边的key和key_length,检查有没有命中索引;然后看type字段,如果是index,说明使用了聚合索引,如果是all,说明是全表扫描,这两种情况都还有优化空间。
-
然后看语句本身,尽量避免使用select*,注明要返回的数据字段,尽可能的使用覆盖索引;左连接和右连接注意以小表为驱动,或者使用innerJoin代替左右连接;然后就是看有没有使用union,把union替换成union all,减少一次数据过滤。
-
如果还不行,那可能就是数据量太大了,分库分表。
36、主键索引,聚簇索引,覆盖索引的关系
概念上不同
主键索引:索引字段为主键,不能为null,字段值不能重复;在Mysql中主键索引叶子节点包含整个数据行
聚簇索引:叶子节点包含整个数据行的一类索引,主键索引属于聚簇索引。
覆盖索引:指的是一个索引包含了查询所需要的所有列的数据;不针对哪一种索引,强调的是查询语句中返回的数据字段是否包含在使用的索引的叶子节点中;聚簇索引一定是覆盖索引;对于不同的查询语句来说,任何索引都可以是覆盖索引。
37、java中的异常体系
顶级父类Throwable,下面有两个子类:Error和Exception
-
Error是程序无法处理的错误,一般是资源耗尽;一旦出现这个错误,整个程序将被迫停止运行。
-
Exception只影响出现异常的线程,不会影响整个程序;又分为两个部分RunTimeException运行时异常(运行时出问题)和CheckedException检查异常(编译期就报错)。
38、MySQL超大分页怎么处理?
描述:超大分页一般是指在数据量比较大的时候,我们使用了limit分页查询,并且需要对数据进行排序,这种情况下效率很低,我们可以使用覆盖索引和子查询来解决。
处理方案:先分页查询数据的id字段,来对id进行分页过滤处理;再根据过滤后的id查出相应数据;因为查询id的时候,走的是覆盖索引的流程,这样效率会提升很多。
39、如何定位慢查询
在调试阶段可以在Mysql中的配置文件里开启慢日志查询功能,一旦 sql 执行超过指定时间(默认为10秒)就会被记录到日志中,这个时间可以在配置文件中自行设置。
40、如何分析SQL语句?
用MySQL自带的分析工具,在SQL语句前添加EXPLAIN字段
会展示出来这条SQL的一些信息
其中重要字段
-
key和key_len 字段检查是否命中了索引;
-
type字段查看sql的类型;如果是index(复合索引)或者all(全表扫描),说明sql还存在优化的空间
-
通过 extra建议 判断是否出现了回表情况,如果出现了可以尝试添加索引或修改返回字段来修复