MySql关系型数据库
1、红黑树,B+,B,二叉树
二叉树可能会退化成链表
红黑树是平衡二叉树的一种,避免退化成链表,但数据规模扩大时,树高会增大,查找效率下降
B树每个节点中有data域,会增加节点的大小,在单次IO吞吐量不变情况,会增加磁盘IO的次数,从而增加耗时;B+的data域存储在叶子结点中,非叶子节点存储key,节点小,IO次数少
B树不适合区间访问,B+树叶子节点之间通过双向指针连接,便于区间访问

2、索引
聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
非聚簇索引:存储其他索引,叶子节点存储主键,用于回表(回到聚簇索引的B+树寻找元组)

聚簇索引的优势:
- 由于行数据和叶子节点存储在一起,同一页中会有多条行数据,访问同一数据页不同行记录时,已经把页加载到了Buffer中,再次访问的时候,会在内存中完成访问,不必访问磁盘。这样主键和行数据是一起被载入内存的,找到叶子节点就可以立刻将行数据返回了。
3、优化
- 降低单次查询范围,分多次查询,确定走索引再操作
- 聚合索引采用最左匹配原则
- 多个字段排序,可以在多个排序字段上添加联合索引来优化排序语句
- 去掉不必要的返回字段,不然优化器认为全表扫描优于索引查找
4、ACID
事务是一组逻辑操作,要么都执行,要么都不执行
原子性:事务是不可分割的整体操作,要么都执行,要么都不执行
一致性:事务执行前后,数据保持一致,多个事务对同一个数据读取的结果是相同的
隔离性:各并发事务之间是相互独立的,一个事务不会被其他事务干扰
持久性:事务提交,对数据库的改变是持久的,即使数据库故障也不会影响
5、并发事务带来的影响
脏读:事务A读取到事务B未提交的数据
丢失修改:事务A和事务B同时获取到a=10,A:a=a-1——>B:a=a-2,得到的结果是a=8,丢失了事务A的修改结果
不可重复读:事务A多次读取同一个数据,在这个过程中,事务B对这个数据进行了修改,导致事务前后读取的数据不同
幻读:和不可重复读类似,区别在于幻读的重点是读取到新增加或删除的数据
6、隔离级别
READ-UNCOMMITTED:允许读取未提交的数据变更
READ-COMMITED:允许读取已提交的数据变更
REPEATABLE-READ:对同一字段的多次读取结果一致,除非数据被本身事务修改
SERIALIZABLE:所有事务逐个执行,事务之间完全不干扰

默认采用REPEATABLE-READ隔离级别
为什么说MySql的默认隔离级别是REPEATABLE-READ却可以保证事务的隔离性?
MySql的REPEATABLE-READ已经可以解决脏读,不可重复读的问题,对于幻读采取Next-Key Lock锁解决
7、MVCC是什么?
(MVCC说到底是一种思想,各大数据库实现的方式也不尽相同,你如果不满意数据库的MVCC设计,你也可以在代码层面自己实现乐观锁从而实现多版本的并发控制等等。)
早期数据库都是采用锁机制,但锁会带来性能问题,于是人们尝试优化,MVCC就是对数据库读操作的优化
MVCC的意思用简单的话讲就是对数据库的任何修改的提交都不会直接覆盖之前的数据,而是产生一个新的版本与老版本共存,使得读取时可以完全不加锁。这样读某一个数据时,事务可以根据隔离级别选择要读取哪个版本的数据。过程中完全不需要加锁。
读已提交和可重复读的底层原理就是MVCC
数据库MVCC和隔离级别的关系是什么?
我以为我对Mysql事务很熟,直到我遇到了阿里面试官
8、MVCC的实现
undo log:用于事务回滚和实现多版本
redo log:MySql使用缓存,修改内存,不会立刻修改磁盘,事务中不断产生redo log,提交flush,保存到磁盘中。即使数据库在事务提交后故障,也会根据redo log恢复事务中数据修改
- InnoDB行记录中除了rowid外,还有trx_id和db_roll_ptr。 trx_id表示最近修改的事务的id;db_roll_ptr指向undo segment中的undo log。在更新和删除操作完成后,新的数据行中的db_rool_ptr指向undo segment中的undo log,从而实现事务回滚和多版本。
ReadView(一致性视图):RC和RR的区别在于ReadView的策略不同
- up_limit_id:当前已经提交的事务号 + 1,事务号 < up_limit_id ,对于当前Read View都是可见的。
- low_limit_id:当前最大的事务号 + 1,事务号 >= low_limit_id,对于当前Read View都是不可见的。
- trx_ids:为活跃事务id列表,即Read View初始化时当前未提交的事务列表。

