以下是Mock回答
回答由三部分组成
-
直接回答:会用简要的语言叙述这个问题的答案
-
引申点(选择性给出):该点是预测面试官会感兴趣or会问的其他要点,可以当成进阶知识掌握
-
业务相关(选择性给出):该部分是在真实业务中遇到过的关于这个问题的处理和思考
核心思路是在基础扎实的回答之上尽可能的扩散出自己深度和广度来,从业务和类似技术来举一反三抢占话语权
面经
Java基础
0.HashMap的源码,实现原理,JDK8中对HashMap做了怎样的优化。
拉链结构,数组+链表,原理是hash找数组,冲突后拉链表,1.8优化为会进化成红黑树提高效率,并且使用2^n来做容量值
引申点:
equal & hashcode
其他地方的hash处理,如redis的hash、集群slot等
对hash算法类型的了解(安全哈希和非安全哈希如mermerhash)
对hashMap实现的了解:取hashcode,高位运算,低位取模
一致性hash(处理了什么问题,在什么场景用到)
红黑树简单描述
1.HaspMap扩容是怎样扩容的,为什么都是2的N次幂的大小。
在容量到达抵达负载因子*最大容量的时候进行扩容,负载因子的默认值为0.75
2N的原因:
-
hash的计算是通过hashcode高低位混合然后和容量的length进行与运算
-
在length=2n的时候,与运算相当于是一个取模操作
-
那么在每次rehash完毕之后mod2N的意义在于要么该元素是在原位置,要么是在最高位偏移多一位的位置,提高效率
引申点:
ConcurrentHashMap的扩容:1.7分段扩容以及1.8transfer并发协同的扩容
redis渐进式hash扩容处理
3.HashMap,HashTable,ConcurrentHashMap的区别。
Map线程不安全(没有用任何同步相关的原语),Table安全(直接加syn),Concurrent提供更高并发度的安全(分段锁思想orSyn+Cas)
引申点:
对线程安全的定义:如hashmap在1.7前会头插死循环,但是在1.8改善后还是不能叫线程安全,因为没有可见性
对锁粒度的思考:在介于map和table之间存在tradeoff之后的均衡解
Syn和ReentranceLock的区别
锁升级
4.极高并发下HashTable和ConcurrentHashMap哪个性能更好,为什么,如何实现的。
分两种情况讨论:
-
极高并发读:并发读的情况下,Table也为读加了锁,没有并发可言,ConcurrentMap读锁并没有加并发,直接可读,若读resize的某个tab为空则转到新tab去读,Node的元素val和指针next都是volatile修饰的,可以保证可见性,所以concurrentMap获胜
-
极高并发写:在并发写的情况下,table也是直接加了Syn做锁,强制串行,并且resize也只能单线程扩容,ConcurrentMap首先对于每个数组都有并发度,其次在resize的时候支持多线程协同,所以concurrentMap获胜
所以整体而言concurrentMap优势在于:
-
读操作基于volatile可见性所以无锁
-
写操作优势在于一是粗粒度的数组锁,二是协同resize
这个问题的思路是先分类讨论然后描述细节最后在下结论
引申点:
volatile的实现:保证内存可见、禁止指令重排序但无法保证原子性
java内存模型
JVM做的并行优化、先行发生原则与指令重排序
底层细节的熟悉
5.HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么。
1.7前死锁,1.7后线程会获取脏值导致逻辑不可靠
6.java中四种修饰符的限制范围。
public:公用,谁来了都给你用
protected:包内使用,子类也可使用
default:包内使用,子类不可使用
private:自己用
7.Object类中的方法。
wait\hashcode\equal\wait\notify\getclass\tostring\nofityall\finalize
引申点:
wait和sleep区别
hashcode存在哪儿(对象头里)
finalize作用:GC前执行,但是不一定能把这个函数跑完
getClass后能获取什么信息:引申到反射
8.接口和抽象类的区别,注意JDK8的接口可以有实现。
接口:可以imp多个接口,1.7之前不允许实现,1.8后可以实现方法
抽象类:只能继承一个类,抽象类中可以存在默认实现方法
接口的语义是继承该接口的类有该类接口的行为
抽象类的语义是继承该抽象类的类本身就是该抽象类
9.动态代理的两种方式,以及区别。
-
CGLIB:其本质是在内存中继承了一个子类,可以代理希望代理的那个类的所有方法
-
JDK动态代理:实现InvocationHandler,通过生成一个Proxy来反射调用所有的接口方法
优劣:
-
CGLIB:会在内存中多存额外的class信息,对metaspace区的使用有影响,但是性能好,可以访问非接口的方法
-
JDK动态代理:本质是生成一个继承所有接口的Proxy来反射调用方法,局限性在于其只能代理接口的方法
引申点:
Spring的AOP实现以及应用场景
反射的开销:检查方法权限,序列化以及匹配入参
ASM
10.Java序列化的方式。
继承Serializable接口并添加SerializableId(idea有组件可以直接生成),ID实际上是一个版本,标志着序列化的结构是否相同
11.传值和传引用的区别,Java是怎么样的,有没有传值引用。
本质上来讲Java传递的是引用的副本,实际上就是值传递,但是这个值是引用的副本,比如方法A中传入了一个引用ref,那么在其中将ref指向其他对象并不影响在方法A外的ref,因为ref在传入方法A的时候实际上是指向同一个对象的另一个引用,可以称之为ref',ref'若直接修改引用的对象会影响ref,但若ref'指向其他对象则和ref没有关系了
12.一个ArrayList在循环过程中删除,会不会出问题,为什么。
分情况讨论:
-
fori删除,不会直接抛异常,但是会产生异常访问
-
foreach删除(实际就是迭代器),会直接抛出并发修改异常,因为迭代器会进行获取迭代器时的exceptModCount和真实的modCount的对比
引申点:
迭代器实现
ArrayList内部细节
13.@transactional注解在什么情况下会失效,为什么。
方法A存在该注解,同时被方法B调用,外界调用的是Class.B的方法,因为内部实际上的this.a的调用方式没走代理类所以不会被切面切到
数据结构和算法
1.B+树
出度为m的一颗树,节点的子女在[M/2,M]之间
叶子节点存储全量信息
非叶子节点只充当索引进行叶子节点的路由(内存友好、局部性友好)
底层的叶子节点以链表的形式进行相连(范围查找友好)
2.快速排序,堆排序,插入排序(其实八大排序算法都应该了解
快排:核心是分治logn
堆排:基于二叉树nlogn
插入:暴力n2
3.一致性Hash算法,一致性Hash算法的应用
一致性hash,将整个hash的输出空间当成一个环,环中设立多个节点,每个节点有值,当对象的映射满足上个节点和这个节点中间值的时候它就落到这个节点当中来
应用:redis缓存,好处是平滑的数据迁移和快速的rebalance
引申点:
一致性hash热点怎么处理:虚拟节点
redis如何实现的:客户端寻址
程序计数器:计算读到第几行了,类似一个游标
方法栈:提供JVM方法执行的栈空间
本地方法栈:提供native方法执行的栈空间
堆:存对象用的,young分eden,s0,s1,分配比例大概是8:1:1,Old只有一个区
引申点:
heap什么时候发生溢出- stack什么时候发生溢出
- 方法区什么时候发生溢出
- hotspot code的机制
- 流量黑洞如何产生的
方法栈是JVM方法使用的,本地方法栈是native方法使用的,在hotspot其实是用一个
3.JVM的栈中引用如何和堆中的对象产生关联。
引用保存地址,直接可以查找到堆上对应地址的对象
4.可以了解一下逃逸分析技术。
方法中开出来的local变量如果在方法体外不存在的话则称之为无法逃逸
-
可以直接分配在栈上,随着栈弹出直接销毁,省GC开销
-
消除所有同步代码,因为本质上就是个单线程执行
引申点:
JVM编译优化:
逃逸分析
栈上分配
分层编译与预热
栈上替换
常量传播
方法内联
...
5.GC的常见算法,CMS以及G1的垃圾回收过程,CMS的各个阶段哪两个是Stop the world的,CMS会不会产生碎片,G1的优势。
常见算法:
-
标记清楚:存在内存碎片,降低内存使用效率
-
标记整理:整理可分为复制整理和原地整理,不存在内存碎片,但是需要额外的cpu算力来进行整理,若为复制算法还需要额外的内存空间
CMS流程:
-
初始标记(stw):获得老年代中跟GCRoot以及新生代关联的对象,将其标记为root
-
并发标记:将root标记的对象所关联的对象进行标记
-
重标记:在并发标记阶段,并没有stw,所以会有一些脏对象产生,即标记完毕之后又产生关联对象修改
-
最终标记(stw):最终确定所有没有脏对象的存活对象
-
并发清理:并发的清理所有死亡对象
-
Reset:重设程序为下一次FGC做准备
CMS优劣:
-
优点:
-
不像PN以及Serial一样全程需要stw,只需要在两个标记阶段stw即可
-
并发标记、清楚来提升效率,减少stw的时间和整体gc时间
-
在最终标记前通过预设次数的重标记来清理脏页减少stw时间
-
-
缺点:
-
仍然存在stw
-
基于标记清楚算法的GC,节省算力但是会产生内存碎片
-
并发标记和清楚会造成cpu的高负担
-
G1流程:
这个我只懂个大概,如下
分块分代回收,可分为youngGC和MixedGC,特点是可预测的GC时间(即所谓的软实时特性)
引申点:
是否进行过线上分析
GC日志是否读过,里面有什么信息
你们应用的YGC和FGC频率以及时间是多少
你清楚当前应用YGC最多的一般是什么吗
业务相关:
在线上大部分curd业务当中,实际上造成ygc影响较严重且可优化的是日志系统
对dump出来的堆进行分析的话里面有很大一块是String,而其中大概率会是日志中的各种入参出参
优化方案有很多:
将不需要打日志的地方去除全量日志打印功能
日志在不同环境分级打印
只打出错误状态的日志
在大促期间关闭非主要日志打印
同步改异步等
6.标记清除和标记整理算法的理解以及优缺点。
上文已答
7.eden survivor区的比例,为什么是这个比例,eden survivor的工作过程。
8:2
定性的来讲:大部分对象都只有极短的存活时间,基本就是函数run到尾就释放了,所以给新晋对象的buffer需要占较多的比例,而s区可以相对小一点来容纳长时间存活的对象,较小的另一个原因是在几次年龄增长后对象会进入老年代
定量的来讲:实验所得,也可以根据自己服务器的情况动态调整(不过笔者没调过)
8.JVM如何判断一个对象是否该被GC,可以视为root的都有哪几种类型。
没有被GCRoot所关联
Root对象:(tips:不用硬记,针对着JVM内存区域来理解即可)
-
函数栈上的引用:包括虚拟机栈和native栈
-
static类的引用:存在方法区内
-
常量池中的常量:堆中
引申点:
gc roots和ref count的区别
9.强软弱虚引用的区别以及GC对他们执行怎样的操作。
强:代码中正常的引用,存在即不会被回收
软:在内存不足的时候会对其进行GC,可用于缓存场景(类似redis淘汰)
弱:当一个对象只有弱引用关联的时候会被下一次GC给回收
虚:又称幽灵引用,基本没啥用,在GC的时候会感知到
引申点:
每个引用的使用场景
是否在源码或者项目中看到过or使用过这几种引用类型(ThreadLocal里用了WeakReference)
10.Java是否可以GC直接内存。
在GC过程中如果发现堆外内存的Ref
11.Java类加载的过程。
加载:从某个地方读取class数据
链接:
-
验证:检验class是否有效
-
准备:开辟class信息存放的空间以及常量初始化
-
解析:符号引用转直接引用
初始化:在真正init的时候为其在堆上分配内存
12.双亲委派模型的过程以及优势。
System -> Ext -> Bootstrap类加载器依次从子类到父类进行双亲委派
本质是默认的类加载器都会直接调用super来尝试进行加载
优势:在不手动介入类加载过程的情况下可以保证基本的类都由统一的类加载器进行load,在内存中统一
13.常用的JVM调优参数。
虚拟机相关:影响整个虚拟机
XMX:最大堆空间
XMS:最小堆空间
堆分区相关:动态调整适配机器
XX:NewRatio 新生代的比例
XX:SurvivorRatio S1S0和edun区的比例
GC机制相关:动态调整适配是否需要低延迟还是高吞吐
XX:CMSInitiatingOccupancyFraction=70
14.dump文件的分析。
jmap dump出来看看哪些文件比较大or比较多,然后找认为可能有问题的看看具体是哪个class,基本就能定位到代码进行排查了,需要辅以日志/全链路监控工具/arthas等工具进行使用
15.Java有没有主动触发GC的方式(没有)。
System.gc可以hint虚拟机进行GC,但是并不一定会执行
多线程
1.Java实现多线程有哪几种方式。
-
重写Thread
-
重写Runnable,赛到Thread里面
-
普通重写
-
匿名类重写
-
lambda重写
-
-
Callable+FutureTask
2.Callable和Future的了解。
Callable vs. Runnable
Callable可以返回结果,Runnable无法返回结果
Future就是Callable返回的结果,底层是用一个volatile的变量标志是否已经结束来让调用者知道任务执行状况
线程池+SynList+Future可以获取一组任务的执行情况
3.线程池的参数有哪些,在线程池创建一个线程的过程。
核心线程数:决定正常情况下最多有几个线程在执行任务
最大线程数:在队列将要溢出的时候最多能多开几个线程
缓冲队列:作为一层buffer提供多任务的缓冲
线程生成工厂:提供名字设置、打日志等功能
拒绝策略:1. 抛出异常丢弃任务 2. 不抛出异常直接拒绝任务 3. 丢弃前面的任务执行新任务 4. 由发起线程执行该任务
存活时间:无任务的线程存活时间
创建过程:判断核心线程数是否达标,未达标直接创建线程执行,达上限后塞入队列中,队列满了创建新线程直到最大线程数,然后执行拒绝策略
4.volitile关键字的作用,原理。
保证该变量在所有线程中的可见性
原理:1. 保证内存可见性 2. 禁止指令重排序(内存屏障,读前读屏障,写后写屏障)
5.synchronized关键字的用法,优缺点。
锁对象:直接锁住对象
锁方法:锁住方法对应的调用对象,若是静态方法则锁class
优点:使用简单,可重入
缺点:缺乏高级特性(condition\cutdownLatch等),1.6前性能不好
6.Lock接口有哪些实现类,使用场景是什么。
ReenterLock:可重入锁,类似syn,AQS实现,通过双端队列+volatile的count值,实现并发,线程抢占分exclusive和share
ReadLock:读锁,支持并发读
WriteLock:写锁,与所有读锁写锁互斥
CopyOnWrite:写的时候Copy出一份对象进行插入,然后原对象供读,在新对象准备完毕之后将引用指过去(这一步需要同步)
Segment:分段锁,1.8前concurrentHashMap实现
7.可重入锁的用处及实现原理,写时复制的过程,读写锁,分段锁(ConcurrentHashMap中的segment)
见上文
8.悲观锁,乐观锁,优缺点,CAS有什么缺陷,该如何解决。
悲观锁:认为锁是强竞争的,修改数据前先将数据锁定,禁止其他用户/线程对其修改
乐观锁:认为锁是弱竞争的,不对数据进行锁定,在写入数据的时候判断是否符合标准,符合标准则写入,不符合则返回错误信息,常见的乐观锁有CAS
CAS缺点:ABA问题,数据从A变成B再变成A,其中经历了两次逻辑处理,但是如果某个线程在变换完毕过后读取这个值认为它依然是初始值A来进行逻辑操作就可能出现问题
ABA问题解法:加版本号
引申点
MySQL锁类型
MVCC流程
9.ABC三个线程如何保证顺序执行。
解法1:
CountDownLatch, 主线程设置一个latch, 值为1, 启动A线程, 执行完毕再继续往下, B线程类似
解法2:
Condition, 设置两个condition, A执行完释放B的condition, B执行完释放C的condition
解法3:
FutureTask, A线程提交一个FutureTask, 然后在主线程阻塞等待返回结果再进行B线程
解法4:
volatile, A执行完设置volatile为1, Bwhile读取volatile为1时进行逻辑操作, 执行完设为2, Cwhile读取2
10.线程的状态都有哪些。
New: 刚创建还没开始运行
Runnable: 交由操作系统执行, 但是不一定在吃cpu时间片
Wait: 等待被notify
TimeWait: 具有超时的wait
Blocked: 对于某个资源产生争用陷入阻塞
引申点:
和操作系统线程状态的区别: 例如在OS里等待IO的线程在Java线程体系中的状态是什么
11.sleep和wait的区别。
sleep阻塞线程到点了就自己醒了
wait阻塞线程并释放当前对象的锁, 需要notify
引申点:
锁升级
wait等待的是什么
只有一个线程的时候它能wait吗?
当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,
(这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)
12.notify和notifyall的区别。
Notify随机挑一个, 剩下的还在wait状态
NotifyAll唤醒全部一起争用, 大部分会处于blocked状态
13.ThreadLocal的了解,实现原理。
一个map, 里面维护了当前线程作为k, 自定义数据解构作为v的键值对, 可以获取当前线程的上下文
为了防止内存泄漏里面的entry用的是弱引用, 当外界没有任何引用指到threadLocal里面变量的时候会被清楚, 即线程消亡的时候其threadlocal里的值会在下一次被GC
数据库相关
1.常见的数据库优化手段
-
log同步刷盘改异步刷盘
-
集群的话强双写改异步同步
-
针对sql优化(explain慢sql)
-
添加索引
2.索引的优缺点,什么字段上建立索引
优点:查的快,支持range
缺点:大部分查询实际需要回表,索引建立会额外消耗内存和磁盘,对开发者的sql也有要求
字段:区分度大的字段
3.数据库连接池。
mybatis有自带的, 市面常用的一般是durid
4.durid的常用配置。
连接池数量,idletime,keepLive原则,是否autocommit,建立链接前是否握手等(数据库的基本配置都大差不差,其实我也没看过duird)
计算机网络
1.TCP,UDP区别。
TCP:面向链接\可靠交付\拥塞控制\线程到线程
UDP:面向报文\尽力交付\n:m传播
2.三次握手,四次挥手,为什么要四次挥手。
三次握手:
-
client send : SYN = 1, seq = x
-
server feedback : ACK = 1, SYN = 1, seq = y, ack = x+1
-
client feedback : ACK = 1, seq = x+1, ack = y+1
四次挥手:
-
client send : FIN = 1, seq = x, ack = z
-
server feedback : ACK = 1, ack = x+1
-
server sned : FIN = 1, seq = y, ack =q
-
client feedback : ACK = 1, ack = q+1
为什么四次挥手 : 全双工通道关闭需要双方通信
3.长连接和短连接。
HTTP1.0属于经典的短链接, 每次通信需要重新开tcp端口
Neety可以制造长链接, websocket也可以, 通过心跳保持链接稳定然后进行传输, 接受端口和握手开销
4.连接池适合长连接还是短连接。
取决于连接池如何使用
如果是数据库连接池的话可以考虑使用长连接, 因为链接目标是一定的, 可以减小重复链接的开销
但是如果是RPC调用的话长短链接都可以
短链接的优势在于在少量请求随机请求到海量服务时不需要维持额外的开销去保持链接
长链接的优势在于如果调用服务比较固定, 那么长连接可以减少握手开销, 自动探活
设计模式
1.观察者模式
举例子wait/notify, 在观察到变化的时候就进行改变
2.代理模式
举例子JDK动态代理,通过一层proxy对真实对象进行代理,进行一些额外操作(e.g.:增强行为、负载均衡等)
3.单例模式,有五种写法,可以参考文章单例模式的五种实现方式
-
普通单例
-
lazyloading+syn单例
-
lazyloading+doublecheck单例
-
枚举
-
最后一种不知道,查了发现是静态内部类单例,利用静态内部类第一次访问才加载的机制实现lazyloading
4.可以考Spring中使用了哪些设计模式
工厂/单例/适配器/代理等
分布式相关
1.分布式事务的控制。
XA -> 2PC -> 3PC
XA:引入单点协调器
2PC:二阶段提交, prepare+commit, 但是问题在于commit阶段不知道能不能成功, 所以一旦超时就只能默认失败
3PC:三阶段提交, 和2PC区别就是commit的拆分为两个阶段, 先让所有执行者执行但是不commit, 然后统一commit, 可以提高成功率, 因为语句已经执行完毕了只差commit
2.分布式锁如何设计。
考虑设计要素
-
过期时间设置
-
是否需要续约
-
key是什么(前缀+业务key+线程uuid)
-
如何让其可重入(鉴权+续约)
-
如何防止ABA问题(线程A锁了之后, 超时释放, B又锁了一个, A错误释放)
-
如何原子释放(lua脚本走cas)
3.分布式session如何设计。
考虑设计要素:
-
过期时间设置
-
单点登录实现
-
续约设置
-
放脱裤存信息设置
4.dubbo的组件有哪些,各有什么作用。
duboo不熟悉
5.zookeeper的负载均衡算法有哪些。
zookeeper就会个zab,不过负载均衡无非是公平轮询、加权轮询、随机轮询或者维护某些资源信息的动态路由这几种
6.dubbo是如何利用接口就可以通信的。
不太熟,估计涉及到服务注册以及序列化反序列化相关内容
缓存相关
1.redis和memcached的区别。
memcached不熟, 不瞎吹牛B了
2.redis支持哪些数据结构。
String: SDS支持, 支持常数时间获取长度, 防缓冲区溢出
Set: 无序集合
Zset: 带score的无序集合, 跳表支持
List: 字符串列表,按照插入顺序排序, 双向链表支持
Hash: 是一个map, 可以存储解构性数据, rehash支持类似copyonwrite的感觉, 渐进式hash
3.redis是单线程的么,所有的工作都是单线程么。
严格意义上来讲redis的网络IO是单线程的, 但是并不是所有的工作都是单线程的
IO事件: 多路复用程序监听多个socket, 然后交给事件分发器有序的交到各个handler中进行处理
时间事件: 处理过期键\处理持久化\定时任务等
4.redis如何存储一个String的。
-
存储一个len
-
超长会扩容, 扩容会留一定的buffer
-
支持所有二进制存储, 不以/0为判断标准
5.redis的部署方式,主从,集群。
主从: master/slave, slave同步所有写事件
Sentinel: 主从模式下主挂了可以通过sentinel进行选主(CP, 会存在一段时间不可用)
集群: 16384slot, 每个节点需要分配一段的slot进行处理, 当所有slot都有节点在处理的时候才可以上线
6.redis的哨兵模式,一个key值如何在redis集群中找到存储在哪里。
sentinel模式下直接找master就行了
7.redis持久化策略。
AOF: 类似binlog, 对执行的写事件都写入log中, 复原的时候直接读取并执行就行了
RDB: 快照式持久化, 保存当前库内的全量key
框架相关
1.SpringMVC的Controller是如何将参数和前端传来的数据一一对应的。
mapping
2.Mybatis如何找到指定的Mapper的,如何完成查询的。
mybatis会读取xml文件, 并获取xml和interface的映射, 将需要执行的sql绑定在interface上, 并构造代理注入spring, 在调用时通过反射获取当前调用的interface以及method, 然后在注册好的映射map中获取具体执行的sql并执行
3.Quartz是如何完成定时任务的。
没研究过
4.自定义注解的实现。
反射+获取runtime期间的注解
5.Spring使用了哪些设计模式。 上文提到了
6.Spring的IOC有什么优势。
控制反转, 不需要在代码中手动的去控制对象的生灭与周期, 将生命周期交由Spring进行处理
7.Spring如何维护它拥有的bean。
一些较新的东西
1.JDK8的新特性,流的概念及优势,为什么有这种优势。
-
Interface可用default方法
-
Stream
-
Optional
流的概念: 一组不间断的数据流
优势: 处理数据的时候可以将一个集合当作集合来看待, 而不是一组对象的拼接, 对集合的操作方便很多
2.区块链了解
不太了解
3.如何设计双11交易总额面板,要做到高并发高可用。
binlog同步+join+导入OLAP/Search型存储中, 可以采用时间分片来降低计算成本
全部评论
(7) 回帖