⭐计算机网络
键入网址到网页显示
解析URL,协议栈(TCP可靠传输、IP远程定位、MAC两点传输)对数据进行封装,然后通过网卡、交换机、路由器进行传输,服务器响应和浏览器渲染
- HTTP:对
URL
进行解析,从而生成发送给Web
服务器的HTTP请求信息 - DNS:浏览器通过 DNS 协议,获取域名对应的 IP 地址,然后将HTTP的传输工作交给协议栈
- TCP:浏览器根据 IP 地址和端口号,向目标服务器发起一个 TCP 连接请求,经过“三次握手”建立可靠连接,如果Http消息太长,会将数据分段,加上TCP头,交给下面的网络层处理
- IP:在网络层根据IP协议,进一步生成IP头部,将数据封装成IP网络包,IP头部包含 源地址 IP 和 目标地址 IP,用于远程定位
- MAC:在网络接口层,网络包还需要在 IP 头部的前面加上 MAC 头部,生成MAC数据帧,用于在不同设备之间进行传输
- 网卡:将数字信息转换为电信号,通过网线发送出去
- 交换机:根据 MAC 地址表查找 MAC 地址,然后将信号发送到相应的端口
- 路由器:根据 路由表 查找转发地址,转发网络包到下一个路由器或目标设备
- 服务器收到 HTTP 请求报文后,处理请求,并返回 HTTP 响应报文给浏览器。
- 浏览器收到 HTTP 响应报文后,解析响应体中的 HTML 代码,渲染网页的结构和样式,同时根据 HTML 中的其他资源的 URL(如图片、CSS、JS 等),再次发起 HTTP 请求,获取这些资源的内容,直到网页完全加载显示。
- 因为路由器是基于 IP 设计的,俗称三层网络设备,路由器的各个端口都具有 MAC 地址和 IP 地址;路由器的端口具有 MAC 地址,因此它就能够成为以太网的发送方和接收方
- 而交换机是基于以太网设计的,俗称二层网络设备,交换机的端口不具有 MAC 地址。
❗Http常见字段
客户端(请求头):Host、Accept、Accept-Encoding、Connection、Cookie
服务器(响应头):Content-type、Content-Length、Content-Encoding
get和post区别
语义、参数位置、安全幂等、缓存
Http和Https区别
传输方式、资源消耗、建立连接、端口号
❗http1.0、1.1、2.0、3.0
1.1:管道传输、长连接;
2.0:头部压缩(消除重复头)、二进制数据、并发传输(解决了应用层的队头阻塞,没有解决tcp层的)、服务器推送
3.0:无队头阻塞、建立连接、连接迁移
HTTPS工作流程
1. 客户端发起 HTTPS 请求,连接到服务端的 443 端口。
2. 服务端将自己的数字证书发送给客户端(公钥在证书里面,私钥由服务器持有)。
3. 客户端收到数字证书之后,会验证证书的合法性。如果证书验证通过,就会生成一个随机的对称密钥(用于后续的数据传输),用证书的公钥加密,发送到服务器。
4. 服务器接收到之后,用自己之前保留的私钥对其进行解密,解密之后就得到客户端的密钥,然后用密钥对返回数据进行对称加密,传输给客户端
5. 客户端收到后,用自己的密钥对其进行对称解密,得到服务器返回的数据
cookie和session
作用(保存客户状态),存储位置,有效期,安全性
Cookie 是保存在客户端的一小块文本串的数据。客户端向服务器发起请求时,服务端会向客户端发送一个 Cookie,客户端就把 Cookie 保存起来。在客户端下次向同一服务器再发起请求时,Cookie 被携带发送到服务器。服务端可以根据这个Cookie判断用户的身份和状态。
Session 指的就是服务器和客户端一次会话的过程,是另一种记录客户状态的机制。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上,这就是session。客户端浏览器再次访问时只需要从该session中查找用户的状态
存储位置:Cookie 保存在客户端,Session 保存在服务器端。
存储数据类型:Cookie 只能保存ASCII,Session可以存任意数据类型,一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。
有效期:Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般有效时间较短,客户端关闭或者 Session 超时都会失效。
隐私策略:Cookie 存储在客户端,比较容易遭到不法获取,早期有人将用户的登录名和密码存储在 Cookie 中导致信息被窃取;Session 存储在服务端,安全性相对 Cookie 要好一些。
存储大小: 单个Cookie保存的数据不能超过4K,Session可存储数据远高于Cookie。
TCP/IP模型
应用层:为用户提供应用功能,定义了数据传输的格式,不用关心数据如何传输,主要协议有http、SMTP、DNS等
传输层:为数据传输建立连接,为不同主机上运行的 进程 提供逻辑通信,主要协议有TCP(可靠、连接)、UDP(不可靠、无连接)
网络层:为不同网络之间的数据传输提供服务,主要是寻址和路由,寻址:确定数据包的目的地,路由:选择合适的路径,主要协议有IP协议(将TCP段封装为IP数据包进行传输)
网络接口层:在以太网(局域网)中的不同设备之间进行数据传输,封装成数据帧
❗TCP和UDP的区别
可靠、连接、字节流报文、首部(tcp20>udp8)、传输对象、效率
TCP三次握手、四次挥手
半连接队列和 SYN Flood 攻击
可靠性、重传、滑动窗口、拥塞控制、流量控制
粘包
当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。两个消息的某个部分内容被分到同一个 TCP 报文
解决:
- 固定长度的消息;
- 特殊字符作为边界;
- 自定义消息结构。
IPv4和ipv6区别
⭐操作系统
进程管理
进程状态
创建、就绪、运行、阻塞、结束
**线程状态:创建、可运行、等待、超时等待、阻塞、结束
PCB、进程上下文切换、线程的实现
线程和进程的区别
基本单位、开销、共享资源、独立性
协程
用户自己管理的一种轻量级线程,切换开销更小
❗进程调度算法
先来先服务、最短作业优先、高响应比优先(1+等待时间/处理时间)、时间片轮转、最高优先级、多级反馈队列
进程通信
多线程竞争
死锁
内存管理
虚拟地址、虚拟内存的作用
分段分页、内存满了
页面置换算法
最佳页面置换、先进先出、最近最久未使用、时钟页面置换算法、最不常用算法
文件管理
文件系统结构
目录项, 索引节点、数据块(文件、目录)、超级块
软硬链接
❗磁盘调度算法
先来先服务、最短寻道时间优先、扫描算法、循环扫描算法、LOOK、C-LOOK算法
⭐Java
基础
包装类缓存
String和StringBuilder和StringBuffer区别
深浅拷贝
equals和hashCode
抽象类和接口
泛型,反射,序列化
❗动态代理和静态代理的区别
- 静态代理:需显式定义代理类,由程序员或者特定工具创建,在代码编译时就确定了被代理的类是一个静态代理。静态代理通常只代理一个类
- 动态代理:在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口下的多个实现类 ( UserService proxy = (UserService) Proxy.newProxyInstance )
- 理解:静态代理和动态代理 - 奇遇yms - 博客园 静态代理类通过直接调用原对象的方法user.getInfo()来实现代理,动态代理类通过method.invoke来调用原对象的方法,所以可以不用在编译时就确定原对象
集合
ArrayList 扩容机制
默认情况下会有一个默认长度为10的数组,也可以通过构造器指定大小
随着数据量增多,当数量超过这个容量时,他就会进行扩容
创建一个新数组,这个新数组的长度会扩容到原数组的1.5倍,比如10扩到15
调用工具类Arrays的方法copyOf将数据复制到新数组
再把新数据添加到新数组
ArrayList 和 LinkedList区别
数据结构、时间复杂度、随机访问、内存占用
BlockingQueue 实现类
HashMap 和 HashTable
数据结构、效率、线程安全、扩容机制
❗HashMap 原理
主要讲一下这个 put 操作
哈希函数:当我们要put一个元素时,首先会去计算他的一个哈希值,通过跟这个 数组的长度减一 的值 做一个与运算 (n - 1) & hash
来判断元素存放的位置。
哈希冲突:如果产生了哈希冲突(也就是两个不同的key计算出来的地址位置是相同的),根据版本的不同也有不同的解决方案。
早期采用的是链表寻址的方式,对于存在冲突的key,hashmap会把它组成一个单向链表,后面来的元素插入到链表的尾部(尾插法,8之前是头插法,有环形链表死循环问题)。
但是为了避免这个链表长度过长导致查询效率降低的问题,JDK8之后做了优化,当链表长度>8、数组长度>=64的时候,会把链表转化为红黑树。
扩容机制:之后还会做一个扩容的判断,如果 当前hashmap的元素个数 超过了 容量跟负载因子(默认是0.75)的乘积,就会扩容到原来的2倍
- 创建新数组,容量为原来的2倍
- 遍历旧表,重新hash取模,放入元素
- 如果是链表,则遍历链表,插入元素
- 如果是红黑树,split()方法:将红黑树遍历成链表的形式,并记录红黑树中的节点数量,如果 树的结点数<= 6个,则退化成链表,否则重新树化成红黑树,添加到新表。
remove:与结点数无关,如果红黑树根 root 为空,或者 root 的左子树/右子树为空,root.left.left 左子树的左子树 为空,都会发生红黑树退化成链表
HashMap负载因子为什么是0.75
负载因子为1.0时,意味着,只有当 map 全部被填满,才会扩容;当元素越来越多时,会出现大量的冲突:
若此时是链表,则查询效率是 O(n),即线性;(插入/删除结点也要遍历到链表);
若此时是红黑树,查询效率是 O(logN),但红黑树的插入与删除就异常复杂,每次都要调整树;
因此,负载因子1.0,实际是牺牲了时间,但保证了空间的利用率。
负载因子为0.5时,意味着,当 map 中的元素达到一半时,就发生扩容,容量扩大一倍:
hash 冲突减少,查询效率提高了;
但存在大量的内存浪费现象,因为数组中的个数永远小于数组长度的一半。
因此,负载因子0.5,实际是牺牲了空间,但保证了时间效率。
为了平衡时间和空间成本,于是就进行了中和,使用了0.75这个中间的负载因子
ConcurrentHashMap 和 HashTable
底层数据结构、实现线程安全的方式
**ConcurrentHashMap使用 cas+synchronized,cas用于添加链表头(没有哈希冲突,添加一个节点到空位置上)
CopyOnWriteArrayList
CopyOnWriteArrayList 核心原理是 写时复制(Copy-On-Write)策略:当需要修改 CopyOnWriteArrayList 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完成后再将修改后的数组赋值回去。这种方式保证了写操作不会影响读操作,从而实现了读写分离,提高了读操作的性能。
主要特点和优势:
1. 读操作无需加锁
2. 写操作不阻塞读操作:写操作会对底层数组进行复制和修改,写操作完成后再将修改后的数组赋值回去,这样写操作不会阻塞读操作。
3. 线程安全:提供了线程安全的操作,适合在读操作频繁、写操作较少的场景下使用。
并发
❗JMM(Java内存模型)
JAVA内存模型,是一组规范,定义了Java程序中多线程 并发访问 内存时的行为(比如说线程之间的共享变量必须存储在主内存中)
确保了在不同的平台(跨平台) 上,Java程序对内存的访问表现出一致的行为。
解决CPU多级缓存和指令重排导致的问题,主要体现在原子性、可见性和有序性。
- 原子性:一次操作或者多次操作,要么所有的操作全部都得到执行,并且不会受到任何因素的干扰而中断,要么都不执行。(
synchronized
、各种Lock
以及各种原子类 来实现) - 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。主要通过volatile来实现(规定每次使用volatile变量都到主存中进行读取)
- 有序性:保证本线程内的代码不被重排序。
***Java内存区域
❗sychronized 和 ReentrantLock
-
用法不同:synchronized 可用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用在代码块上。
-
获取锁和释放锁方式不同:synchronized 会自动加锁和释放锁, ReentrantLock 需要手动加锁和释放锁
-
ReentrantLock 增加了一些高级功能:公平锁、可响应中断(解决死锁)、支持多个条件变量、可以设置超时时间
-
底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
-
性能:1.6之前,synchronized性能比reentrantlock差很多,后续做了优化,就差不多了
synchronized 和 volatile
-
volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的互斥和同步性 -
volatile不需要加锁,比synchronized更轻量级,不会阻塞线程;
-
synchronized既能够保证可见性,又能保证原子性,而volatile只能保证可见性,无法保证原子性。
-
volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
❗volatile可见性原理
volatile是通过内存屏障来保证可见性的,Load屏障保证volatile变量每次读取数据的时候都强制从主内存读取;Store屏障每次volatile修改之后强制将数据刷新会主内存。
有哪些锁
乐观锁、悲观锁、synchronized、reentrantlock
❗sychronized原理
Synchronized关键字底层是使用 monitor对象锁 实现的
每一个对象关联一个monitor对象,当使用synchronized时,对象头中的 MarkWord 会指向monitor对象,而monitor对象可以看成是一个对象锁
如果monitor的owner是其他线程,那么本线程就加入EntryList阻塞,否则就成为Owner
它采用互斥的方式 让同一时刻至多只有一个线程能持有对象锁,其他线程再想获取这个对象锁时会被阻塞住,这样就能保证拥有锁的线程可以安全的执行临界区的代码。
后期做了优化
❗锁优化
-
锁膨胀:synchronized 根据锁的竞争情况和对象的状态,从 无锁 升级到 偏向锁,再到 轻量级锁,最后到 重量级锁 的过程,它叫做锁膨胀也叫做锁升级。
JDK 1.6 之前,synchronized 是重量级锁,也就是说 synchronized 在释放和获取锁(阻塞唤醒)时都会从用户态转换成内核态,而转换的效率是比较低的。
但有了锁膨胀机制之后,synchronized 的状态就多了无锁、偏向锁以及轻量级锁了,这时候在进行并发操作时,大部分的场景都不需要用户态到内核态的转换了,这样就大幅的提升了 synchronized 的性能。 -
偏向锁:资源被一个线程多次访问时的锁状态。共享资源首次被访问时,JVM会对该共享资源对象做一些设置,比如将对象头中是否偏向锁标志位置为1,并存储当前线程ID指针,后续当前线程再次访问这个共享资源时,会根据偏向锁标识跟线程ID进行比对是否相同,比对成功则直接获取到锁,进入临界区域,这也是synchronized锁的可重入功能。(无cas)
轻量级锁:在无竞争的情况下(不同时竞争),JVM会使用轻量级锁,首先在线程的栈中创建一个锁记录,以 CAS 方式来获取锁,将 锁记录地址 存储到对象头中的 MarkWord(一般就是自旋加锁,不阻塞线程采用循环等待的方式)
成功则获取到锁,状态为轻量级锁,失败则进行 自旋等待( 默认10次),等待次数达到阈值仍未获取到锁,则升级到重量级锁
**自旋:线程的阻塞唤醒 需要从用户态切换到内核态,然后内核态切换 tcb,切到另一个线程的内核态,再从内核态进入用户态,这是一个重量级的操作,效率低 -
锁消除:指的是在某些情况下,JVM 虚拟机如果检测不到某段代码被共享和竞争的可能性,就会将这段代码所属的同步锁消除掉,从而到底提高程序性能的目的。
-
锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
-
自适应自旋锁:指通过自身循环,尝试获取锁的一种方式,优点在于它避免一些线程的挂起和恢复操作,因为挂起线程和恢复线程都需要从用户态转入内核态,这个过程是比较慢的,所以通过自旋的方式可以一定程度上避免线程挂起和恢复所造成的性能开销。
-
CAS
CAS操作是一种乐观锁机制,适用于多线程环境下对共享变量的并发访问控制。
主要包括三个参数:
- V:原来的变量值(Var)
- E:预期值(Expected)
- N:新值(New)
CAS操作会比较V和E,相同的话将V更新为N,否则失败重试
AQS
AQS 就是一个抽象类,主要用来构建锁和同步器。
核心思想:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套 线程阻塞等待 以及 被唤醒时 锁分配 的机制,这个机制 AQS 是基于 CLH 锁 实现的。
CLH 锁是一个虚拟的双向队列,用于存储阻塞线程。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。FIFO
AQS内部还维护一个state变量,表示锁的占有状态,共享变量,使用volatile保证可见性。state被初始化为0,表示释放,被占有时,state被设置为1,还可以通过累加state实现重入。
应用:ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier
线程池
参数、创建方式、工作流程、优点
池化技术(连接池、字符串常量池),用来存放和管理一系列线程,避免频繁创建销毁带来的开销,提高利用率
核心线程数、最大线程数、阻塞队列。(其它:存活时间、线程工厂、拒绝策略)
ThreadPoolExecutor、Executors类(内置线程池有哪些)
❗ThreadLocal
对于一个变量,ThreadLocal让每个线程绑定自己的值,实现了线程之间的隔离
用户信息上下文的存储
每个线程内部都维护了各自的一个ThreadLocalMap对象,是一个map类数组,key存放的是ThreadLocal对象的弱引用,value存放的是线程本地变量
内存泄漏:key是弱引用,value是强引用。如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
⭐JVM
内存区域
JVM的内存结构主要分为以下几个部分:
-
程序计数器:当线程执行 Java 方法时,程序计数器保存当前执行指令的地址,以便在 JVM 调用其他方法或恢复线程执行时重新回到正确的位置。
-
Java 虚拟机栈:虚拟机栈保存着方法执行期间的局部变量、操作数栈、方法出口等信息。线程每调用一个 Java 方法时,会创建一个栈帧(Stack Frame),栈帧包含着该方法的局部变量、操作数栈、方法返回地址等信息。栈帧在方法执行结束后会被弹出。
-
本地方法栈:与 Java 虚拟机栈类似,但是为本地方法服务。
-
Java 堆:Java 堆是 Java 虚拟机中最大的一块内存区域,用于存储各种类型的对象实例和静态变量,也是垃圾收集器的主要工作区域,Java 堆根据对象存活时间的不同,Java 堆还被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区
-
方法区:方法区也是所有线程共享的部分,它用于存储类的加载信息、常量池、方法字节码等数据。在 Java 8 及以前的版本中,方法区被实现为永久代(Permanent Generation),在 Java 8 中被改为元空间(Metaspace)
-
直接内存:不属于jvm,常用于NIO操作,比如读文件,没有使用直接内存时,需要先把文件读入系统缓冲区再把数据复制到Java堆中。现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
类加载
类加载过程
加载:通过类加载器将字节码文件加载到内存,并在方法区和堆上各分配一个对象
连接
验证:验证字节码文件是否符合虚拟机规范
准备:为静态变量分配内存并赋初值
解析:将类中的符号引用替换为直接引用
初始化:为静态变量赋值
使用、卸载
类加载器
类加载器作用,主要包含三个:启动(由C++实现)、扩展、应用程序
双亲委派模型:
解决 类由哪个类加载器来加载 的核心问题
当一个类加载器接收到加载类的任务时,会自底向上查找是否加载过,再由顶向下进行加载
好处有两点:第一是避免恶意代码替换JDK中的核心类库,比如java.lang.String,确保核心类库的完整性和安全性。第二是避免一个类重复地被加载。
垃圾回收
判断是否可以垃圾回收
❗四种引用
强引用:强引用数据不会被回收,只有当强引用引用的对象不再被任何其他对象引用时,该对象才会被回收
软引用:当程序内存不足时,就会将软引用中的数据进行回收,可用来实现缓存
弱引用:无论内存是否充足,只要被垃圾回收器扫描到,且只有弱引用关联,弱引用数据就会被回收
虚引用:虚引用并不会决定对象的生命周期,主要用来跟踪对象被垃圾回收的状态
死亡对象判断
引用计数法、可达性分析法
垃圾回收算法
标记清除、标记整理、复制、分代回收算法
分代回收算法(放到Eden区,内存不够使用复制算法+幸存者区,多次回收年龄超过阈值15后放到老年区)
为什么要分代
可以根据各个年代的特点选择合适的垃圾收集算法
新生代-复制算法:
每次垃圾收集都能发现大批对象已死, 只有少量存活. 因此选用复制算法, 只需要付出 少量 存活对象的复制成本就可以完成收集
老年代-标记整理算法:
因为对象存活率高、没有额外空间对它进行分配担保, 就必须采用“标记—清理”或“标记—整理”算法来进行回收, 不必进行内存复制, 且直接腾出空闲内存
垃圾回收器
-
Serial 收集器,串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。适用于单cpu场景
-
ParNew 收集器,ParNew 收集器其实就是 Serial 收集器的多线程版本。
-
Parallel 收集器,Parallel Scavenge 收集器类似 ParNew 收集器,可以自动调整堆大小,更关注系统的吞吐量,不能保证单次停顿时间(适用于后台任务,不需要与用户交互,且容易产生大量的对象。比如:大数据的处理,大文件导出)
-
Parallel Old 收集器,Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法
-
CMS 收集器,CMS(Concurrent Mark Sweep 并发标记)收集器是一种以获取最短回收停顿时间为目标的收集器。标记清除算法。初始标记、并发标记、重新标记、并发清理。适用于对停顿时间要求高的场景(大型的互联网系统中用户请求数据量大、频率高的场景,比如订单接口、商品接口等)
-
G1 收集器,G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。分区回收,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征
❗G1垃圾回收器
- 分为大小相同的多个区:eden、幸存者、old、humongous
- 同时兼顾吞吐量和停顿时间
- 回收的范围是整个堆,包括新生代和老年代
- G1收集器的运行过程:
- YoungGC:JVM 启动时,G1 先准备好 Eden 区,程序在运行过程中不断创建对象到 Eden 区,当 Eden 空间耗尽时,G1 会启动一次年轻代垃圾回收过程,使用复制算法,复制到幸存者区。(stw)。多次回收后长期存活对象会进入老年区
- 并发标记:当老年代占用内存超过阈值(默认是45%)后,触发并发标记,这时无需暂停用户线程stw,之后会有重新标记阶段解决漏标问题,此时需要暂停用户线程。(标记老年代存活对象,为混合回收做准备,同时统计各个区域存活对象)
- 混合GC:这些都完成后就知道了老年代有哪些存活对象,随后进入混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收 价值高(存活对象少) 的区域 (这也是 Gabage First 名称的由来)。eden和survivor复制到新的survivor,old复制到新的old
- 优点
- 没有内存碎片:整体上是标记整理算法,局部上看是复制算法
- 停顿时间可预测:分区回收,可以根据用户定义的暂停时间来决定回收多少内存。能够让使用者明确的指定 在一个长度为M毫秒的时间片段内,垃圾收集消耗的时间不得超过N毫秒
❗G1和cms区别
回收算法、回收区域、堆结构、回收过程、优点、使用场景
垃圾回收类型
各种GC区别
部分收集 (Partial GC)是指垃圾收集器只对Java堆中的部分区域进行垃圾回收的过程。具体而言,部分收集包括以下几种类型:
- 新生代收集(Minor GC/Young GC)
指垃圾收集器只对新生代进行垃圾回收的过程。新生代通常由Eden区和Survivor区组成,Minor GC会清理Eden区和Survivor区中的垃圾对象,并将存活的对象复制到Survivor区或晋升到老年代。Minor GC的频率较高,通常伴随短暂的停顿时间。 - 老年代收集(Major GC/Old GC)
指垃圾收集器只对老年代进行垃圾回收的过程。老年代中存放的是存活时间较长的对象,因此Major GC的频率相对较低,可能伴随较长的停顿时间。目前,只有部分垃圾收集器(如CMS收集器)会有单独收集老年代的行为。 - 混合收集(Mixed GC)
指垃圾收集器同时对新生代和部分老年代进行垃圾回收的过程。混合收集旨在减少全局停顿时间,提高系统的吞吐量。目前,只有G1收集器会有这种行为,它通过划分整个堆内存为多个大小相等的区域(Region),并根据垃圾分布情况选择性地进行收集。
整堆收集 (Full GC)是指垃圾收集器对整个Java堆和方法区进行垃圾回收的过程。Full GC的频率较低,但回收的内存较多,可能伴随较长的停顿时间。在整堆收集期间,所有的应用线程都会被暂停,直到垃圾回收完成。
FullGC触发条件
- 空间分配担保失败
在要进行 Young GC 的时候,发现 老年代可用的连续内存空间 < 新生代历次Young GC后升入老年代的对象总和的平均大小 ,说明本次Young GC后可能升入老年代的对象大小,可能超过了老年代当前可用内存空间,那就会触发 Full GC。 - 老年代空间不足
发生youngGC后长期存活对象或者大对象需要进入老年代时,老年代空间不足,也会触发Full GC。 - 方法区内存空间不足
如果方法区由永久代(1.8之祈前)实现,永久代空间不足 Full GC。 - System.gc()等命令触发
System.gc()、jmap -dump 等命令会触发 full gc。
什么时候进入老年代
- 长期存活的对象
对象的迭代年龄会在每次Young GC之后的对象移动操作中增加,当迭代年龄达到一定阈值(默认为15)时,对象将被移入老年代。可以通过调整 -XX:MaxTenuringThreshold 参数来设置这个阈值。 - 大对象
占用大量连续内存空间的对象,如大数组或长字符串,会直接进入老年代。可以通过调整 -XX:PretenureSizeThreshold 参数来设置大对象的大小阈值。 - 动态年龄判断
如果在Survivor空间中 相同年龄所有对象大小的总和 > Survivor空间的一半,大于或等于该年龄的对象就可以直接进入老年代。 - 空间分配担保(YoungGC后仍无法存放时)
在Young GC之后,如果新生代仍有大量对象存活,就需要老年代进行分配担保,将Survivor区无法容纳的对象直接送入老年代。这个机制避免了频繁将存活对象从新生代复制Survivor区,提高了垃圾回收的效率。
调优
jps、jstack
⭐Spring
Spring是什么
轻量级框架、包含IOC、AOP模块、优点
模块
IoC、AOP、Spring Web、数据访问、Spring Test
优点
实现类之间的解耦,实现代码的复用,很快地集成第三方组件
常用注解
IoC、AOP
IoC:将应用程序手动创建对象的控制权交给外部框架或容器,由容器去管理对象之间的依赖关系,并完成依赖注入,当我们需要使用对象时,直接从容器中获取(配置文件/注解),解耦
AOP:将横切关注点(分散在多个类或对象中的公共行为,如日志记录、事务管理、性能监控)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,形成切面,实现代码的复用和解耦,提高代码的可维护性和可扩展性
应用场景、实现方式
Spring Bean声明、注入
@Component和@Bean区别
❗Bean作用域、生命周期
❗循环依赖、设计模式
❗springboot启动过程、自动装配和自动配置
❗事务
编程式和声明式(注解,基于AOP)原理,ACID,隔离级别、❗传播行为,失效
❗SpringMVC
MVC定义、定义、组件、工作流程
MVC 是模型、视图、控制器的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。SpringMVC能够帮助我们快速构建一个MVC架构的程序,进行更简洁的 Web 层的开发
⭐MySQL
SQL语句执行过程
server层(连接器、查询缓存、分析器、优化器、执行器),存储引擎
连接器:tcp三次握手连接,验证用户名和密码,保存权限
分析器:词法分析(识别关键词比如select、from),语法分析(根据词法分析结果判断是否满足语法规则)
预处理器:
检查 SQL 查询语句中的表或者字段是否存在;
将 select * 中的 * 符号,扩展为表上的所有列;
优化器:确定 SQL 查询语句的执行方案(比如选择哪个索引)
执行器:调用存储引擎查询数据
MyIsam和Innodb区别
外键、行级锁、MVCC、事务、灾难恢复(redolog)、索引实现
索引
有哪些索引
B+树、hash、全文
聚集、二级
主键、唯一、 普通、前缀、联合、覆盖
索引优化
选择or不选择哪些字段、前缀索引、覆盖索引、NOT NULL、主键自增、防止索引失效
B树和B+树的区别
数据存储、引用链
innodb为什么使用B+树
查询速度快
聚集索引,所有非叶子节点只存放索引,不存放数据,因此它一页存放的数据就会更多,那么因为内存跟磁盘交互是以页为单位,所以一页存放的数据多,从而可以减少跟磁盘的交互次数
查询速度稳定
每次查询都是从根节点到叶子节点
适合范围查询
找到下限节点,然后只需遍历链表;在 B 树中进行范围查询时,首先找到要查找的下限,然后对 B 树进行中序遍历,直到找到查找的上限
为什么不用BST
数据量大可能造成树倾斜,最终退化成一个线性表结构,查找性能下降
为什么不用红黑树
数据量大,树的高度会很大,磁盘IO次数很多,并且本身也不是严格的平衡,导致查询速度下降
为什么不用Hash
Hash 在做等值查询的时候效率快,搜索复杂度为 O(1)。
但是 Hash 表不适合做范围查询,它更适合做等值的查询
为什么不用跳表
B+树更适合磁盘IO,一次读取一页,减少IO次数
跳表不适合磁盘IO,一次读取一个节点
B+树是一种多路平衡查找树,它的节点可以有多个子节点,这使得树的高度比跳表要低
事务
事务四大特性、事务隔离级别、事务实现
❗事务隔离级别的实现
RC和RR是通过MVCC实现的,可串行化使用锁
MVCC是一种并发控制机制,对于一条记录会生成多个快照,存在多个历史版本,主要通过隐藏字段、ReadView、undolog来实现。当一个事务执行查询操作时,会创建一个ReadView,保存活跃(未提交)事务id列表,数据库通过对比 数据的创建者事务ID 和 活跃事务id列表,来判断数据的可见性。如果数据不可见,就通过隐藏字段 回滚指针找到 undolog 中对应的历史版本,事务只能在该版本上进行操作。
ReadView确定要读取哪个版本,隐藏字段存储对应版本的位置,回滚日志存储对应版本的数据
- 在RC级别下,每次select查询 前都生成一个ReadView,读取一个快照
- 在RR级别下,只有在 事务开启后的第一次select查询 前生成ReadView,后续该事务的操作都在该版本上进行
ReadView
ReadView是MVCC的一个关键概念
- 事务ID列表:当前系统中所有活跃(未提交)的事务ID列表
- 最小活跃事务ID:事务ID列表中最小的事务ID
- 最大事务ID:创建ReadView时系统应该分配给下一个事务的ID
当事务通过ReadView来读取数据时,它会按照以下规则来判断版本链中的每个版本是否可见:
- 如果数据版本的创建者事务ID 小于 最小活跃事务ID,那么这个版本是可见的,因为创建它的事务在ReadView创建之前就已经提交了。
- 如果数据版本的创建者事务ID 大于或等于 最大事务ID,那么这个版本是不可见的,因为创建它的事务是在ReadView创建之后开始的。
- 如果数据版本的创建者事务ID在 最小活跃事务ID 和 最大事务ID 之间:
- 如果创建者事务ID 在 活跃事务ID列表 中,说明这个事务在ReadView创建时还未提交,因此版本不可见。
- 如果创建者事务ID 不在 活跃事务ID列表 中,说明这个事务在ReadView创建时已经提交,因此版本可见。
RR解决幻读
一般来说,可重复读隔离级别下还是会存在幻读问题,但是各个具体的数据库可以自定义的实现对应的事务隔离级别。在 MySQL 中,通过 MVCC 和临键锁 可以在可重复读隔离级别下解决幻读的问题,接下来说一下 MySQL 如何解决幻读问题
1、执行普通 select
,此时会以 MVCC
快照读的方式读取数据
在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View
,并使用至事务提交。所以在生成 Read View
之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
2、执行 select...for update / lock in share mode、insert、update、delete 等当前读
在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB
使用 Next-key Lockopen in new window 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。
通过以上方式,可以解决可重复读隔离级别的幻读问题,但是在一些极端场景下还是会发生幻读。
锁
有哪些锁
全局锁、表级锁(表锁、意向锁、元数据锁、自增锁)、行级锁
表锁:
//表级别的共享锁,也就是读锁;
lock tables t_student read;
//表级别的独占锁,也就是写锁;
lock tables t_stuent write;
//释放表锁
unlock tables
意向锁:
事务A锁住了表中的一行,让这一行只能读,不能写。
之后,事务B申请整个表的写锁。
如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁冲突。这时就需要意向锁,来判断表中是否含有行级锁
- 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」;
- 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」;
意向锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独占表锁(lock tables ... write)发生冲突。
元数据锁:
针对表结构的锁,比如给一张表添加字段,不需要显式调用
- 对一张表进行 CRUD 操作时,加的是 MDL 读锁;
- 对一张表做结构变更操作的时候,加的是 MDL 写锁;
死锁
日志
各个日志的作用
undolog
记录了需要回滚的信息(事务开始前的数据)
过程:在事务没提交之前,记录更新前的数据到 undo log 日志文件里,当事务回滚时,利用 undo log 来进行回滚
作用:回滚、MVCC(undolog版本链)
Buffer Pool
读:缓冲池读未命中,到磁盘读,并加载一页到缓冲池中
写:直接在缓冲池中更新,将页设置为脏页,通过 刷盘策略 写到磁盘
索引页、数据页、Undo 页,插入缓存页、自适应哈希索引、锁信息等等
redolog
物理日志,记录了某个数据页做了什么修改(事务提交后的数据)
作用:
MySQL:为什么要引入redo log机制_为什么要有redolog-CSDN博客
不可能每次事务一提交,就把事务更新的缓存页都刷新到磁盘文件里去。因为缓存页刷新到磁盘文件里,是随机磁盘读写,性能会很差,导致数据库性能和并发能力很弱
持久化,用于故障重启后恢复,防止缓冲页丢失;将写操作从「随机写」变成了「顺序写」,提升 MySQL 写入磁盘的性能
- 为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来,这个时候更新就算完成了。后续,由后台线程将缓存在 Buffer Pool的脏页刷新到磁盘里,这就是WAL(Write-Ahead Logging)技术
- 写入 redo log 的方式使用了追加操作,所以磁盘操作是顺序写
redolog先写入到内存的redolog buffer中,再根据刷盘策略 刷盘
问题:事务提交的时候把修改过的缓存页刷入磁盘,跟事务提交的时候把redo log写入日志文件,它们不都是写磁盘吗?差别在哪里?
- 缓存页数据比较大,刷入磁盘比较耗时,而且可能就修改了缓存页里的几个字节的数据,每次事务提交都刷盘,会对性能造成很大压力;而redo log数据小,写入磁盘速度很快
- 而且缓存页刷入磁盘是随机写磁盘,性能很差;写日志是一个顺序写磁盘,每次都是追加到磁盘文件末尾去,速度也是很快的
- 虽然最终数据还是需要刷盘,但是有redolog的存在,可以延迟或者合并数据页的刷盘操作,或者说减少操作,提高了性能
刷盘时机:正常关闭、后台线程、内存空间一半、事务提交
事务提交时的刷盘策略:不刷盘、写入pagecache并刷盘、只写入pagecache
日志文件组:循环写,write pos:记录下一次写的开始位置,checkpoint:记录擦除位置
每次刷盘 redo log
记录到日志文件组中,write pos
位置就会后移更新。
每次 MySQL
将脏页刷新到磁盘 或者 加载日志文件组恢复数据 时,会清空加载过的 redo log
记录,并把 checkpoint
后移更新。
日志文件组满了:write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被 阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要 ),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘(是更新数据库数据,不是更新redolog)中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动,然后 MySQL 恢复正常运行,继续执行新的更新操作
binlog
记录了修改表数据的SQL语句
行格式:statement、row、mixed
- statement记录的是sql语句,有动态函数问题(比如时间)
- row记录的是数据行的实际变化
主从复制过程:
主库事务提交,写入binlog,更新数据
从库开启IO线程,连接主库的log dump线程,读取binlog日志,写入从库的中继日志
从库开启新的线程,执行中继日志的SQL语句
刷盘时机:提交事务时
刷盘策略:只写入pagecache、写入pagecache并刷盘、写入pagecache但积累N个事务后再刷盘
update语句更新过程
MySQL执行更新的流程_mysql更新事务流程-CSDN博客
加载数据,加锁
两阶段提交:避免出现两份日志之间(redo和binlog)的逻辑不一致的问题
redolog和binlog区别:
实现层、记录的东西、用途、写方式、刷盘时机
优化
慢sql定位
慢查询日志 查看超过执行时间的语句
explain 查看语句的执行计划
show profiles 查看语句的执行时间、CPU消耗
索引、sql、数据库优化
高性能
分库分表
为什么,方式(水平:根据分片算法拆分记录到多个表;垂直:按照业务场景拆分字段)
分片算法、问题、数据迁移
读写分离
主写读从,主从复制原理
主从延迟原因、减少、避免
⭐Redis
基础
redis为什么快
内存、数据类型、单线程多路io复用
数据类型及场景
❗数据类型实现
字符串、字典(结构、哈希冲突、rehash时机、渐进式哈希)、zset(结构、查询过程、优点)
线程模型
单线程模式
IO多路复用:接收请求 (epoll机制:可以监听多个socket)
单线程:命令处理
多线程:处理网络IO(read、write)
后台线程:关闭文件、AOF 刷盘(2.6之后)、unlink释放内存(4.0)
IO线程:处理网络IO
Redis到底是单线程还是多线程?
- 如果仅仅聊Redis的核心业务部分(命令处理请求),答案是单线程
- 如果是聊整个Redis,那么答案就是多线程(关闭文件、AOF 刷盘、unlink释放内存等耗时操作,避免阻塞主线程),BIO
在Redis版本迭代过程中,在两个重要的时间节点上引入了多线程的支持:
- Redis v4.0:引入多线程异步处理一些耗时较长的任务,例如异步删除命令unlink
- Redis v6.0:在核心网络模型中引入 多线程,进行IO读写操作,进一步提高对于多核CPU的利用率
为什么使用单线程/为什么单线程还快
为什么引入多线程
用在网络IO的处理上,提高网络 IO 读写性能
❗IO模型
等待数据、数据复制(内核缓冲区->用户缓冲区)
❗IO多路复用
IO多路复用是一种高效的IO处理方式,它允许单个进程或线程同时监视多个文件描述符,如网络连接或文件句柄。当这些描述符中的任何一个就绪时,比如有数据可读或可写,多路复用机制就能够通知应用程序进行相应的读写操作。这种机制的核心优势在于,它可以在不增加额外线程或进程的情况下,处理大量的并发连接,从而显著地提高系统的并发性和响应能力。
常见的IO多路复用技术包括select、poll和epoll等,核心思想都是通过一个线程来管理多个连接,减少系统资源的消耗,并提高程序运行的效率。
IO多路复用监听方式
持久化
RDB和AOF的定义、优缺点 -> AOF的写入过程 -> 混合持久化
RDB:快照副本,占用空间小、恢复快、不能实时或者秒级持久化数据、丢失数据风险高
AOF:命令追加,占用空间大、恢复慢、支持秒级数据持久化、丢失数据风险低
RDB:将某一时刻的内存数据,以二进制的方式写入磁盘
AOF:将写操作命令,以追加的方式写入到文件里
AOF写后日志
AOF过程
AOF缓冲区 -> 内核缓冲区page cache -> 磁盘 -> 重写
AOF写回策略
always直接刷盘,everysec后台线程每秒fsync,no由操作系统控制
❗AOF重写机制
在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件
❗AOF重写过程
后台子进程 bgrewriteaof:触发重写机制后,主进程会创建重写 AOF 的子进程,此时父子进程共享物理内存,子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到 新的 AOF 文件
重写过程中,主进程依然可以正常处理命令,如果主进程修改了已经存在的数据,那么会发生写时复制,在副本上进行修改。同时 为了避免父子进程的数据不一致,父进程将执行后的写命令同时追加到 「AOF 缓冲区」和「AOF 重写缓冲区」
当子进程完成 AOF 重写工作后,会向主进程发送一条信号(信号是进程间通讯的一种方式,且是异步的)。主进程将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,新的 AOF 的文件进行改名,覆盖现有的 AOF 文件
RDB阻塞主进程?
RDB过程能否修改数据?
混合持久化
AOF重写过程,子进程RDB,父进程AOF追加。所以AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
主从复制
定义
解决并发问题、读写分离
方式
全量:第一次同步以及断开时间长导致数据覆盖,三步(建立连接、发送RDB加载数据、执行第二阶段中的写命令 repl_backlog)
增量:两步(恢复连接、发送 repl_backlog中的写命令 执行)
哨兵机制
解决主节点故障恢复问题 ,工作机制(三步):
监控:心跳ping,主观客观下线
选举领导者:选举一个哨兵领导者(Raft算法)
故障转移:由领导者从从节点中选出一个新的主节点(优先级,数据复制进度offset,运行id),让其它节点成为新master的从节点
通知:将新的主节点通知给redis客户端
集群
解决什么问题
主节点写请求多或者数据量大,需要扩展
结构
多个master,多个slave,心跳检测
❗实现
- 采用数据分区(哈希槽分区)来实现数据分布式存储:将 数据 映射到 哈希槽 上( CRC16(key)%16384 ),集群中的每个节点负责一部分哈希槽的管理,可以实现动态扩容缩容
- 采用自动故障转移来实现高可用:Redis集群的故障转移和哨兵的故障转移类似,但是Redis集群中所有的节点都要承担状态维护的任务。
其它
重定向
内存管理
❗过期删除原理和策略
定时删除(设置过期时间)、惰性删除、定期删除
定时删除策略的做法是,在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。
redis采用「惰性删除+定期删除」
redis定期删除:
1. 从过期字典中随机抽取 20 个 key
2. 检查这 20 个 key 是否过期,并删除已过期的 key
3. 如果本轮检査的已过期 key 的数量,超过5个,也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。4. Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
内存淘汰机制
缓存设计
缓存击穿、穿透、雪崩
缓存更新策略
数据库和缓存数据一致性
1. 先更新数据库再删除缓存
删除失败导致数据不一致怎么办
2. 延迟双删(解决 先删除缓存再更新数据库 数据不一致性)
删除缓存 -> 更新数据库 -> 睡眠 -> 删除缓存
3. 先更新数据库再更新缓存:(怎么解决数据不一致?/ 双写一致性)
Redis优化
bigkeys、hotkeys
⭐设计模式
单例模式:私有构造函数、私有共享变量、公共静态方法获取实例
工厂方法模式:抽象产品、具体产品、抽象工厂、具体工厂
抽象工厂模式:一个工厂生产一类产品
代理模式:抽象目标、目标对象、代理对象。(用于增添一些自定义功能,扩展)
策略模式:抽象策略、具体策略、执行策略的类
观察者模式(发布-订阅):发布者接口、具体发布者、观察者接口、具体观察者
适配器模式:原接口、目标接口、适配器(将一个类的接口转换成客户希望的另一个接口,适配器实现目标接口,重写目标接口方法,在该方法中调用原接口方法)
代理模式的主要作用是为了控制真实对象的访问,在客户端和被代理对象之间引入一个代理对象,代理对象可以在被代理对象之前或之后对真实对象的请求进行一些额外的处理。换句话说,代理对象充当了一个类似于阀门的角色,将客户端的请求先传递到代理对象上,再由代理对象转发给真实对象进行处理。在代理模式中,最常见的实现方式是静态代理和动态代理。
装饰模式是一种行为型模式,其主要作用是动态地为对象添加一些功能,在不改变原有对象的结构的情况下,增加一些新的功能或行为。可以将装饰模式看作是对继承关系的一种替代方法。
innodb的存储结构
行、页(16KB)、区(1M,64页)、段
一行有65535个字节(包括可变长字段长度,NULL列表、真实数据,不包含隐藏字段)
TCP、UDP首部