对于read_committed,每次读取都会生成一个新的ReadView
对于repeatable_read,会复用事务第一次生成的ReadView
9、介绍一下MySql的锁机制
==表锁,行锁;3种行锁;读写锁;意向锁;关于锁的sql语句==
MyISAM——表锁
InnoDB——表锁,行锁(默认)
表锁:粒度大,加锁快,实现简单,触发冲突的概率大,并发度小
行锁:粒度小,加锁慢,减少数据库操作冲突,并发度大
InnoDB的3种行锁算法:
- record lock:单个行记录上锁
- gap lock:间隙锁,区间上锁,不包括记录本身
- next-key lock:record+gap(RR+next-key lock解决幻读问题)
读写锁:
- S锁(共享锁,读锁),得到S锁的事务只能读,不能写,其他事务可以为数据对象加S锁,但不能加X锁
- X锁(排他锁,写锁),得到S锁的事务可读写,其他事务不能加S锁,也不能加X锁,直到原始X锁释放

不同sql语句对加锁的影响:
- SELECT ... 语句正常情况下为快照读,不加锁(默认);
- SELECT ... LOCK IN SHARE MODE 语句为当前读,加 S 锁;
- SELECT ... FOR UPDATE 语句为当前读,加 X 锁;
- 常见的 DML 语句(如 INSERT、DELETE、UPDATE)为当前读,加 X 锁;
- 常见的 DDL 语句(如 ALTER、CREATE 等)加表级锁,且这些语句为隐式提交,不能回滚。
10、RR配合锁机制如何解决幻读?
这里的锁机制指的是next-key lock
==next-key lock=record lock+gap lock==
在事务第一次select * from user where id>1 for update(使用X锁,强制当前读,id是主键)
这个时候根据在B+树上查询到的数据==区间加锁==,这里是(1,+inf),使得后续操作无法插入到这个区间中,从而解决幻读问题。
11、其他
1.InnoDB和MyISAM的区别
- InnoDB支持行锁,MyISAM仅支持表锁
- InnoDB支持外键,MyISAM不支持
- InnoDB支持事务,MySIAM不支持
- InnoDB支持MVCC,MySIAM不支持
该用谁,不用我多说了吧!
2.三大范式
- 第一范式:原子性,每个数据表的字段都是不可分割的
- 第二范式:第一范式基础上,避免部分依赖:主键(A,B,C);D字段只依赖A字段,这就是部分依赖
- 第三范式:第二范式基础上,避免传递依赖:主键(A);B字段依赖A字段,C字段依赖B字段,即C==>B==>A,这就是传递依赖
12、索引在什么情况下会失效
- 条件中有or,例如select * from table_name where a = 1 or b = 3
- 必须所有的or条件都必须是独立索引,才会使用到索引
- 在索引上进行计算会导致索引失效,例如select * from table_name where a + 1 = 2
- 在索引的类型上进行数据类型的隐形转换,会导致索引失效,例如字符串一定要加引号,假设 select * from table_name where a = '1' 会使用到索引,如果写成select * from table_name where a = 1 则会导致索引失效。
- 在索引中使用函数会导致索引失效,例如select * from table_name where abs(a) = 1
- 在使用like查询时以%开头会导致索引失效
- 索引上使用!=、<>进行判断时会导致索引失效,例如select * from table_name where a != 1
- 索引字段上使用 is null/is not null判断时会导致索引失效,例如select * from table_name where a is null
13、SQL的优化
一个 ==SQL 执行的很慢==,我们要分两种情况讨论:
-
大多数情况下很正常,偶尔很慢,则有如下原因
- 数据库在刷新脏页,例如 redo log 写满了需要同步到磁盘。
- 执行的时候,遇到锁,如表锁、行锁。(
show processlist
)
-
这条 SQL 语句一直执行的很慢,则有如下原因。
- 没有用上索引:例如该字段没有索引;由于对字段进行运算、函数操作导致无法用索引。
- 由于统计的失误,导致系统没有走索引,而是走了全表扫描
select * from t force index(a) where c < 100 and c < 100000;
analyze table t;
优化策略:
14、面试题
- Redis中的数据类型有哪些?
- Redis中的String类型底层是如何实现的?
- 使用Redis进行数据统计,在高并发的情况下会不会有问题?
- 数据库的三大范式
- 数据库事务的特性 ==(4)==
- 事务是如何实现隔离性的? ==(6,7,10)==
- 引入MVCC机制是为了实现什么? ==(7,8)==
- B树和B+树的区别,能不能用二叉搜索树作为数据库的索引? ==(1)==
- B+树对比B树的好处
- 乐观锁与悲观锁的区别
- 幻读是什么?MySQL怎么解决幻读
- MySQL锁,解决幻读,怎么实现可重复读
SQL题目
四个表,学生表(学号,姓名)、成绩表(学号,课程号,成绩)、课程表(课程号、课程名、教师号)、教师表(教师号、教师名)
查出所有平均成绩大于60的学生学号和平均成绩
15、思维导图
计算机网络
1、TCP和UDP的区别
- TCP面向连接,UDP无连接
- TCP提供可靠服务,UDP不保证
- TCP面向字节流,UDP面向报文
- TCP传输速度慢,UDP快
2、TCP三次握手

