基础面试题
基础篇
一、面向对象三大特性
封装、继承、多态
二、集合
- ArrayList和LinkedList区别 :
- ArrayList基于动态数组实现。支持随机访问,但插入删除的代价很高,需要移动大量元素
- LinkedList基于双向链表实现。不支持随机访问,但插入删除方便,只需要改变指针
- HashMap实现原理 :
- HashMap的内部存储结构其实是数组+链表+树。当实例化一个HashMap时,会初始化默认长度(initialCapacity)和加载因子(loadFactor),系统会创建一个长度为默认长度(initialCapacity)的Node数组,这个长度在哈希表中被称为容量(Capacity),在这个数组中可以存放元素的位置称之为“桶”(bucket),每个桶(bucket)都有自己的索引,系统可以根据索引快速的查找桶(bucket)中的元素。
- 每个桶(bucket)中存储一个元素,即一个Node对象。每一个Node对象可以带一个引用变量next,用于指向下一个元素。因此在一个桶(bucket)中,就有可能生成一个Node链,也可能是一个个TreeNode对象,每个TreeNode对象可以有两个叶子节点left和right。因此在一个桶(bucket)中就有可能生成一个TreeNode树。新添加的元素作为链表的last或树的叶子节点。
- HashMap扩容 :当HashMap中的元素个数超过数组大小(initialCapacity)加载因子(loadFactor)时,就会进行数组扩容,加载因子(loadFactor)的默认值是0.75。默认情况下,数组大小(initialCapacity)为16,那么当HashMap中元素个数超过160.75=12(此值为代码中的threshold值,即临界值)时,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,这是一个非常消耗性能的操作,所以如果我们已经能预知HashMap中元素的个数,那么预设元素的个数能够有效的提高其性能。
- HashMap树形化 :当HashMap中其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决。若达到64,那么这个链会转化为树,节点类型由Node变为TreeNode。如果对象被删除后,下次resize方法时判断树的节点个数低于六个,则会把树转化为链表。
- JDK1.8相较于之前的变化
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
- 负载因子值的大小,对HashMap有什么影响?
- 负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
- 谈谈你对HashMap中put/get方法的认识?如果了解再谈谈HashMap的扩容机制?默认大小是多少?什么是负载因子(或填充比)?什么是吞吐临界值(或阈值、threshold)?
- put():
- 当put(k,v)时,创建一个长度为16的Node数组
- 首先按照k所在类的hashcode()计算其哈希值,用哈希值计算出k位于哪个桶
- 如果该桶为空,则直接插入
- 如果该桶不为空
- 如果该桶使用的是红黑树,则调用红黑树的方法插入
- 用链式方法插入。如果链的长度达到临界值,则把链表转化为红黑树
- 如果桶中存在重复的k,则为该k替换新值
- 如果size大于阈值,则进行扩容
- get():
- 通过哈希值找到该k映射的桶
- 桶上的k就是要查找的k,直接命中
- 桶上的 key 不是要查找的 key,则查看后续节点:
- 如果后续节点是树节点,通过调用树的方法查找该 key
- 如果后续节点是链式节点,则通过循环遍历链查找该 key
- 扩容机制 :默认容量为16,负载因子为0.75,如果使用容量超过16×0.75=12(阈值)时,进行扩容,扩容至一倍(2×16=32),并重新计算每个元素的位置
- put():
- 创建HashSet会创建一个初始值为16,负载因子为0.75的HashMap,key为add()的值,value为Object类型常量
- HashMap和HashTable区别
- HashTable中的方法是synchronized的
- HashMap可以插入键为null,HashTable不可以
- HashMap不能保证对着时间的推移Map中的元素次序是不变的
- HashTable直接使用对象的hashCode,HashMap要重新计算
- AVL和红黑树的区别
- AVL是严格的平衡树,在增加或删除节点的时候,旋转的次数比红黑树要多。红黑树用非严格的平衡来减少旋转次数
- 红黑树根节点是黑色,叶子节点都为黑色且空,每一个红色节点的两个子节点都是黑色
- AVL靠平衡因子旋转,红黑树靠节点颜色以及一些约定旋转
- AVL更适合搜索,红黑树更适合增删
并发篇
一、java如何开启线程?如何保证线程安全?
- 线程和进程的区别 :进程是操作系统进行资源分配的最小单元,线程是操作系统进行任务分配的最小单元,线程属于进程
- 进程拥有资源而线程不拥有,线程可以访问隶属进程的资源
- 线程是独立调度的基本单位,同一进程中线程切换不会引起进程切换,不通进程中的线程切换会引起进程切换
- 创建切换进程会产生极大的系统开销(内存、I/O、CPU),而线程不会(只需设置和保存少量寄存器)
- 线程可以通过同一进程中的数据进行通信,进程间通信需要借助IPC
- 进程间通信方式:
- 管道通信: 一种半双工的通信方式,数据只能单向流动,且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常指父子进程关系
- 命名管道通信:与管道通信相似,但是它允许无亲缘关系进程间的通信
- 信号量: 是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止多个进程访问同一资源。主要作为进程间以及不同线程之间的同步手段
- 消息队列:由消息产生的链表,存放在内核中并由消息队列标识符标识。其克服了信号量传递少、管道只能承载无格式字节流以及缓冲区受限等缺点
- 共享内存:就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程可以访问。共享内存是最快的IPC方式,它使很对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制来配合使用来实现进程间的同步和通信
- 套接字:可以用于本地进程间通信和不同主机进程间通信
- 线程通信方式:
- 共享内存:线程之间通过读写内存中的公共状态来隐式通信
- 消息传递:线程之间没有公共状态,通过明确的发送信息来显示的进行通信
- 管道流
- 开启线程4种方式:
- 继承Thread类,重写run方法
- 实现Runnable接口,实现run方法
- 实现Callable接口,实现call方法
- 创建线程池
- 保证线程安全:加锁
- JVM提供的锁:Synchronized关键字
- JDK提供的锁:Lock
- 线程的状态
- 新建:创建一个线程但还没有调用start方法
- 就绪:调用start方法,进入队列等待cpu时间片,具备运行条件,没有被分配到cpu资源
- 运行:就绪的线程获取到cpu资源
- 阻塞:遇到锁或人为挂起时,让cpu临时中止自己的执行
- 死亡:线程完成了它的任务或被提前强制性中止或出现异常导致结束
二、Volatile和Synchronized有什么区别? Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加Volatile?
- Synchronized用来加锁,Volatile保证变量的内存可见性
- 不能。Volatile不能保证原子性
- Volatile可以防止指令重排
三、JAVA线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?
- JAVA的锁就是在对象头中记录一个锁状态
- JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程
- synchronized 锁升级原理: 在锁对象的对象头里面有一个 threadid 字段,在第一次访问 的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进 入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象, 如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之 后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程 就构成了 synchronized 锁的升级。 锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗
四、谈谈你对AQS的理解。AQS如何实现可重入锁?
- AQS(抽象队列同步器)是一个JAVA线程同步框架,是JDK中很多锁工具的核心实现框架。在AQS中,维护了一个信号量state和一个线程组成的双向链表。其线程队列就是用来给线程排队的,而state就像是一个红绿灯来控制线程等待或放行。在不同的场景下有不同的意义。
- 在可重入锁下,state用来标识加锁的次数。0表示无锁,每加一次锁state加1,每释放一次state减1。
五、CAS是什么?CAS底层原理?为什么不用Synchronized而用CAS?CAS缺点?ABA问题?
- CAS(CompareAndSwap,比较并交换) :是一条CPU并发原语,功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的
- 底层原理: JAVA的CAS操作都依赖UnSafe类中的方法,UnSafe时CAS的核心类,其中含有大量的native方法,可以直接调用操作系统底层资源操作特定内存中的数据
- 比较: Synchronized只允许有一个线程访问,保证一致性但没有提高并发性,而CAS既保证了一致性,又提高了并发性
- CAS缺点:
- 循环时间长,CPU开销大
- 只能保证一个共享变量的原子操作
- ABA问题: 因为 CAS 算法是在某一时刻取出内存值然后在当前的时刻进行比较,中间存在一个时间差,在这个时间差里就可能会产生 ABA 问题。
- 当有两个线程 T1 和 T2 从内存中获取到值A,线程 T2 通过某些操作把内存 值修改为B,然后又经过某些操作将值修改为A,T2退出。线程 T1 进行操作的时候 ,使用预期值同内存中的值比较,此时均为A,修改成功退出。但是此时的A以及不是原先的A了
- 解决ABA问题:
- AtomicReference类,原子引用
- AtomicStampedReference,时间戳原子引用
六、集合类不安全,ConcurrentHashMap原理?
- 故障现象: java.util.ConcurrentModificationException
- 解决方案: 以ArrayList为例
- new Vector
- Collections.synchronizedList()
- CopyOnWriteArrayList(),写时拷贝,(map是ConcurrentHashMap(其原理是使用了分段锁))
七、各种锁
- 公平锁与非公平锁: ReentrantLock(默认非公平锁)
- 公平锁: 按申请锁的顺序来获取锁。排队,先来后到
- 非公平锁: 后申请锁的线程有可能比先申请的线程优先获取锁。高并发下可能出现优先级反转或饥饿现象
- 可重入锁(递归锁) :A同步方法中调用同步方法B,B也可以获得A的锁,A和B拥有同一把锁
- Synchronized和ReentrantLock都是可重入锁
- 自旋锁(SpinLock): 尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁
- 独占(写)锁: 该锁只能被一个线程所持有
- 共享(读)锁: 该锁可被多个线程所持有
八、CountDownLatch、CyclicBarrier、Semaphore、LockSupport
- CountDownLatch: 一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信。它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务
- CyclicBarrier: 与CountDownLatch相反。初始值为0,达到要求后才可以恢复执行接下来的任务
- Semaphore:
- 用于多个共享资源的互斥(抢车位)
- 用于并发线程数的控制
- LockSupport: 用来创建锁和其他同步类的基本线程阻塞原语,它使用了一种许可证的概念来做到阻塞和唤醒线程的功能,许可证最多有一个。其park()与wait()类似,unpark()与notify()类似
九、阻塞队列?
- 当队列为空时,从队列中获取元素的操作将会被阻塞;当队列为满时,往队列里添加元素的操作将会被阻塞
- 优点: 不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程
- BlockingQueue接口实现Collection接口,与List平级
- ArrayBlockingQueue:由数组构成的有界阻塞队列
- LinkedBlockingQueue:由链表构成的有界(但大小默认为Integer.MaxValue)阻塞队列
- synchronousQueue:只存储单个元素的队列
十、Synchronized和Lock有什么区别?
- 原始构成
- synchronized是关键字,属于JVM层面
- Lock是具体类,属于API层面
- 使用方法
- synchronized不需要手动释放,当其执行完后系统会让线程释放对锁的占用
- Lock需要用户去手动释放锁。若没有释放,可能会出现死锁
- 等待是否可中断
- synchronized除非抛异常或者运行完成,否则不可中断
- Lock可中断。可设置超时方法
- 加锁是否公平
- synchronized非公平锁
- Lock两种都可
- 锁绑定多个条件Condition
- synchronized没有
- Lock可精确唤醒,而不是像synchronized要么唤醒一个,要么唤醒全部
十一、线程池
- 线程池优势: 降低资源消耗,提高响应速度,提高线程的可管理性
- 线程池7大参数
- 核心线程数
- 最大线程数: 必须大于1
- 存活时间: 当前线程数大于核心线程数、空闲时间达到存活时间时,多余的线程会被销毁
- 时间单位
- 阻塞队列: 被提交但未被执行的任务
- 创建工厂 :创建工作线程的工厂。一般默认
- 拒绝策略: 当队列满了并且工作线程等于最大线程数时如何拒绝后来的任务
- AbortPolicy(默认):抛异常阻止运行
- CallRunSPolicy:将某些任务回退到调用者执行
- DiscardOldestPolicy:抛弃队列中等待最久的任务,将当前任务加入到队列中
- DiscardPolicy:直接丢弃任务(允许任务丢失的最好方案)
- 线程池的底层工作原理
- 小于核心线程数,立即运行此任务
- 大于等于核心线程数,放入队列等待
- 队列满且小于最大线程数,创建非核心线程立即运行此任务
- 等于最大线程数,启用拒绝策略
- 空闲线程超过存活时间时,判断大于核心线程数,将其销毁
- 线程池完成所有任务后会收缩到核心线程池的大小
- 线程池合理配置参数
- CPU密集型: CPU核数 + 1
- IO密集型
- CPU核数 * 2
- CPU核数/(1 - 阻塞系数) 阻塞系数0.8-0.9
- 死锁检查: jps -l -> jstack 进程号
网络篇
一、TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?
- TCP(Transfer Control Protocol) 是一种面向连接的、可靠的传输层通信协议
- 特点 :面向连接;点对点;高可靠的;效率较低;占用系统资源较多
- UDP(User Datagram Protocol) 是一种无连接的、不可靠的传输层通信协议
- 特点 :不需要链接;可进行广播发送;传输不可靠,有可能丢失消息;效率较高;占用系统资源较少
- TCP三次握手过程:
- 服务端一直处于监听状态,等待客户端的连接请求
- 第握手一次: 客户端向服务端发送连接请求报文。SYN=1,ACK=0,初始序号x
- 第二次握手: 服务端收到请求报文,如果同意建立连接,则向客户端发送确认报文。SYN=1,ACK=1,确认号x+1,初始序号y(客户端发送功能正常,服务端接收功能正常)
- 第三次握手: 客户端收到服务端的确认报文,也要想服务端进行确认。ACK=1,x+1,y+1(服务端发送功能正常,客户端接收功能正常)
- TCP四次挥手过程:
- 第一次挥手: 客户端发送释放连接报文。FIN=1,初始序号u
- 第二次挥手: 服务端收到释放报文并向客户端发送确认报文(此时TCP处于半关闭状态)。ACK=1,u+1,初始序号v
- 服务端进入CLOSE-WAIT状态, 服务端将未传输完的数据继续传输
- 第三次挥手: 服务端传输完数据,向客户端发出释放连接报文。FIN=1,ACK=1,u+1,初始序号w
- 第四次挥手: 客户端收到释放报文并向服务端发送确认报文。u+1,w+1
- 客户端进入TIME-WAIT状态, 等待2MSL时间再进入CLOSE状态。
- 确认发送的最后一段报文能到达服务端。如果服务端没收到客户端的确认报文,服务端就会重新向客户端发送释放连接报文
- 让本连接持续时间内所产生的报文都从网络中消失,使下一个新的连接不会出现旧连接报文
二、JAVA有哪几种IO模型?有什么区别?
BIO 同步阻塞IO: 用户线程发起IO读/写操作之后,线程阻塞,直到可以开始处理数据
- 可靠性差,吞吐量低,适用于较少且比较固定的场景
Client------> Thread <----- | Client------> Thread <-----------Server | Client------> Thread <-----
- 可靠性差,吞吐量低,适用于较少且比较固定的场景
NIO 同步非阻塞IO: 发起IO请求之后可以立即返回,如果没有就绪的数据,需要不断地发起IO请求直到数据就绪
- 可靠性较好,吞吐量较高,适用于连接较多且连接较短(轻操作),编程模型最复杂
Client---- ↓ Client----> selector---> Thread <----Server ↑ Client----
- 可靠性较好,吞吐量较高,适用于连接较多且连接较短(轻操作),编程模型最复杂
AIO 异步非阻塞IO: 用户线程发出IO请求之后,继续执行,由内核进行数据的读取并放在用户指定的缓冲区内,在IO完成之后通知用户线程直接使用。
- 可靠性最好,吞吐量最高,适用于连接较多且连接较长(重操作),编程模型较简单,需要操作系统支持
|→Client---- | ↓ |→Client----> selector---> Thread <----Server | ↑ | |→Client---- | |_______________________________________↙
- 可靠性最好,吞吐量最高,适用于连接较多且连接较长(重操作),编程模型较简单,需要操作系统支持
三、JAVA NIO的几个核心组件是什么?分别有什么作用?
- Buffer(缓冲区): 缓冲区的出现导致了NIO和BIO 的不同。读数据时可以先读一部分到缓冲区中,然后处理其他事情;写数据时可以先写一部分到缓冲区中,然后处理其他事情。读和写可以不再持续,所以不会阻塞。当缓冲区满后才会将其真正的进入读写
- Channel: NIO的所有操作都从Channle开始。
- 从通道进行数据读取:创建一个缓冲区,然后请求通道读取数据
- 从通道进行数据写入:创建一个缓冲区,填充数据,并要求通道写入数据
- Selector: 选择器可以让单个线程处理多个通道,达到复用的目的
四、描述下HTTP和HTTPS的区别
- HTTP: 是互联网上应用最广泛的以中网络通信协议,基于TCP协议,可以使浏览器工作更高效,减少网络传输
- 状态码
- 1xx(信息性状态码): 接受的请求正在处理
- 2xx(成功状态码): 请求正常处理完毕
- 3xx(重定向状态码): 需要进行附加的操作来完成请求
- 4xx(客户端错误状态码): 服务器无法处理请求
- 5xx(服务器错误状态码): 服务器内部错误
- 状态码
- HTTPS: 是HTTP的加强版,可以认为是HTTP+SSL(Secure Socket Layer)。在HTTP的基础上增加了一系列的安全机制。一方面保证数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案
- 区别:
- HTTP的连接是简单无状态的;HTTPS的数据传输时经过证书加密的,安全性更高
- HTTP是免费的;HTTPS需要申请证书,证书是要收费的
- 它们的传输协议不同,所以端口也不同,HTTP默认:80 HTTPS默认:443
- 区别:
- HTTPS的缺点:
- HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量
- 功能越强大的证书费用越高
五、计算机网络体系结构
- 应用层: 为特定应用程序提供数据传输服务,如HTTP、DNS等协议。数据单位为报文。
- 表示层: 负责数据格式的转换。将应用处理的信息转换为适合网络传输的格式。
- 会话层: 自动发包,自动寻址。建立和管理应用程序之间的通讯
- 传输层: 为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整***,UDP 主要提供及时***。
- 网络层: 为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。
- 数据链路层: 网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。
- 物理层: 考虑的是怎样在传输媒体上传数据比特流,而不是指具体的传输媒体。作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
- TCP/IP只有四层,相当与五层协议中数据链路层和物理层合并为网络接口层。它不严格遵循OSI分层概念,应用层可能会直接使用IP层或者网络接口层
六、访问一个url涉及到了那些东西?
- 到DNS服务器将url解析为IP地址
- 得到IP地址后使用TCP协议进行连接
- 连接完成后发起HTTP请求
- 服务器响应HTTP请求
- 浏览器解析代码并请求资源
- 断开TCP连接
- 浏览器渲染页面
JVM篇
一、JVM的内存模型
- 程序计数器
- 本地方法栈
- 虚拟机栈: 栈帧(最小单位):局部变量表、操作数栈、方法返回地址、动态链接、一些附加信息
- 堆: 新生代(Eden区、s0、s1)、老年代
- 方法区: 永久代←1.8→元空间
二、JAVA类加载的全过程是怎样的?什么是双亲委派机制?有什么作用?一个对象从加载到JVM,再到被GC清除,都经历了什么过程?
- 全过程
- 加载阶段(引导类加载器(BootStrap ClassLoader)→扩展类加载器(Extension ClassLoader)→系统类加载器(Application ClassLoader))
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将该字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的Class对象,作为方法区中该类各种数据的访问入口
- 链接阶段
- 验证: 确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
- 准备: 为类变量(不包含用final修饰的static变量)分配内存并且设置该类变量的默认值
- 解析: 将常量池的符号引用替换为直接引用的过程。往往在初始化阶段之后再开始
- 初始化阶段: 执行类构造器方法<clinit>()的过程</clinit>
- 加载阶段(引导类加载器(BootStrap ClassLoader)→扩展类加载器(Extension ClassLoader)→系统类加载器(Application ClassLoader))
- 双亲委派机制
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器还存在其父类加载器,则进一步向上委托,请求最终将到达顶层的引导类加载器
- 如果父类加载器可以完成类加载任务,就成功返回,若无法完成此加载任务,子加载器才会尝试自己去加载
- 作用:
- 避免类的重复加载
- 保护程序安全,防止核心API被随意篡改
- 如何打破双亲委派机制: 自定义一个类加载器,继承ClassLoader类;重写findClass()和loadClass()
- 过程:
- JVM首先需要到方法区找到对象的类型信息,创建对象
- JVM要实例化一个对象,会在堆中创建一个对象
- 对象首先分配到Eden区,经过一次Minor GC,对象存活就会进入S区。如果对象经历GC一直存活,就会在S区来回拷贝,每移动一次,年龄就会加一。当年龄到15时,就会 将对象转入老年代。
- 当方法执行结束后,栈中的指针会被移除掉
- 对象经过Full GC后会被垃圾回收器回收
三、怎么确定一个对象到底是不是垃圾?什么是GC Root?
- 引用计数法: 每个对象都有一个计数器,如果计数器为0,则这个对象是垃圾。它无法解决循环引用的问题(内存泄漏)
- 可达性分析法: 以GC Roots为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达,如果不可达,则被认为是垃圾
- GC Root:
- 虚拟机栈中引用的对象
- 本地方法栈内引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁synchronized持有的对象
- 虚拟机内部的引用
四、JVM有哪些垃圾回收算法
- 标记-清除: 使用可达性分析算法将可达的对象标记起来,然后回收不可达的对象(会产生大量的内存碎片)
- 复制: 将内存分为两份,每次只使用一份,回收时将可达的对象依次复制到另一份中,回收当前份内存(空间浪费,效率与存活对象的个数有关)
- 标记-压缩: 先完成一次标记清楚算法后,再进行一次内存碎片整理
五、JVM有哪些垃圾回收器?他们都是怎么工作的?什么是STW?他都发生在哪些阶段?什么是三色标记?为什么要设计这么多的垃圾回收器?
- STW: Stop The World。在垃圾回收执行过程中,需要暂停用户线程使垃圾回收线程运行
- 垃圾回收器
- Serial( 复制 ) - Serial Old( 标整 ) 串行
- ParNew( 复制 ) - CMS( 标清 )
- Parallel( 复制 ) - Parallel Old( 标整 ) JDK1.8默认
- G1( 整体标整,局部复制 ) JDK1.9默认
- ZGC
- CMS垃圾回收器工作原理: 耗时最长为②④步,总体来看为并发执行
- 初始标记: 标记GCRoots能直接关联的对象,触发STW
- 并发标记: 进行GCRoots的跟踪过程,和用户线程一起工作,主要标记过程
- 重新标记: 修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,触发STW
- 并发清除: 清除垃圾
- CMS垃圾回收器工作原理: 耗时最长为②④步,总体来看为并发执行
- 三色标记: CMS核心算法,将每个对象分成三种颜色
- 黑色: 表示自己和成员变量都已经标记完成
- 灰色: 自己标记完成,成员变量标记未完成
- 白色: 自己未标记完成
- 由于内存越来越大,所以需要更强大的垃圾回收器来管理内存
六、如何进行JVM调优?JVM参数有哪些?怎么查看一个JAVA进程的JVM参数?谈谈你了解的JVM参数。
- JVM调优主要是通过定制JVM运行参数来提高应用程序的运行速度
- JVM参数:
- -开头: 标准指令,HotSpot支持的参数,java -help打印
- -X开头: 非标准指令,与特定HotSpot版本对应,java -X打印
- -Xms512m:设置堆初始值
- -Xmn512m:新生代内存配置
- -Xss256k:每个线程栈最大值
- -XX开头: 不稳定参数,与特定HotSpot版本对应且变化非常大
- -XX+PrintCommandLineFlags:查看当前命令的不稳定指令
- -XX+PrintFlagsInitial:查看所有不稳定指令的默认值
- -XX+PrintFlagsFinal:查看所有不稳定指令最终生效的实际值
- -XX:+UseG1GC:开启G1垃圾收集器
- -XX:-UseG1GC:关闭G1垃圾收集器
七、OOM相关
- SOFE StackOverFlowError: 栈溢出。递归。是错误
- OOM java heap space: 堆内存不足
- OOM GC overhead limit exceeded: 大量资源在进行垃圾回收且回收很少
- OOM Direct buffer memory: NIO写入本地内存而不是堆内存,本地内存可能用光
- OOM unable to create new native thread: 一个进程创建多个线程,超过系统承载极限。服务器不允许应用创建这么多线程
- OOM Metaspace: 元空间内存不足
MQ篇
一、MQ有什么用?有哪些具体的使用场景
- MQ: 消息队列。队列是一种FIFO(先进先出)的数据结构。消息由生产者发送到MQ进行排队,然后由消费者对消息进行处理
- 作用:
- 异步: 提高系统的响应速度和吞吐量
- ** 解耦:** 减少服务之间的影响,提高系统的稳定性和可扩展性。解耦之后可以实现数据分发,生产者发送一个消息后,可以由多个消费者来处理
- 削峰: 以稳定的系统资源应对突发的流量冲击
- 缺点:
- 系统可用性降低: 一旦MQ宕机,整个业务就会产生影响(高可用)
- 系统的复杂度提高: 消息丢失,消息重复调用,消息乱序
- 数据一致性: 两个系统一同处理消息,处理结果不同,则数据不一致
二、 如何进行产品选型?
- ** Kafaka:**
- 优点:吞吐量非常大,性能非常好,集群高可用
- 缺点:有可能会丢数据,功能比较单一
- 使用场景:日志分析,大数据采集
- RabbitMQ:
- 优点:消息可靠性高,功能全面
- 缺点:吞吐量比较低,消息积累会严重影响性能,语言不好定制
- 使用场景:小规模场景
- RocketMQ
- 优点:较高吞吐,较高性能,高可用,功能非常全面
- 缺点:开源版功能不如云上商业版。官方文档和周边生态还不够成熟。客户端只支持JAVA
- 使用场景:几乎是全场景
三、如何保证消息不丢失?
- 生产者发送消息不丢失
- Kafka:消息发送 + 回调
- RocketMQ:消息发送 + 回调 + 事务
- RabbitMQ:消息发送 + 回调 + 事务(channel手动事务(可能会产生阻塞)) + Publisher Confirm(与RocketMQ事务机制基本基本相同)
- MQ主从消息同步不丢失
- RocketMQ:
- 普通集群:同步同步、异步同步。前者不会丢消息,后者效率更高
- Dledger集群-两阶段提交
- RabbitMQ:
- 普通集群:消息分散存储,节点之间不会主动进行消息同步,可能丢消息
- 镜像集群:会在节点之间主动进行数据同步,数据安全性得到提高
- Kafka:通常用在允许消息少量丢失的场景中
- MQ消息存盘不丢失
- RocketMQ:同步刷盘、异步刷盘,前者不会丢消息,后者效率更高
- RabbitMQ:队列配置为持久化队列。新增Quorum类型的队列,采用Raft协议来进行消息同步
- MQ消费者消费消息不丢失
- RocketMQ:使用默认的方式消费,不采用异步
- RabbitMQ:关闭AutoCommit -> 手动提交offset
- Kafka:手动提交offset
四、如何保证消息消费的幂等性?
- 幂等性: 对于同一个系统,在同样条件下,一次请求和重复多次请求对资源的影响是一致的
- 消息消费幂等性: 防止消费者重复消费消息
- 所有MQ产品并没有提供主动解决幂等性的机制,需要有消费者自行控制
- RocketMQ:给每个消息分配了个MessageID(不太建议),建议使用有业务标识的ID
五、如何保证消息的顺序?
MQ只需要保证局部有序,不需要保证全局有序
RocketMQ中有完整的设计:生产者把一组有序的消息放到同一个队列当中,消费者一次消费整个队列中的消息
RabbitMQ:实现上述方式。保证目标Exchange只对应一个队列,一个队列只对应一个消费者
Kafka:实现上述方式。生产者通过Partition分配规则,将消息分配到同一个Partition。Topic下只有一个消费者
六、如何保证消息的高效读写?
Kafka和RocketMQ都是通过零拷贝技术来优化文件读写
零拷贝:
- mmap(JDK中MappedByteBuffer):文件不经过用户空间,直接在内核空间完成拷贝。适合比较小的文件
- transfile(JDK中FileChannel):文件传输过程中直接使用DMA
RocketMQ: mmap(commitlog)
Kafka: 在index日志文件中通过mmap方式,使用transfile方式将硬盘数据加载到网卡
七、使用MQ如何保证分布式事务的最终一致性?
分布式事务: 业务相关的多个操作,保证其同时成功或失败
最终一致性: 系统中的所有分散在不同节点的数据,经过一定时间后,最终能够达到符合业务定义的一致的状态。
保证:
- 生产者要保证100%的消息投递
- 消费者需要保证幂等性消费
八、如何自己设计一个MQ?
从整体到细节,从业务场景到技术实现;以RocketMQ为基础
- 实现一个单机的队列数据结构。高效,可扩展
- 将单机队列扩展分布式队列。分布式集群管理
- 基于Topic定制消息路由策略。发送者路由策略,消费者与队列对应关系,消费者路由策略
- 实现高效的网络通信
- 规划日志文件,实现文件高效读写
- 定制高级功能:死信队列,延迟队列,事务消息
缓存
一、为什么使用缓存?
高性能、高并发
二、什么是缓存穿透、缓存击穿、缓存雪崩?怎么解决?
- 缓存穿透: 数据在缓存中查不到,数据库也查不到
- 对参数进行合法性校验
- 将数据库没查到结果的数据也写入到缓存(短的有效期)
- 布隆过滤器(存在一定误判率),在访问Redis之前判断数据是否存在
- 缓存击穿: 缓存单个key失效,去查数据库,数据库瞬间压力增大
- 设置热点数据永不过期
- 加互斥锁
- 缓存雪崩: 大面积缓存失效,都去请求数据库,造成数据库短时间内承受大量请求而挂掉
- 将缓存的失效时间分散开(加随机值)
- redis高可用
- 限流降级
- 数据预热
三、如何保证Redis与数据库的数据一致?
- 双写模式: 先写数据库,再写缓存
- 失效模式: 先写数据库,在删除缓存
- 最终一致性
四、如何设计一个分布式锁?如何对锁性能进行优化?
- 本质: 在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程来竞争锁资源。响应快、性能高、与业务无关
- Redis实现分布式锁
- SETNX k v
- EXPIRE k locktime
- DEL k
- GETSET k v
- synchronized单机版锁。在分布式下不ok
- 使用redis的SETNX达成分布式锁
- 出现异常无法释放锁。在finally中释放锁
- 宕机后,代码没走到finally。设置锁的过期时间
- SETNX操作和设置过期时间必须为原子操作
- 只能删除自己的锁,使用lua脚本或redis事务
- 判断锁所属业务与删除锁也必须是原子操作
- redis集群环境下,使用Redisson
五、Redis如何设置Key的过期时间?它的实现原理是什么?Redis默认内存大小
- EXPIRE SETNX
- 实现原理(两种结合)
- 定期删除: 默认每隔100ms随机抽取设置了过期时间的key,判断其是否过期,过期就删除
- 惰性删除: key过期后,下一次用到它时再删除
- 没有配置默认最大内存, 生产上推荐设置为最大物理内存的四分之三。当达到最大内存时,Redis会报OOM
六、海量数据下,如何快速查找一条记录?
- 使用布隆过滤器,快速过滤不存在的记录
- 在Redis中建立缓存(以Hash来存储)
七、五大基本数据类型
- string
- 应用场景:incr items 商品编号、订单号、是否喜欢
- 数据结构:简单动态字符串(SDS)。是可以修改的字符串,内部结构类似于ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配
- list
- 应用场景:微信公众号
- 数据结构:快速链表(quicklist)。在列表元素较少的情况下会使ziplist,即压缩列表。他将所有的元素紧挨着一起存储,分配的是一块连续的内存。数据量较多时改为quicklist。Redis将链表和ziplist结合起来组成quicklist。也就是将多个ziplist使用双向指针串起来。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余
- hash:Map<String,Map<Object,Object>>
- 应用场景:购物车早期
- 数据结构:与HashMap相似,通过数组+链表的链地址法来解决部分哈希冲突。hash结构的内部包含两个hashtable,通常情况下只有一个hashtable是有值的,但是在字典扩容时需要分配新的hashtable,然后进行渐进式搬迁(rehash)。渐进式rehash会在rehash同时保留新旧两个hash结构,查询时会同时查询两个hash,然后在后续的定时任务以及hash操作指令中,循序渐进的把旧hash的内容迁移到新hash中。当搬迁完成后,新hash取代旧hash
- set:HashSet 无序无重复
- 应用场景:微信抽奖小程序,微信朋友圈点赞,抖音共同关注、可能认识的人(交并差集)
- 数据结构:与HashSet相似,内部键值对是无序且唯一的。内部实现一个特殊的hash,其中所有的v指都是null
- zset
- 应用场景:商品排序,热搜热度
- 数据结构:数据少时,使用ziplist,每个元素都是(数据+score)的方式连续存储,按照score从小到大排序。数据多时,使用hash+跳表。hash用来根据数据查score,调表用来根据score查数据。跳表是基于一条有序单链表构造的,通过构建索引提高查找效率,空间换时间,查找方式是从最上面的链表层层往下查找,最后在最底层的链表找到对应节点
八、LRU算法
最近最少使用,常见的页面置换算法。选择最近最久未使用的数据予以淘汰
//巧用LinkedHashMap完成 class LRUCache<k,v> extends LinkedHashMap<k,v> { private int capacity; public LRUCache(int capacity) { super(capacity,0.75,true); this.capacity = capacity; } protected boolean removeEldestEntry(Map.Entry<k,v> eldest){ return super.size > capacity; } }
public class LRUCache { /** * 146. LRU 缓存机制 * 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。 * 实现 LRUCache 类: * LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 * int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 * void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则 * 插入该组「关键字-值」。 * 当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出 * 空间。 * 进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作? */ //map负责查找,构建一个虚拟的双向链表,里面是一个个Node节点作为数据载体 //1.构造Node节点作为数据载体 class Node<K, V> { K key; V value; Node<K, V> prev; Node<K, V> next; public Node() { this.prev = this.next = null; } public Node(K key, V value) { this.key = key; this.value = value; this.prev = this.next = null; } } //2.构造一个虚拟的双向链表,里面放Node class DoubleLinkedList<K,V>{ Node<K,V> head; Node<K,V> tail; //2.1 构造方法 public DoubleLinkedList(){ head = new Node<>(); tail = new Node<>(); head.next = tail; //头节点下一个节点指向尾节点 tail.prev = head; //尾节点上一个节点指向头节点 } //2.2 添加节点 public void addHead(Node<K,V> node){ node.next = head.next; //当前节点下一个节点指向尾节点 node.prev = head; //当前节点上一个节点指向头节点 head.next.prev = node; //尾节点上一个节点指向当前节点 head.next = node; //头节点下一个节点指向当前节点 } //2.3 删除节点 public void remove(Node<K,V> node){ node.next.prev = node.prev; //尾节点上一个节点指向头节点 node.prev.next = node.next; //头节点下一个节点指向尾节点 node.prev = null; //当前节点上一个节点置空 node.next = null; //当前节点下一个节点置空 } //2.4 获取最后一个节点 public Node<K,V> getLast(){ return tail.prev; } } private int capacity; Map<Integer,Node<Integer,Integer>> map; DoubleLinkedList<Integer,Integer> list; public LRUCache(int capacity) { this.capacity = capacity; map = new HashMap<>(); list = new DoubleLinkedList<>(); } public int get(int key) { if (!map.containsKey(key)) return -1; Node<Integer, Integer> node = map.get(key); list.remove(node); list.addHead(node); return node.value; } public void put(int key, int value) { if (map.containsKey(key)){ Node<Integer, Integer> node = map.get(key); node.value = value; map.put(key,node); list.remove(node); list.addHead(node); }else { if (map.size() == capacity){ Node<Integer, Integer> lastNode = list.getLast(); map.remove(lastNode.key); list.remove(lastNode); } Node<Integer, Integer> newNode = new Node<>(key, value); map.put(key,newNode); list.addHead(newNode); } } }
微服务篇
一、谈谈你对微服务的理解,微服务有哪些优缺点?
- 微服务是一种架构风格,通过将单体的应用划分为较小的服务单元,从而降低整个系统的复杂度
- 优点:
- 服务部署更灵活:每个应用都可以是一个独立的项目,可以独立部署,耦合性低
- 技术更新灵活:微服务可以根据业务特点,灵活选择技术栈
- 应用性能高:一个应用可以部署到多个服务器
- 代码复用:很多底层服务可以以REST API的方式对外提供统一的服务,所以基础服务可以在整个微服务中通用
- 缺点:
- 服务调用的复杂性提高:网络问题,容错问题,负载问题,高并发问题
- 分布式事务:尽量不要使用
- 测试难度提升
- 运维难度提升
二、SpringCloud和SpringCloudAlibaba都有哪些组件?都解决了什么问题?
- SpringCloud: 是提供构建微服务系统多需要的一组通用开发模式以及一系列快速实现这些开发模式的工具
- Eureka: 服务注册与发现
- Hytrix: 服务熔断降级
- Feign - Ribbon: 远程服务调用
- OpenFeign: 远程服务调用
- Zuul: 网关路由
- Config: 配置中心
- SpringCloudAlibaba:
- Sentinel: 服务熔断降级
- Nacos: 注册中心和配置中心
- Stream RocketMQ: 消息中间件整合
- Seata: 分布式事务
三、分布式事务如何处理?怎么保证事务一致性?
- 分布式事务: 不同节点的实务操作提供操作原子性保证。在原本没有直接关联的事务之间建立联系,
- HTTP: 最大努力通知,事后补偿
- MQ: 事务消息机制
- Seata: 通过TC来在多个事务之间建立联系
- 两阶段: AT XA (锁资源)
- 三阶段: TCC (在两阶段前增加一个准备阶段,不锁资源)
- SAGA: 类似于熔断,业务自己实现正向操作和补偿操作的逻辑
四、怎么拆分微服务?怎么样设计出高内聚,低耦合的微服务?有没有了解过DDD领域驱动设计?什么是中台?中台和微服务有什么关系?
- 拆分微服务的基本准则:
- 微服务之间尽量不要有业务交叉
- 微服务之间只能通过接口进行调用
- 高内聚,低耦合
- 高内聚低耦合是一种从上到下指导微服务设计的方法,实现其工具主要有同步的接口调用和异步的事件驱动
- DDD领域驱动设计是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。
- 中台: 将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件
- 业务中台、数据中台、技术中台
五、你的项目中是怎么保证微服务敏捷开发的?微服务的链路追踪、持续集成、AB发布要怎么做?
- 敏捷开发: 为了提高团队的交付效率,快速迭代,快速试错
- 链路追踪: 基于日志,形成全局事务ID,落地到日志文件
- 持续集成: maven pom -> build ->shell
- AB发布:
- 蓝绿发布,红黑发布。新老版本同时存在
- 灰度发布
Spring篇
一、Spring框架中Bean的创建过程是怎么样的?
- 实例化、属性赋值、初始化、销毁
- 实例化
- 当客户端容器申请一个Bean时
- 当容器在初始化一个Bean时发现还需要依赖另一个Bean时
- 以BeanDefinition对象的形式保存
- 属性赋值(依赖注入): Spring通过BeanDefinition找到对象依赖的其他对象,并将这些对象赋予当前对象
- 处理Aware接口:Spring会检测对象是否实现了xxxAware接口,实现了则调用相应的方法
- BeanPostProcessor前置处理:调用BeanPostProcessor的postProcessBeforeInitialization()
- InitializationBean:Spring检测对象如果实现此接口,就会执行它的afterPropertiesSet()方法,定制初始化逻辑
- init-method:Spring发现Bean配置了这个属性,就会调用它的配置方法,执行它的初始化逻辑 @PostConStruct
- BeanPostProcessor后置处理:调用BeanPostProcessor的postProcessAfterInitialization()
·····Bean创建完成·····
- DisposableBean:如果Bean实现此接口,在对象销毁前会调用destory()方法
- destory-method:@PreDestory
二、Spring中Bean是线程安全的吗?如果不安全,要如何处理?
- 不是
- SpringBean作用域:
- singleton:单例
- prototype:为每个Bean请求创建实例
- request:为每一个request请求创建一个实例,请求完成后失效
- session:为每一个session请求创建一个实例,请求完成后失效
- global-session:全局
- singleton: 默认线程不安全。大部分Bean无状态,不需要保证线程安全
- prototype: 每次都生成一个新的对象,所以不存在线程安全问题
三、Spring如何处理循环依赖问题?
- 循环依赖: 多个Bean之间相互依赖,形成了一个闭环。A依赖B,B依赖C,C依赖A
- Spring中singleton支持循环依赖,Prototype不支持。只有单例的Bean会通过三级缓存提前暴露来解决循环依赖问题,而非单例Bean每次都是从容器中获取一个新的对象,都会重新创建,所以 非单例的Bean是没有缓存的,不会将其放到三级缓存中
- 三级缓存
- 第一级缓存(单例池): singletonObjects,存放已经经历了完整生命周期的Bean对象
- 第二级缓存: earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)
- 第三级缓存: Map<String,ObjectFactory<?>> singletonFactorices,存放可以生成Bean的工厂
- 三级缓存的迁移过程
- A创建过程中需要注入属性B,于是将A放入三级缓存,去创建B
- B创建过程中发现需要注入属性A,于是B先查一级缓存,没有再查二级缓存,没有再查三级缓存,在三级缓存中找到A,然后将三级缓存的A放入二级缓存并删除三级缓存的A
- B创建完毕,将自己放入一级缓存(此时B的属性A的状态依然是创建中),然后继续创建A,从一级缓存里拿到B,完成A的创建,将A放入一级缓存
四、Spring如何处理事务
编程式事务
声明式事务: Spring在AOP基础上提供的事务实现机制
传播级别
- PROPAGATION_REQUIRED:默认传播行为。如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入到事务中。
- PROPAGATION_SUPPORTS:如果当前存在事务,就加入到该事务。如果当前不存在事务,就以非事务方式运行。
- PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务。如果当前不存在事务就抛出异常。
- PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。
- PROPAGATION_NOT_SUPPORTED:以非事务方式运行。如果当前存在事务,就将当前事务挂起。
- PROPAGATION_NEVER:以非事务方式运行。如果当前存在事务,就抛出异常。
- PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务则按REQUEIRED属性执行。
@Transactional注解的失效场景
- @Transactional 应用在非 public 修饰的方法上:事务拦截器中会检查目标方法的修饰符,不是public不会获取@Transactional的属性配置信息
- @Transactional 注解属性 propagation 设置错误
- @Transactional 注解属性 rollbackFor 设置错误
- 同一个类中方法调用,导致@Transactional失效
- 异常被catch"吃了"导致@Transactional失效
- 数据库引擎不支持事务
隔离级别
- ISOLATION_DEFAULT:使用数据库默认的事务隔离级别。
- ISOLATION_READ_UNCOMMITTED:读未提交。允许事务在执行过程中,读取其他事务未提交的数据。
- ISOLATION_READ_COMMITTED:读已提交。允许事务在执行过程中,读取其他事务已经提交的数据。
- ISOLATION_REPEATABLE_READ:可重复读。在同一个事务内,任意时刻的查询结果是一致的。
- ISOLATION_SERIALIZABLE:所有事务依次执行。
五、SpringMVC中的控制器是不是单例模式?如果是,如何保证线程安全?
- 是
- 单例模式下就会有线程安全问题
- Spring中保证线程安全的方法
- 将scop设置成非singleton:prototype,request
- 将控制器设计成无状态模式。在controller中不要携带数据,但是可以引用无状态的service和dao
六、Spring AOP顺序
- 正常
- 环绕通知前
- @Before
- 被调用方法
- @AfterReturning
- @After
- 环绕通知后
- 异常
- 环绕通知前
- @Before
- 被调用方法
- @AfterThrowing
- @After
Linux篇
一、生产环境服务器变慢,诊断思路和性能评估?
- 整机: top(精简版uptime)
- load average(系统负载均衡):1,5,15系统平均负载值,三个值相加除以3高于60%,系统压力重
- CPU: vmstat
- 内存: free -m
- 硬盘: df -h
- 磁盘IO: iostat -xdk
- 网络IO: ifstat
二、生产环境出现CPU占用过高,分析思路和定位?
- 先用top命令找出CPU占比最高的
- ps -ef或jps进一步定位
- 定位到具体线程或者代码
- ps -mp 进程ID -o THREAD,tid,time
- 将需要的线程ID转换为16进制格式(英文小写)
- jstack进程ID|grep tid(16进制线程ID英文小写) -A60
MySql篇
一、索引
- 索引是帮助Sql高效获取数据的数据结构。索引存储在文件系统中。索引的文件存储形式与存储引擎(InnoDB、MyISAM)有关。索引文件的结构有hash、二叉树、B树、B+树
- MySql索引类型
- 主键索引
- 唯一索引:索引列的所有值都只能出现一次,必须唯一,值可以为空
- 普通索引:基本索引类型,值可以为空,没有唯一性的限制
- 全文索引:索引类型为FILLTEXT。可以在varchar、char、text类型的列上创建
- 组合索引:多列值组成一个索引,用于组合搜索
- 为什么要用B+树:
- hash缺点:比较耗费内存空间,不适合范围查找
- 二叉树、红黑树缺点:会由于树的深度过深而造成io次数变多,影响读取效率
- B树缺点:不能存取较大的数据
- 名词解释
- 回表:从索引中查出主键,再用主键去查索引中没有的数据
- 覆盖索引:从索引中就可以查到,不需要回表
- 最左匹配:在组合索引中,如果sql语句中用到了组合索引中的最左边的索引,那么这条sql语句就可以利用这个组合索引去进行匹配。当遇到范围查询(>、<、between、like)就会停止匹配
- 索引下推:在最左匹配原则的基础上,直接按照条件查询的所有条件做判断,并从存储引擎中拉去符合条件的数据
- 索引优化
- 当使用索引进行查询的时候尽量不要使用表达式,把计算放到业务层而不是数据库层
- 尽量使用主键查询,而不是其他索引,因为主键查询不会触发会标查询
- 使用前缀索引
- 使用索引扫描来排序
- union all、in、or都能够使用索引,但是推荐用in
- 范围列可以用到索引
- 强制类型转换会全表扫描
- 更新十分频繁,数据区分度不高的字段上不宜建立索引
- 创建索引的列,不允许为null,可能会得到不符合预期的效果
- 最好不要超过三张表连接,因为需要join的字段,数据类型必须一致
- 能用limit尽量用limit
- 单表索引建议控制在5个以内
- 组合索引时,但索引字段数不允许超过5个
- 索引不是越多越好
- 不要在不了解系统的情况下进行过早优化
- like、不等于、大于小于走不走索引?
- like语句要使索引生效,like后不能以%开始,也就是说 (like %字段名%) 、(like %字段名)这类语句会使索引失效,而(like 字段名)、(like 字段名%)这类语句索引是可以正常使用
- 在使用不等于(!=或者<>)的时候无法使用索引导致全表扫描 is not null也无法使用索引,但是is null是可以使用索引的.
- 聚簇索引和非聚簇索引
- 聚簇索引的B+树叶子节点的data是完整的数据记录
- 非聚簇索引的B+树叶子节点的data是主键的值
二、事务
- 原子性A:要么全部成功,要么全部失败
- 一致性C:所有事务读取同一个数据的结果是相同的
- 隔离性I:一个事务在提交之前,对其他事务不可见
- 持久性D:一旦事务提交,所做的操作永久保存在数据库中
三、存储引擎
- InnoDB:默认隔离级别可重复读
- 解决幻读:MVCC+Next-key Locking
- MVCC:写操作更新最新的版本快照,读操作读旧版本快照,快照存储在Undo日志中
- Next-key Locking:锁定一个记录上的索引和索引之间的间隙。锁定一个前开后闭区间。一个索引包含:10,11,13,20。锁定的区间为(-∞,10](10,11](11,13](13,20](20,+∞)
- 解决幻读:MVCC+Next-key Locking
- MyISAM