过分高估自己了,每日5题,完成不了,时间不够,因为对每一个知识点深入查缺补漏,然后记住需要的时间很多
1、详细说说类加载器
启动类加载器(Bootstrap Class Loader):
启动类加载器负责加载存放在<JAVA HOME>\lib
目录,或者被Xbootclasspath参数所指定的路径中存放的,而且是java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器的时候,如果需要把加载请求委派给启动类加载器取处理,那直接使用null代替即可
扩展类加载器(Extension Class Loader):
这个类加载器是在类sum.misc.Launcher$ExtClassLoader
中以Java代码的形式实现的。它负责加载<JAVA_HOME>\lib\ext
目录中,或者被java.ext.dirs
系统变量所指定的路径中所有的类库
应用程序类加载器(Application Class Loader):
这个类加载器由sum.miscLauncher$AppClassLoader
来实现。由于应用程序类加载器是ClassLoader
中的getSystemClassLoader()
方法的返回值,所以有些场合也称它位“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中的默认类加载器。
2、Synchronize关键字的底层原理
synchronized 关键字底层原理属于 JVM 层面。
第一种情况:synchronized 同步语句块的情况
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过 JDK 自带的 javap
命令查看 SynchronizedDemo
类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java
命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class
。
从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
当执行 monitorenter
指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。
在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitor实现的。每个对象中都内置了一个
ObjectMonitor
对象。另外,
wait/notify
等方法也依赖于monitor
对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify
等方法,否则会抛出java.lang.IllegalMonitorStateException
的异常的原因。
在执行monitorenter
时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
第二种情况:synchronized 修饰方法的的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized 方法");
}
}
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED
访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。
总结:
synchronized
同步语句块的实现使用的是 monitorenter
和 monitorexit
指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置。
synchronized
修饰的方法并没有 monitorenter
指令和 monitorexit
指令,取得代之的确实是 ACC_SYNCHRONIZED
标识,该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。
答案参考javaguide
衍生问题:
- synchronized的原理(从ObjectMonitor角度分析)
3、MyISAM和InnoDB的区别
-
InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
-
InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
-
InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是非聚簇需要两次查询,先查询到主键,然后再通过主键查询到数据。而 MyISAM 是非聚集索引,值得注意的是数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
InnoDB的非聚簇索引data域存储相应记录
主键的值
,而MyISAM索引记录的是地址
-
InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
-
InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
答案参考知乎:
https://www.zhihu.com/question/20596402
4、为什么Redis比较快,单线程就一定快吗?
第一个问题为什么redis这么快
- 基于内存实现
我们都知道内存读写是比磁盘读写快很多的。Redis是基于内存存储实现的数据库,相对于数据存在磁盘的数据库,就省去磁盘磁盘I/O的消耗。MySQL等磁盘数据库,需要建立索引来加快查询效率,而Redis数据存放在内存,直接操作内存,所以就很快。
- 高效的数据结构
我们知道,MySQL索引为了提高效率,选择了B+树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。
redis设计到的数据结构如下:
SDS简单动态字符串
1、常数复杂度获取字符串长度
由于 len 属性的存在,我们获取 SDS 字符串的长度只需要读取 len 属性,时间复杂度为 O(1)。而对于 C 语言,获取字符串的长度通常是经过遍历计数来实现的,时间复杂度为 O(n)
2、减少修改字符串的内存重新分配次数
在C语言中,修改一个字符串,需要重新分配内存,修改越频繁,内存分配就越频繁,而分配内存是会消耗性能的。而在Redis中,SDS提供了两种优化策略:空间预分配和惰性空间释放。
- 空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。
- 惰性空间释放:当SDS缩短时,不是回收多余的内存空间,而是用free记录下多余的空间。后续再有修改操作,直接使用free中的空间,减少内存分配。
哈希
Redis 作为一个K-V的内存数据库,它使用用一张全局的哈希来保存所有的键值对。这张哈希表,有多个哈希桶组成,哈希桶中的entry元素保存了*key
和*value
指针,其中*key
指向了实际的键,*value
指向了实际的值。
哈希表查找速率很快的,有点类似于Java中的HashMap,它让我们在O(1) 的时间复杂度快速找到键值对。首先通过key计算哈希值,找到对应的哈希桶位置,然后定位到entry,在entry找到对应的数据。
Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。
为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。
跳跃表
跳跃表是Redis特有的数据结构,它其实就是在链表的基础上,增加多级索引,以提高查找效率。跳跃表的简单原理图如下
- 每一层都有一条有序的链表,最底层的链表包含了所有的元素。
- 跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
压缩列表ziplist
压缩列表ziplist是列表键和字典键的的底层实现之一。它是由一系列特殊编码的内存块构成的列表, 一个ziplist可以包含多个entry, 每个entry可以保存一个长度受限的字符数组或者整数,如下:
由于内存是连续分配的,所以遍历速度很快
各个字段详细解释见redis笔记
- 合理的数据编码
Redis支持多种数据基本类型,每种基本类型对应不同的数据结构,每种数据结构对应不一样的编码。为了提高性能,Redis设计者总结出,数据结构最适合的编码搭配。
Redis是使用对象(redisObject)来表示数据库中的键值,当我们在 Redis 中创建一个键值对时,至少创建两个对象,一个对象是用做键值对的键对象,另一个是键值对的值对象。
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//指向底层数据结构的指针
void *ptr;
//引用计数
int refcount;
//记录最后一次被程序访问的时间
unsigned lru:22;
}robj
redisObject中,type 对应的是对象类型,包含String对象、List对象、Hash对象、Set对象、zset对象。encoding 对应的是编码。
-
String:如果存储数字的话,
int 编码
:保存的是可以用 long 类型表示的整数值。raw 编码
:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。embstr 编码
:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。 -
List:
当同时满足下面两个条件时,使用ziplist(压缩列表)编码:
1、列表保存元素个数小于512个
2、每个元素长度小于64字节
不能满足这两个条件的时候使用 linkedlist 编码。
-
Hash:
当同时满足下面两个条件时,使用ziplist(压缩列表)编码:
1、列表保存元素个数小于512个
2、每个元素长度小于64字节
不能满足这两个条件的时候使用 hashtable 编码
-
Set:
当集合同时满足以下两个条件时,使用 intset 编码:
1、集合对象中所有元素都是整数
2、集合对象所有元素数量不超过512
不能满足这两个条件的就使用 hashtable 编码。
-
Zset:
当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:
1、保存的元素数量小于128;
2、保存的所有元素长度都小于64字节。
不能满足上面两个条件的使用 skiplist 编码。
-
合理的线程模型
单线程模型:避免了上下文切换
Redis是单线程的,其实是指Redis的网络IO和键值对读写是由一个线程来完成的。但Redis的其他功能,比如持久化、异步删除、集群数据同步等等,实际是由额外的线程执行的。
Redis的单线程模型,避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。Redis是面向快速执行场景的内存数据库,所以要慎用如lrange和smembers、hgetall等命令。
I/O 多路复用
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
- 虚拟内存机制
Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
答案整理参考博客地址:
- https://blog.csdn.net/hollis_chuang/article/details/118865058
- https://www.cnblogs.com/ysocean/p/9102811.html#_label0
- https://www.codenong.com/cs106843764/
第二个问题单线程就一定快吗?
不是的
举例说明:
- 当你往 Redis 中写入大量数据后,就可能发现操作有时候会突然变慢了,可能是由于哈希表的冲突问题和 rehash 可能带来的操作阻塞。(当然redis为了解决这个问题提出了相应的解决方案)
- 我们在进行AOF日志和RDB日志文件的时候就可能由于数据量过大导致fork子进程的时候导致主线程阻塞,从而让其他操作变慢
5、三次握手四次挥手过程
三次握手过程
三次握手(Three-way Handshake)指客户端和服务器建立一个TCP连接时,双方总共需要发送3个报文段。目的:
- 确认双方的接收能力和发送能力是否正常
- 同时指定双方的初始化序列号(ISN)为后面的可靠性传送做准备
刚开始服务端处于监听状态,进行三次握手由客户端主动发起:
- 第一次握手:客户端给服务端发一个
连接请求报文段
,头部指明SYN=1
,以及初始化序列号ISN
(seq=x
)。此报文段不能携带数据,但要消耗掉一个序号。随后客户端进入SYN_SENT
(同步发送)状态。 - 第二次握手:服务端收到客户端的
连接请求报文段
之后,向客户端发送连接确认报文段
,头部指明SYN=1
,ACK=1
,确认号(ack
)为x+1
,并且也选择一个初始化序列号y
。随后服务器进入SYN_RCVD
(同步接收)的状态。 - 第三次握手:客户端收到服务端的
连接确认报文段
之后,会向服务端回送一个确认报文段
,头部指明ACK=1
,确认号ack=y+1
,序号seq=x+1
,该报文段可以携带数据,不携带数据则不消耗序号。随后客户端进入ESTABLISHED
(连接已建立)状态。待服务器收到客户端发送的ACK
报文段也会进入ESTABLISHED
状态,完成三次握手。
四次挥手的过程
由于客户端或服务端均可主动发起挥手动作,因此这里称主动方和被动方。四次挥手(Four-way handshake)指主动方和和被动方断开 TCP
连接需要发送四个包报文段。挥手前,双方都处于ESTABLISHED
状态,假如是客户端先发起关闭请求,对应过程如下:
- 第一次挥手:客户端向服务端发送一个
连接释放报文段
,头部指明FIN=1
,序号seq=u
。并停止发送数据,主动关闭TCP
连接。随后客户端进入FIN_WAIT1
(终止等待1)状态,等待服务端的确认。 - 第二次挥手:服务端收到客户端发来的
连接释放报文段
后,回送确认报文段
,头部指明ACK=1
,确认号ack=u+1
,序号seq=v
,随后服务端进入CLOSE_WAIT
(关闭等待) 状态。客户端收到服务端的确认报文段
后,进入FIN_WAIT2
(终止等待2)状态,等待服务端发出的连接释放报文段
。 - 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,向客户端发送
连接释放报文段
,头部指明FIN=1
,ACK=1
,序号seq=w
,确认号ack=u+1
,随后服务端进入LAST_ACK
(最后确认)状态,等待客户端的确认报文段
。 - 第四次挥手:客户端收到
连接释放报文段
之后,同样向服务端发出确认报文段
,头部指明ACK=1
,seq=u+1
,ack=w+1
,此时客户端进入TIME_WAIT
状态。服务端收到客户端的确认报文段
之后,进入CLOSED
状态。客户端必须经过2*MSL
后才进入CLOSED
状态。此时TCP
连接已经完全释放。