3、为什么需要3次握手?
为了确保客户端和服务端的可靠通信,⽽三次握⼿最主要的⽬的就是双⽅确认⾃⼰与对⽅的发送与接收是正常的
第一次:服务端知道客户端发送正常,自己接收正常
第二次:客户端知道服务端发送和接收正常,自己接收,发送正常
第三次:服务端知道客户端接收正常,自己发送正常
4、回传SYN?
通知客户端,服务端接收到的消息确实是客户端传来的,起到一校验的作用。
5、四次挥手

6、为什么四次挥手
原因参照三次握手
中间ACK和FIN分开发送的原因:发送FIN是表示自己不再发送信息,,但还是会接收信息,而服务端此时可能还有数据未传输完,所以FIN和ACK分开发送
7、客户端为什么等待2MSL
保证客户端的最后一个ACK能够到达服务端,考虑服务器的FIN在传输过程中丢失,等待服务端重发FIN,从丢失到重发,最长需要2MSL时间,所以需要等待2MSL。
8、TCP如何保证可靠传输
- TCP会分隔应用数据,把它分割成最适合发送的数据块
- TCP对包编号,接收方对数据包排序
- 校验和,通过校验和判断传输过程中数据是否产生变化
- TCP会丢弃重复数据
- 流量控制:TCP利用滑动窗口实现,TCP接受端只允许发送端发送接收端缓冲区能接纳的数据,如果来不及接受,提示对方降低发送速率,防止丢包
- 拥塞控制
- ARQ协议
- 超时重传,开启定时器,等待确认报文,无法及时收到就重发报文
9、拥塞控制
TCP发送方维持一个拥塞窗口
拥塞控制的四种算法:
- 慢开始
- 拥塞避免
- 快重传
- 快恢复

10、url到显示主页的过程
在浏览器地址栏输入一个URL后回车,背后会进行哪些技术步骤?
前提条件:网址是https,但我们使用默认的http访问
url合法性检测——》检查本地DNS缓存,检查本地host文件——》DNS服务器查询域名对应的IP地址——》ARP翻译IP地址成MAC地址——》数据传输——》到达服务器——》服务器告知https的网址——》重复上述过程,不过中间会添加CA证书验证,TLS/SSL的加密等等——》服务器接受到浏览器的请求,回显页面
11、HTTP1.0,1.1,2.0的区别
1.0和1.1的区别
- 1.0短连接;1.1长连接
- 1.1新增22个错误状态码
- 1.1引入更多缓存策略
- 带宽优化及网络连接的使用,1.0可能只需要某个对象的部分,而服务器却传输整个对象;1.1引入range字段,允许只请求部分资源
1.X和2.0的区别
- 新的二进制格式,HTTP1.X解析基于文本,文本表现具有多样性
- 多路复用
- HTTP/1.1 Pipeling解决方式为,若干个请求排队串行化单线程处理,后面的请求等待前面请求的返回才能获得执行机会,一旦有某请求超时等,后续请求只能被阻塞,毫无办法,也就是人们常说的线头阻塞;
- HTTP/2多个请求可同时在一个连接上并行执行。某个请求任务耗时严重,不会影响到其它连接的正常执行;
具体如图:

- 服务端推送
- 服务端推送能把客户端所需要的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。

12、URI和URL的区别
URI:统一资源标识符,可以唯一标识一个资源
URL:统一资源定位符,可以唯一定位一个资源,是一种具体的URI
13、HTTP和HTTPS的区别
- 默认端口:http80,https443
- 安全性能:http建立在TCP/IP上,明文传输;https建立在SSL/TSL上,SSL/TSL建立在TCP/IP上,采用对称加密和非加密结合的方式加密数据,安全性能更高,但性能消耗也越大
14、IO模型
100%弄明白5种IO模型
阻塞IO

IO多路复用
复用IO的基本思路就是通过select或poll、epoll 来监控多fd ,来达到不必为每个fd创建一个对应的监控线程,从而减少线程资源创建的目的。

信号驱动IO模型

异步IO

JVM
JVM内存模式和Java内存模型的关系:JVM内存模型是对Java内存模型的实现。
全面理解Java内存模型
1、 了解JVM内存区域嘛?
JDK1.8的JVM内存模型

线程共享
线程私有
- 虚拟栈
- 由一个个栈帧组成
- 栈帧中都有:局部变量表,操作数栈,动态链接,方法出口信息
- 本地方法栈
- 程序计数器
- 字节解释器通过程序计数器读取指令,实现代码流程控制
- 多线程情况下,程序计数器用于记录当前线程执行的位置
2、说说JDK1.6和JDK1.8对JVM的实现差别
方法区:
- JDK1.8摒弃了方法区,改成在直接内存中使用元空间
- 原方法区的作用:存储被虚拟机加载的类信息,常量,静态变量,即使编译器编译后的代码等数据
- 方法区,又被称为永久带,非堆(Java虚拟机规范中有方法区的概念,而hotspot使用永久带的方法实现了方法区这个概念,关系好比接口和实现类的关系)
常量池:
- JDK1.8 hotspot移除了永久代⽤元空间**(Metaspace)取⽽代之,** 这时候字符串常量池还在堆**,** 运⾏时常量池还在⽅法区**,** 只不过⽅法区的实现从永久代变成了元空间**(Metaspace)**
- 运行时常量池中存储的是对象的引用,真正的对象还是存储在堆中
3、Java对象创建的过程
-
类加载检查。检查类是否被加载,解析和初始化过
-
分配内存。对象所需的内存大小在类加载完成后便可确定。
- 指针碰撞(内存规整,挪动指针即可)
- 空闲列表(内存不规整,需要遍历寻找适合大小的内存块)
-
初始化0值。区分于构造函数,初始化0值指的是内存方面的初始化
-
设置对象头。对象头中包含哈希码等等
-
执行init方法。根据构造函数初始化
(分配内存会有线程安全问题,但为了在面试时不把自己坑了,还是不说了,但其中涉及的CAS了解一下)
CAS(比较并交换):乐观锁的一种实现
4、Java对象定位的方法
- 句柄
- 栈中的引用存储句柄地址,句柄存储对象实例地址和对象类型地址
- 直接指针
- 栈中引用存储对象实例地址,对象实例中还存储对象类型的地址
5、如何判读对象是否死亡
强引用:对于强引用对象,就算出现OOM也不会回收对象
软引用:内存不足,JVM会自动回收软引用对象
弱引用:只要GC一运行,不管内存是否足够,都会回收该对象的内存
虚引用:主要作用是跟踪对象被垃圾回收的状态
- 可达性分析算法
- GC Roots作为根节点,如果GC Roots无法到达该对象代表的节点,则代表对象可以被销毁
- 可作为GC Roots:线程栈变量,静态变量,常量池,JNI指针
6、如何判断⼀个类是⽆⽤的类?
- 类所有实例被回收
- ClassLoader被回收
- Class没有被引用
7、GC算法及特点
标记-清除算法

空间效率:标记和清除的效率都不高
空间占用率:标记清除之后会产生大量不连续内存碎片,分配较大内存对象的时候无法得到足够的连续内存而不得不提前触发一次GC
复制算法

空间利用率:内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低,但不用考虑内存碎片的问题
标记-压缩算法

比标记-清除多了一步压缩的操作
分代收集算法
“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据对象的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收
过程:伊甸园区==>幸存者区==>老年区

8、什么是双亲委派机制?
工作流程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类
图解

源码
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 判断是否被缓存
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果存在父类加载器,就委托给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果是BootStrap加载器,就调用本地方法,调用C++的方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父类加载失败,尝试自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);// 这个方法会被每个加载器类重写
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
好处
- 系统类防止内存中出现多份同样的字节码(当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类)
- 保证Java程序安全稳定运行,不会被自定义类注入
9、看你的项目用到了SPI,你知道SPI和双亲委派机制有关吧?为什么说SPI破坏了双亲委派机制?
关系:SPI破坏了双亲委派机制
原因:双亲委派机制的==可见性==决定了子加载器可以查看父加载器的所有类,父加载器却无法查看子加载器的所有类,而我们常用的如JDBC,它的接口java.sql.DriverManager
是由启动类加载器加载的,而它的实现类则是由第三方提供,由应用加载器加载。
==最终矛盾出现在,要在BootstrapClassLoader加载的类里,调用AppClassLoader去加载实现类==
核心是通过SPI实现,所以我们前面才说SPI破坏了双亲委派机制
源码分析(DriverManager==>getConnection()==>loadInitialDrivers())
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
那SPI是如何解决的呢?
- jdk提供了两种方式,Thread.currentThread().getContextClassLoader()和ClassLoader.getSystemClassLoader()一般都指向AppClassLoader,他们能加载classpath中的类
- SPI则用Thread.currentThread().getContextClassLoader()来加载实现类,实现在核心包里的基础类调用用户代码
逻辑:双亲委派机制的特点(金字塔型)==>造成的矛盾==>SPI解决==>(源码)==>如何解决
10、垃圾收集器
(这篇文章火钳刘明)
两篇文章带你搞懂GC垃圾回收之基础篇
- Serial收集器:单线程,新⽣代采⽤复制算法,⽼年代采⽤标记**-**整理算法;

- ParNew收集器:ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余一样;

- Parallel Scavenge + Parallel Old 收集器:关注点是吞吐量(所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值,JDK1.8默认收集器)。

- CMS回收器:CMS是多线程并发的,也就是说垃圾线程和业务进程是可以一起执行的
- 初始标记(CMS initial mark):标记 GC Roots 能直接关联到的对象
- 并发标记(CMS concurrent mark):进行 GC Roots Tracing
- 重新标记(CMS remark):修正并发标记期间的变动部分(这里要注意:重新标记就不能业务线程和垃圾线程一起执行了,也就是不能并发执行了)
- 并发清除(CMS concurrent sweep)

11、类加载过程
加载,链接,初始化
链接又分为验证,准备,解析
1.加载
2.链接
-
验证:确保被加载类的正确性
- 文件格式验证,验证字节流是否为Class文件格式的规范
- 元数据验证,对字节码描述的信息进行语义分析
- 字节码验证,确保程序语义合法符合逻辑
- 符号饮用验证,确保解析动作正确执行
-
准备:被类静态变量分配内存,并初始化为默认值,final特殊情况
public static final int value=3;
在初始化时会被赋值为3
-
解析:符号引用转化为直接引用(逻辑地址转化为内存地址)
3.初始化
这个阶段主要是对类变量初始化,是执行类构造器的过程。
换句话说,只对static修饰的变量或语句进行初始化。
初始化步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类(由此可见类加载属于懒加载和懒连接)
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
12、请你讲一下整个GC的过程
首先新对象进入伊甸区,等待伊甸区满,查看From区,From区满,伊甸区和From区移动存活对象到To区,To区期间如果满就移动到老年代,最后伊甸区和From区清理死亡对象,这样就结束了一次轻GC,而现在新生代中只有To区有对象,等伊甸区再次满,就将To区和伊甸区的对象移动到From区,同理,From区满就移到老年区,最后清理伊甸区和To区的死亡对象,再一次结束轻GC。
第一阶段复习
1.前言
复习了3天的面经,第一天数据库,第二天计网,第三天JVM,感觉都会了,但一刷面试题就直接暴毙(真就云会,云玩家),所以打算静下心来好好复习一下,复习的流程打算通过刷面试题巩固,所以接下来会放出很多在牛客上找的面试题,包括但不限于阿里,字节的。(同时为了不让自信心受挫,我只会拿出其中关于数据库,计网,JVM,Java基础的面试题,其他的没复习到,就不是很会哈哈哈。)
==如果这篇文章侵犯您的著作权,请联系我删除。==
1.Java与C++的区别
- Java程序运行在JVM虚拟机(往这个方向上带)上,一次编译,多地运行,具有良好的跨平台性;C++的跨平台性较差,需要考虑不同主机的硬件细节;
- Java没有指针,可以说更加的安全,同时Java对象的析构由虚拟机负责;C++的对象析构则有由程序员自己通过delete去完成
- Java属于半编译半解析语言,编译成字节码(.class),再有虚拟机去一句一句执行;C属于编译型语言,代码直接编译成二进制文件执行,所以在性能和速度方面C会更加优秀一点(所以你到底是来面C++还是Java的???)
- Java类单继承,C++可以多重继承。
2.Java内存,堆和栈的区别
这个问题我们可以从虚拟机角度给出答案
- 堆线程共享,里面存储着对象的实例,也是GC发生的场所
- 栈线程私有,里面存储着局部变量表,动态链接,方法出口等信息,我们调用对象方法的执行逻辑就是通过栈完成的
- Java内存中运行着JVM虚拟机,JVM是对Java内存模型的一种实现(这点可以忽略,不然带到Java内存模型就爆炸了)
3.GC,什么时候会full GC
- 老年代空间不足
- System.gc()
- young gc的悲观策略(新生代进入老年代,老年代的空间小于或可能小于新生代晋升的空间,就触发一次full gc)
扩展:啥时候出发young gc??
4.Java常用的容器有哪些
Map,List,Queue,Set
大概这四种,为什么没有提到Stack呢?是因为Stack是实现Vector的,Vector是实现List的
更加具体一点的话
ArrayList,Stack,LinkedList,PriorityQueue,HashSet,HashMap,TreeMap,CurrentHashMap(挑自己会的说)
5.ConcurrentHashMap
使用ConcurrentHashMap的原因是HashMap在高并发情况下会出现环形链表,HashTable虽然线程安全,但读写操作都会对整个集合加锁,性能不好。
ConcurrentHashMap底层数据结构:二级哈希表+链表+红黑树
读:
- key作hash,定位Segment
- 再hash,定位Segment的具体位置
写:
- key作hash,定位Segment
- 获得锁
- 再hash,定位到Segment具体位置
- 插入或覆盖HashEntry对象
- 释放锁
6.Spring AOP的原理
动态代理
扩展:JDK和CGlib代理的区别在于:JDK代理的对象必须实现接口,CGlib通过为代理对象派生子类的方式实现动态代理,所以不能代理final类和priavte方法。
(这时候会不会问我项目的内容了???我的RPC项目屏蔽客户端用的就是JDK动态代理)
7.MySql 写一个Sql语句 MySql的事务,四个隔离级别是什么,事务的实现原理,索引的数据结构,为什么使用B+树,联表查询的优化,慢查询的优化
写一个事务???
begin;
select * from user;
(假装出错)
rollback;
insert into user(id,name) value(1,“abc”);
commit;
四个隔离级别:读未提交,读已提交,可重复读,串行
事务实现原理:undo log,里面记录了事务操作的逆操作,通过当前版本和undo log逆推到特定版本,具体细节的话是数据库隐藏的字段db_rollback_id构建版本链,使新旧版本共同存在。
索引数据结构:哈希表和B+树,B+树使用较多
为什么使用B+树:B+树的树内节点只存储key,所以在IO吞吐量不变的情况下可以读取到更多的有效信息(相比于B树),同时B+树的叶子节点之间使用双向指针连接,对于范围查找更加高效。
联表查询的优化:使用join替代子查询,子查询不会走索引
慢查询的优化:(这个我不会,但我把我的思考过程将一下,两个方向Sql和表结构)
- 通过走索引减少数据库的IO访问次数
- 最左匹配原则
- 避免索引字段的计算和类型转化
- 不使用否定命令,如何!= <>这种
- 使用in代替or,or可能走不了索引
- 表结构
- 通过分库和分表减少数据量,减少了数据量,那Sql的执行时间自然就下来了
8.设计模式 列举常用的设计模式
==单例==,==工厂==,==策略==,迭代器,装饰者,==观察者==,建造者,==代理==,模版,命令(《我理解的常用》)
黄色字是项目中涉及到的
- 单例:单例工厂,SPI
- 策略:SPI
- 观察者:Netty的回调
- 代理:@RpcReference
Java基础
1.面向对象和面向过程的区别
面向对象易维护,易扩展,易复用
2.面向对象的三大特性
封装:把一个对象的属性私有化,同时提供一些可以被外界访问的属性和方法
继承:将已存在的类作为建立新的类的基础,子类可以复用父类的方法,同时定义一些私有的属性和方法
多态:子类可以用自己的方式实现父类的方法
3. 接⼝和抽象类的区别是什么?
- 方法修饰符
- 设计理念(接口是对行为的抽象,抽象类是对类的抽象)
- 抽象程度
4.==和equal
== : 它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据
类型==⽐较的是值,引⽤数据类型==⽐较的是内存地址)。
equals() : 它的作⽤也是判断两个对象是否相等。但它⼀般有两种使⽤情况:
情况 1:类没有覆盖 equals() ⽅法。则通过 equals() ⽐较该类的两个对象时,等价于通过
“==”⽐较这两个对象。
情况 2:类覆盖了 equals() ⽅法。⼀般,我们都覆盖 equals() ⽅法来⽐两个对象的内容是
否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为⼀个引⽤
String b = new String("ab"); // b为另⼀个引⽤,对象的内容⼀样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,⾮同⼀对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
5.为什么重写 equals 时必须重写 hashCode⽅法?
节省时间开销
如果只通过equals判断是否相同,计算量太大;
先通过hashCode判断,如果相同再通过equals判断会更加高效。
不直接通过判断hashCode得出结论的原因在于相同的hashCode,但对象可能不相同
6.为什么 Java 中只有值传递?
Java中只有值传递
- ⼀个⽅法不能修改⼀个基本数据类型的参数(即数值型或布尔型)。
- ⼀个⽅法可以改变⼀个对象参数的状态。
- ⼀个⽅法不能让对象参数引⽤⼀个新的对象。
7.排序算法
leetcode 912
快排
class Solution {
public int[] sortArray(int[] nums) {
quickSort(nums, 0, nums.length - 1);
return nums;
}
//核心代码,其他是三元选中法的辅助方法
private void quickSort(int nums[], int L, int R) {
if (L > R)
return;
int l = L, r = R;
//三元选中法
//choosePivot(nums, L, R);
int tmp = nums[L];
while (L < R) {
while (L < R && nums[R] >= tmp)
R--;
if (L < R)
nums[L] = nums[R];
while (L < R && nums[L] < tmp)
L++;
if (L < R)
nums[R] = nums[L];
}
nums[L] = tmp;
quickSort(nums, l, L - 1);
quickSort(nums, L + 1, r);
}
private void choosePivot(int[] nums, int L, int R) {
int mid = (L + R) >> 1;
if (nums[L] >= nums[mid] && nums[mid] >= nums[R] || nums[R] >= nums[mid] && nums[mid] >= nums[L])
swap(nums, L, mid);
if (nums[L] >= nums[R] && nums[R] >= nums[mid] || nums[mid] >= nums[R] && nums[R] >= nums[L])
swap(nums, L, R);
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
}
归并
class Solution {
public int[] sortArray(int[] nums) {
mergeSort(nums, 0, nums.length - 1);
return nums;
}
private void mergeSort(int[] nums, int l, int r) {
if (l >= r)
return;
int mid = (l + r) >> 1;
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
int[] res = new int[nums.length];
int st = mid + 1;
int tmp = l;
int pos = 0;
while (l <= mid && st <= r) {
if (nums[l] < nums[st]) {
res[pos++] = nums[l];
l++;
} else {
res[pos++] = nums[st];
st++;
}
}
while (l <= mid)
res[pos++] = nums[l++];
while (st <= r)
res[pos++] = nums[st++];
for (int i = 0; i < pos; i++)
nums[tmp++] = res[i];
}
}
设计模式
1.单例模式
整个应用运行过程中对象只会创建一次的,多次使用的设计模式,它解决了同一对象多次被创建的资源浪费问题。
懒汉模式(线程不安全)
public class Singleton {
private static Object obj;
public static Object getInstance(Class<?> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (obj == null) {
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object obj = constructor.newInstance();
}
return obj;
}
}
懒汉模式(线程安全)
public class Singleton {
private static volatile Object obj;//使用volatile防止指令重排序
public static Object getInstance(Class<?> clazz) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (obj == null) {
synchronized (Singleton.class) {
if (obj == null) {
Constructor<?> constructor = clazz.getDeclaredConstructor();
Object obj = constructor.newInstance();
}
}
}
return obj;
}
}
饿汉模式
public class Singleton {
private static final Singleton obj = new Singleton();
private Singleton() {
}
public static Object getInstance(Class<?> clazz) {
return obj;
}
}
枚举(不常用)
2.工厂模式
不管你用什么语言,创建什么资源。当你开始为“创建”本身写代码的时候,就是在使用“工厂模式”了。
为了创建资源,但有屏蔽了很多细节
工厂模式
public interface Factory {
Object creatObject();
}
class AFactory implements Factory {
@Override
public Object creatObject() {
Object o = new Object();
//此处省略较多初始化工作
return o;
}
}
class BFactory implements Factory{
@Override
public Object creatObject() {
Object o = new Object();
//此处省略较多初始化工作
return o;
}
}
为每一个子类生成一个工厂会很繁琐,所以可以采用抽象工厂模式
把厂品分组,组内不同厂品对应于同一工厂的不同方法的设计模式叫抽象工厂
3.观察者模式
Java的事件监听机制还有回调机制
4.策略模式
将每个策略都抽象成一个类,方便切换
5.代理模式
静态代理
public interface Home {
void getHome();
}
class Host implements Home {
@Override
public void getHome() {
System.out.println("租房");
}
}
class Agent implements Home {
private Host host;
public Agent(Host host) {
this.host = host;
}
@Override
public void getHome() {
System.out.println("aaaaa");
host.getHome();
System.out.println("bbbbb");
}
}
动态代理
6.适配器模式
7.装饰者模式
框架
1.Spring中IOC和AOP的原理
IOC的中文名叫控制反转,它通过反射将对象注入到Spring容器中,由Spring容器控制对象的生命周期,从而实现了实例对象被调用者和调用者的解耦,为此我们只需要使用Spring提供的注解或配置文件完善对象的属性即可,当我们调用实例时,Spring会根据依赖关系自动帮我们注入需要的bean
AOP的原理是动态代理,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行,AOP是对OOP的补充和完善
2.Netty是什么?
- Netty是一个基于NIO的网络框架,可以快速开发网络应用
- 简化TCP和UDP的Socket编程,并且性能和安全性比自己编写的更好
- Netty被广泛应用到其他框架中,我知道的就有Dubbo,RocketMQ
3.为什么要用Netty
- 自带编码器解决TCP拆包和粘包问题
- 简单而强大的线程模型(引申Netty的线程模型)
- ⽐直接使⽤ Java 核⼼ API 有更⾼的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制(Netty零拷贝)
- 同一的API,支持阻塞和非阻塞
4.Netty零拷贝
零复制(英语:Zero-copy;也译零拷⻉)技术是指计算机执⾏操作时,CPU 不需要先将数据从某处内存复制到另⼀个特定区域。这种技术通常⽤于通过⽹络传输⽂件时节省 CPU 周期和内存带宽。
Netty 中的零拷⻉体现在以下⼏个⽅⾯:
-
使⽤ Netty 提供的 CompositeByteBuf 类, 可以将多个 ByteBuf 合并为⼀个逻辑上的ByteBuf , 避免了各个 ByteBuf 之间的拷⻉。
-
ByteBuf ⽀持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同⼀个存储区域的ByteBuf , 避免了内存的拷⻉。
-
通过 FileRegion 包装的 FileChannel.tranferTo 实现⽂件传输, 可以直接将⽂件缓冲区的数据发送到⽬标 Channel , 避免了传统通过循环 write ⽅式导致的内存拷⻉问题。
项目
1.什么是RPC?
RPC(remote produce called),远程过程调用,可以使应用程序在客户端直接调用服务端方法,就像调用本地方法一样
2.RPC为什么不使用专用序列化
RPC 框架,它需要序列化的数据是,用户调用远程方法的参数,这些参数可能是各种数据类型,所以必须使用通用的序列化实现,确保各种类型的数据都能被正确的序列化和反序列化。(自己设计不方便且正确率不高)
3.使用什么序列化方法以及优点?
Java默认序列化,使用虽然方便,但速度慢,占空间,而且无法跨语言
java原生序列化的缺点
Kryo 速度快,序列化体积小,但跨语言较复杂
序列化框架性能对比(kryo、hessian、java、protostuff)
任务:动态代理+注册中心
4.单例工厂不能获取接口的构造函数(这个问题很白痴,但还是写出来了)
5.SPI
java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
6.说说你项目目前可以改进的点
- 项目本身对Spring有依赖,争取使用SPI完全替代Spring。在项目中SPI仅做寻找服务实现的功能,而Spring在消费端和服务端端注解实现中,除了服务寻找的功能外还有bean注册前后的行为管理。现在能想到的是如果在该项目中完全使用SPI,那么消费端的动态代理和网络细节将不再是透明的,消费端需要在获取到服务后,自己启动动态代理,增加了使用这个框架的负担。
任务:注解+SPI
7.你的自定义注解如何实现的
- @import
- 快速的导入一个组件到容器中
- 导入@Configuration注解的配置类
- 导入ImportSelector的实现类
- 导入ImportBeanDefinitionRegistrar的实现类
- 声明一个bean
- Spring会自动调用:implements了ResourceLoaderAware接口类的实现方法:setResourceLoader(),将ApplicationContext的ResourceLoader注入进去
- BeanPostProcessor:bean注册前后发行为
8.maven的依赖问题
父项目A,子项目B,C
B依赖C,但B无法引用C的方法——父项目maven使用clean+install方法即可
9.Spring扫描问题
//公用部分
CustomScanner rpcScanner = new CustomScanner(registry, RpcService.class);
CustomScanner springScanner = new CustomScanner(registry, Component.class);
if (resourceLoader != null) {
rpcScanner.setResourceLoader(resourceLoader);
springScanner.setResourceLoader(resourceLoader);
}
//私有部分
log.info("The number of Spring scans : [{}]", springScanner.scan(SPRING_COMPONENT_PACKAGE));//1 SpringBeanPostProcessor
log.info("The number of RPC scans : [{}]", rpcScanner.scan(rpcScannerBasePackage));//2 本来是3个 HelloWorldImpl NettyRpcServer SpringBeanPostProcessor 除去统计过的SpringBeanProcessor就是2个
//---------------------------------
log.info("The number of RPC scans : [{}]", rpcScanner.scan(rpcScannerBasePackage));//3 HelloWorldImpl NettyRpcServer SpringBeanPostProcessor
log.info("The number of Spring scans : [{}]", springScanner.scan(SPRING_COMPONENT_PACKAGE));//0
以上这两段的结果不一样????
带着这样的疑问,我使用了默认的构造函数
CustomScanner scanner = new CustomScanner(registry);
if (resourceLoader != null) {
scanner.setResourceLoader(resourceLoader);
}
log.info("scans : [{}]",scanner.scan(rpcScannerBasePackage));//2 NettyRpcServer SpringBeanPostProcessor
发现Spring的scan
默认统计的是@Component
,在这里是NettyRpcServer
SpringBeanPostProcessor
所以第一段代码的的第一句结果是1,而第二句扫描的是默认(包扩展又多了一个)+@RpcService
,所以也就扫描到了3个
但显示的却是2个这是为什么呢?看看源码
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
//统计指定包下的组件个数
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
//判断注册器中是否存在已经存在这个bean
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}
protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {
//不存在这个bean,就返回true,可以添加
if (!this.registry.containsBeanDefinition(beanName)) {
return true;
}
BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);
BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();
if (originatingDef != null) {
existingDef = originatingDef;
}
//如果存在bean,就返回false,不添加
if (isCompatible(beanDefinition, existingDef)) {
return false;
}
throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +
"' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +
"non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");
}
可以从源码中看出来对于同一个注册器,相同的bean只会被统计一次。
在探寻这个问题的同时,发现统计的bean类型有点出入,最后决定减少对Spring的依赖,把NettyRpcServer
的@Component
注解去掉,在Main里面直接new好了,所以整个框架对Spring的依赖也就是@RpcScan
的组件扫描和@RpcServer
@RpcRefrence
的AOP(Bean注册前后的行为)。
10.SPI的实现细节

任务:负载均衡+配置+细节.....
11.负载均衡如何实现的
基于hash一致性的ConsistentHashLoadBalance
通过建立虚拟节点,对IP地址做映射和位运算,形成多个虚拟节点对应的一个机械节点的关系
再通过服务名在虚拟节点中寻找第一个大于hash(服务名)的节点,如果找不到说明超过最大值,就返回第一个节点

12.如何去关闭资源
jvm有一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的addShutdownHook添加的钩子,jvm只有执行了这些钩子,jvm才会关闭。
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
InetSocketAddress address = new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(), NettyRpcServer.port);
CuratorUtil.clearRegister(CuratorUtil.getZkClient(), address);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}));
任务:文档+简历改写