这是2023年年初换工作面试时总结的,汇总了面试时的高频题。每一题都是在搞懂的情况下,用自己的语言回答的。目前内容还不完善,一是技术面不够广,二是研究的还不深入。后面会不断丰富。写这个既是对自己学习的总结,也希望对正准备面试的小伙伴能有所帮助。
Mysql
1.mysql索引有哪些类型?
- 按数据结构来分,有B+树,hash,全文索引。哈希索引不支持排序,不支持范围查询,只支持等值查询。复杂度O(1);
按物理存储来分,有聚簇索引和非聚簇索引。
- 前者叶子节点存储的是数据页,后者叶子节点存储的是索引和主键。
- 主键索引是聚簇索引,其它字段建的索引都是非聚簇索引。
- 非聚簇索引在查询时,可能需要回表。
- 聚簇索引在增删改的时候需要更新索引树。
按字段特性来分,有主键索引、唯一索引、普通索引、前缀索引。前缀索引就是当字段比较长的时候,建索引会很大,不建索引又很慢。可以只对字符串前面一部分建立索引,达到时间和空间的均衡。
- 按字段数量来分,有单列索引、联合索引。
2.Innodb支持hash索引吗?
Innodb不能手动创建哈希索引,Innodb支持自适应哈希索引。
当Innodb判断建立哈希索引可以提升查询效率时(由于哈希索引查询复杂的是O(1),B+树索引查询复杂度是O(logn),当树的高度比较大时,B+树查询效率较低),就会建立哈希索引。
由于哈希索引底层是哈希表(数组+链表)结构,适合存在内存中,因此哈希索引不会生成磁盘文件。
3.联合索引的作用?
- 节约空间。索引本身会存在空间开销,联合索引有助于降低开销。
- 索引覆盖。查询多个条件时,如果查询的字段都在联合索引内,就可以避免回表。
4.什么是索引下推?
t表有一个联合索引(a,b),对于
1 | select * from t where a=m, b=n; |
v5.6之前,会先根据a=m找到所有符合条件的id,然后回表,查到所有记录后再去过滤b=n;
v5.6之后,会直接根据a=m, b=n找到对应的id,再去回表。
这就是索引下推,相当于在非聚簇索引中完成全部查询,减少了回表次数。
5.使用索引一定能提升效率吗?
不一定。因为索引本身要占空间,索引有创建成本,然后还有每次增删改时的维护成本。
比如下面两种情况:
- 查询中使用少的字段不要加索引;
- 数据种类少的字段也不要加索引,如性别。
6.InnoDB索引和MyIsam索引有什么区别?
- innodb使用聚簇索引和非聚簇索引,其中聚簇索引叶子节点存储的是主键id;
- myIsam只有非聚簇索引,但叶子节点存储的是数据行地址。
7.为什么说MyIsam索引查询速度更快?
- 因为MyIsam的非聚簇索引叶子节点直接存的是数据行地址,不需要回表,因此查询更快;
- MyIsam不支持事务,InnoDB支持事务,即使没有用到,但也少不了检测的步骤,这就影响了效率。
8.什么是MVCC?
MVCC(Multi Version Concurrency Control),即多版本并发控制,主要是为了解决数据库读写时的并发问题。
- 读读,没有并发问题;
- 写写,可以加锁;
- 读写,要用到MVCC,为每条记录维护一个版本链,使读写对应不同的版本。
原理:
每张表都隐藏了trx_id
和roll_pointer
字段,即事务id和回滚指针。每次修改,都会逆向生成一条undo日志,表示一个版本。回滚指针总是指向上一个版本,形成版本链。
MVCC支持在RC/RR隔离级别中生效。对于RC,事务中每个快照读都会创建ReadView,因此读取的是已提交的最新的记录,或者是当前事务修改但未提交的记录。对于RR,只在第一次快照读时创建ReadView,在之后都不再创建ReadView,因此读取的是在当前事务之前提交的记录,或是自己修改但未提交的记录。对于RU,只有快照读,因此存在脏读的问题。
9.什么情况会索引失效?
隐式类型转换。对字段加减操作、类型转换、使用函数都会造成索引失效。理解:因为以上操作都会造成字段的索引树结构变化,导致走索引代价太大。比如原先’1’在’a’之前,类型转换后’1’→1,’a’→0,导致’1’要移动到’a’之后,造成索引结构变化。
不符合最左前缀原则。联合索引要符合最左前缀原则,即左边第一个字段必须出现,无关顺序。对于(a,b,c)联合索引,(a,b,c),(a,b),(a,c),(a)四种情况可以走索引。
引擎判断全表扫描效率更高。如
select * from t order by a,b,c;
尽管走abc联合索引避免了重新排序,但由于查询的是*,会导致回表多次。而全表扫描虽然需要重新在内存中排序,但不需要回表,相比之下全表扫描效率更高。模糊查询,%在左的情况。
10.Innodb中的三层B+树可以储存多少数据?
B+树中每个节点都是数据页,一页的大小默认是16KB。其中叶子节点存储的是数据行,以一行数据1KB计算,一页可以存储16KB/1KB=16条记录。一层和二层节点存的是索引和指向子节点的指针。int类型的索引是4字节,指针是6字节,因此一个节点是10字节,一页可以存储16KB/10B=1638个索引,两层节点可以存储1638x1638=268w个索引,三层节点可以存储268wx16=4300w条记录。超过这个数B+树层高就会增加,导致IO次数增加,表现为查询变慢。
11.如何提高insert的效率?
- 一次插入多条,可以减少日志量(如binlog)、减少sql解析次数。如
insert into t values(a,b,c) (d,e,f)...
。 - 调大
bulk_insert_buffer_size
,调大缓存,仅作用于MyISAM。 - 开启手动提交事务。
12.char与varchar的区别?
char是不可变字符串,长度不足会在末尾填充空格,查的时候会将空格去除。可以通过修改sql_mode参数,保留原始空格。char适合存储定长字符串,如身份证号、ip地址等。
varchar是可变字符串,会按实际的字符长度来存储。缺点是更改时可能会导致页分裂。适合存储长度不一致且修改较少的字符串,如邮箱,住址。
13.B树和B+树的区别
B树的节点上都存了一份数据,子节点间没有双向链表;
B+树只在叶子节点存数据,子节点间有双向链表。
在数据页大小相同时,B树的层高更高。
B+树范围查询可以沿着叶子节点遍历,不用每次都从根节点查找。
Java
1.HashMap的内部实现
建议看一遍源码,面试必问。
包括以下知识点:
构造方法(数组初始长度、扩容阈值);
put方法(扰动、链表转红黑树);
扩容方法(扩容条件、树分裂);
与JDK1.7的区别(红黑树、头插尾插);
红黑树的定义,插入、删除(难点,加分项)。
2.哈希冲突的解决方式
哈希冲突是指哈希表中多个key落到了同一个槽位上,表示哈希值对数组长度的模相等,不代表哈希值相等。有以下解决方法:
- 开放寻址法,包括线性寻址(如ThreadLocalMap)、二次寻址;
- 再哈希法,如果有冲突,就对哈希值再哈希,直到找到空位;
- 链地址法(如HashMap)。
3.ConcurrentHashMap如何实现并发?
JDK1.7:数组分为若干个Segment,Segment实现了ReentrantLock,因此每个Segment都是一把锁。相比HashTable,锁的粒度变小;
JDK1.8:每个数组节点一把锁,synchronized + CAS实现。其中synchronized锁住链表,数组节点通过CAS更新。锁的粒度进一步细化。
4.ConcurrentHashMap如何统计节点个数?
维护了一个baseCount和CounterCell数组,都是volatile修饰。每次修改时,优先对baseCount累加。如果对baseCount累加失败,则会对CountCell累加。最后统计baseCount和CountCell各项之和。
5.创建线程的三种方式与对比?
三种方式
- 集成Thread,重写run方法。
- 实现Runnable接口,重写run方法,并将runnable传入thread中。
实现Callable接口,重写call方法,并将callable传入futureTask中,将futureTask传入thread中。
对比
Runnable相比Thread,适合多个线程资源共享,避免Java中单继承的限制,线程池只能接受Runnable类型。
- Callable本质上也是Runnable,重写的是call方法,有返回值,可以抛出异常,可以通过Future异步拿到执行结果。
6.线程的五种状态?
NEW:线程被创建,但还没调用start方法
RUNNABLE:调用start方法后,包含运行和就绪两种状态
BLOCKED:阻塞状态,线程等待获取锁
WAITING:无限期等待,执行wait,join方法后进入,执行notify方法后退出
TIME_WAITING:有限期等待,执行带超时时间的wait(),join()和sleep()进入,执行notify方法后退出
TERMINATED:线程终止
7.sleep和wait的区别
- sleep是Thread的静态方法,wait是Object的方法,任何对象都能调用
- sleep可以在任何地方调用,wait只能在同步代码块中调用(要求首先占有锁)
- sleep会交出CPU时间片,但不会释放锁,wait会释放锁,直到notify唤醒
8.四种线程池的创建方式?
Executors提供了四种创建线程池的静态方法:
newFixedThreadPool
,固定线程数线程池,多余的任务丢到队列里,可能导致队列很大产生OOM异常。
newCachedThreadPool
,线程数无上限的线程池,60s自动回收,可能导致线程数很多产生OOM异常。
newScheduledThreadPool
,延迟或周期执行的线程池。队列使用DelayedWorkQueue
,本质是基于堆。堆是基于数组的二叉树,根据根节点是否小于子节点,可分为最小堆跟最大堆。
newSingleThreadPool
,只有一个线程的线程池。
9.说一下线程池的几个参数?
建议看源码。
以ThreadPoolExecutor.execute()执行过程为例:
- 提交一个任务,先判断当前线程数是否达到核心数,没有则通过线程工厂创建核心线程;
- 如果达到了,则将任务丢到阻塞队列里;
- 如果队列满了,则判断当前线程数是否达到最大数,没有则创建非核心线程;
- 如果达到了,则执行拒绝策略(默认有四种:静默处理、抛出异常、交给调用线程、丢弃老任务);
- 非核心线程超时未获取到任务,会被销毁。
10.说一下工作中线程池的使用场景,如何设置参数?
举个例子:有一个任务,需要批量获取输入源信息。
核心数:由于扫描涉及到CPU解码,是CPU密集型的。机器是8核,因此核心数设为7,留1个给主线程。
最大数:最大数也是7,因为再增加非核心线程,也不会提高效率。
队列:队列数可以根据日志中队列的积压情况来判断,假如一天内最多积压100,则可以设置一个容量200的有界队列。
拒绝策略:由于一个流会被扫描多次,后面扫描的会比之前扫描的结果更新。因此拒绝策略可以使用DiscardOldestPolicy
,即丢弃老任务。
11.常用的并发工具类
CountDownLatch:门闩。可以给多个线程设置一个门闩,当门闩开启时,多个线程同时执行。或者给主线程设置多个门闩,子线程每完成一个,则开启一个门闩,直到全部完成,主线程得以执行。new CountDownLatch(n)指定门闩个数,await()等待,countDown()门闩减1。适用于控制多个任务同时执行。
CyclicBarrier:栅栏。new CyclicBarrier(parties)指定需要等待的线程数。每个线程执行到await()时都会等待,且parties计数减一,直到parties=0则会唤醒所有线程。适用于协调多个任务同时执行。
Semaphore:信号量。用来控制访问某个资源的线程数。new Semaphore(permits, fair)会创建一个有permits个许可证的信号量,每次执行acquire方法会拿走一张许可证,执行resease方法会还回一张许可证。fair默认是false,表示非公平锁,效率较高。true表示公平锁,阻塞时间长的优先拿到。
Exchanger:交换器。用于多个线程间交换信息。第一个线程在执行到exchange()时会阻塞,直到第二个线程到来时会进行交换。
12.什么是CAS,有什么问题?
CAS是CompareAndSwap,即比较和替换。是Unfafe类的一个本地方法,比较内存地址的值与预期值是否相同,相同则允许修改,否则自旋重试。是一种乐观锁。
对于CAS中的ABA问题(即将一个值从A改到B再改到A,此时比较值会认为没有修改),可以考虑加一个版本号来解决。JDK提供了一个AtomicStampedReference
类,内部维护了一个stamp版本号。如果reference或stamp有一个发生了变化,则更新失败。
13.synchronized锁升级过程?
锁分为四个状态,无锁、偏向锁、轻量级锁、重量级锁。
程序启动4s内没有线程访问synchronized代码块,则为偏向锁,类似if条件的CAS。mark word中会记录线程id,执行完线程不会主动释放偏向锁,因此下次进入代码块时之需判断线程id是否相同,无需再加锁,效率很高。如果有其它线程来争抢,则升级为轻量级锁,类似while条件的CAS。如果超过10次自旋失败,则升级为重量级锁,线程会阻塞。重量级锁被释放后会回到无锁状态,此时再有线程来,会直接升级为轻量级锁,偏向锁被禁用了。因为既然出现过重量级锁,那么一定有线程争抢,升级偏向锁没有意义。
14.synchronized和lock锁的区别?
synchronized在JDK1.6之前是重量级锁,是交由操作系统通过切换CPU状态来实现的。编译后,会在代码块的前后加上monitorEnter和monitorExit。
在JDK1.6之后,对锁进行了优化,提出了偏向锁和轻量级锁。锁状态保存在对象头的mark word里面,mark word有32位,最后3位表示了锁状态。
Lock锁与synchronized关键字关系相似,但是Lock锁使用更灵活,但是用完需要记得释放。
15.公平锁和非公平锁区别?
区别
公平锁按队列先进先出的原则来获取锁,新来的线程如果发现锁被占用,只能进队列排队。
非公平锁会先尝试CAS来获取锁,只有获取不到才会进队列。因为公平锁需要去队列中唤醒第一个线程,而非公平锁不需要,所以非公平锁效率高。但是非公平锁可能导致某个线程长时间获取不到锁。
ReentrantLock默认是非公平锁。
16.什么是AQS?
AQS(AbstractQueuedSynchronizer)抽象同步队列,内部维护了一个volaile修饰的state
,一个独占线程exclusiveOwnerThread
,和一个等待队列
。当一个线程获取锁时,会尝试通过CAS将state从0改为1,表示锁被占用,如果成功则将独占线程设置为自身。否则就进入等待队列中。
ReentrantLock就是通过AQS来实现的。内部持有一个Sync对象,Sync继承了AQS。Sync有NonfairSync和FairSync两种实现。
17.ThreadLocal总结
ThreadLocal用来为每个线程保留一份私有资源,避免多线程安全问题。
历史:
在早期,ThreadLocal内部维护一个Map,其中key是thread,value是每个线程私有的资源,理解起来比较简单。
现在是Thread内部维护了一个threadLocalMap,其中key是threadLocal,value是线程私有的资源。
这样有两个好处:Map变小(只保存当前线程的资源),哈希冲突会减少很多。二是如果线程销毁了,map也会销毁,节约内存。
数据结构:threadLocalMap内部维护了一个Entry数组,采用线性探测法存储数据(与HashMap有区别),当节点数达到数组长度2/3时,会触发扩容。
清理:分为探测式清理和启发式清理。即当发现entry不为null而key为null时,将这个entry清理掉。
18.ThreadLocal中的弱引用
threadLocalMap.Entry继承了WeakReference,其中key指向threadLocal对象,value指向实际存放的资源。
一般线程池中核心线程的寿命与项目相同,而threadLocal对象是短生命周期。当栈中threadLocal的引用被回收时,意味着threadLocal对象也应当被回收。
如果key指向threadLocal的是强引用,根据引用计数法可知,threadLocal对象无法被销毁。如果是弱引用,那么threadLocal对象可以在GC时被销毁。就避免了threadLocal的内存泄漏。
19.ThreadLocal内存泄漏问题
上述可知,使用了弱引用,可以避免threadLocal的内存泄漏。但由于线程依然存活,Entry存在强引用无法被销毁,value因为由entry持有也无法销毁。而threadLocal变量已被销毁,无法通过threadLocal获取到对应的value。还是会导致value的内存泄漏。
JDK提供了一种被动的解决方案:ThreadLocal的get()、set()、remove()方法在调用时,如果判断key为null,则会将value和entry也置为null,来帮助GC。但这种方案是不完善的,如果这些方法一直不被调用,那还是会发生内存泄漏。
主动解决方案:
- 在结束对threadLocal的调用时,应当执行一次remove方法。在remove方法中,会将key、value、entry都置为null,帮助GC回收。
- 可以将threadLocal设置为静态,这样threadLocal只会有一份,相当于每个线程中的threadLocalMap只会有一个节点,那么即使不回收问题也不大。
20.Java中的引用类型
- 强 — new一个对象时赋值的引用,任何时候都不会被清除;
- 软 — SoftReference,在GC时,如果内存不足,则会被回收;
- 弱 — WeakReference,在GC时一定会被回收,在ThreadLocal中用到;
- 虚 — PhantomReference,用途是在GC时返回一个引用。
spring
1.spring中如何解决循环依赖?
什么是循环依赖:就是A依赖B,B依赖A,构成了闭环。两个bean互相等待持有对方。可以分为构造器循环依赖和属性循环依赖。
spring中解决:spring中使用三级缓存的方式解决。
- 当创建A对象时,发现需要B对象,则去创建B对象;
- B对象发现需要A对象,则从一级缓存singletonObjects中去找;
- 一级缓存找不到,则去二级缓存earlySingletonObjects中去找;
- 二级缓存也找不到,则去三级缓存singletonFactories中找到objectFactory,再通过getObject方法获取A对象;
- getObject方法会通过A的无参构造创建一个对象,并将对象存到二级缓存,并从三级缓存中移除;
- B对象现在持有了一个不完整的A对象,但至少可以正常创建B;
- 回到A对象,此时A对象持有了B对象,因此也可以正常创建A。
- 最后B对象也持有了一个完整的A对象。
Redis
1.redis的持久化机制?
redis的持久化分为AOF(Append Only File)和RDB(Redis Date Base)。
其中,AOF是以日志形式存储的redis写命令。优点是安全,当配置策略为everysec时,最多只会有1s的数据丢失。缺点是文件大、数据恢复慢。
RDB是以二进制形式存储的快照文件,在执行bgsave命令后,主线程会fork出一个新线程来记录当前的字典状态。优点是文件体积小,数据恢复快。缺点是有数据丢失的风险。
2.项目中如何使用持久化的?
在4.0版本之后,redis开始支持混合持久化模式。通过修改aof-use-rdb-preamble yes
来开启,默认关闭。
开启后,当执行bgReWriteAof命令后,会新创建一个AOF文件。其中,文件的前半部分是以二进制形式存储,后半部分是以日志形式存储。
混合持久化模式集成了AOF和RDB的优点,但是由于AOF中含有二进制格式日志,可读性变差。
3.热key问题怎么发现?怎么解决?
发现:提前预知,比如秒杀活动;在redis上层环节进行统计;在代理层做收集,如twemproxy;自带的redis-cli -hotkeys命令。
解决:加到hashMap中,使请求在JVM中直接返回;备份热key,把key+随机数,然后分散到不同的节点上;对热key分布比较多的主机,多增加几个从节点。
4.redis的一致性哈希算法
redis集群本身没有支持一致性哈希,一致性哈希是在Jedis或twemProxy中实现的。
传统的槽分配方法,比如按顺序分配或按余数分配,扩展性和容错性都不够好。因此引入了一致性哈希算法。
对于节点,可以根据节点名或节点ip的哈希值,计算出对应的槽位。每个节点负责自己之前的槽位。这样如果新增一个节点,则其负责的槽位就是上一个节点到自己之间的部分,只需要移动这部分即可。同理,如果某个节点被移除,则只需要将自己负责的槽位转交给下一个节点即可。
对于数据倾斜和节点雪崩,导致剩余节点压力过大的问题,可以考虑使用虚拟节点的方式。
5.redis为什么快
- redis使用IO多路复用,只有一个线程去处理socket连接,避免线程上下文切换,也不会导致死锁
- redis完全基于内存
- redis提供的高效的数据结构,比如字典(哈希表)、跳跃表(链表+多级索引)、压缩列表、简单动态字符串(记录长度、空间预分配、惰性空间释放)、双向链表
6.redis的IO多路复用
包含四个部分:与客户端建立的socket套接字、IO多路复用程序、文件事件分派器、文件事件处理器(命令请求、命令回复、连接应答)。
IO多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。
7.redis是单线程的吗?
redis不是严格意义的单线程。redis的单线程是指IO线程和读写线程。而如持久化是多线程的。
8.redis的内存淘汰策略?
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
- volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
- allkeys-random:加入键的时候如果过限,从所有key随机删除
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu:从所有键中驱逐使用频率最少的键
9.redis的过期键删除策略?
- 定时删除。使用定时器,可以保证过期键会尽可能快的被删除。内存友好,CPU不友好。
- 惰性删除。只在查询的时候判断当前键是否已过期,过期则删除。对CPU友好,对内存不友好。
- 定期删除。定时扫描,从过期字典中随机选择20个键,删除其中过期的。如果过期比例超过1/4,则重新扫描。性能消耗介于其它两种策略之间。
redis中是使用的是定期删除和惰性删除的结合。
10.redis的三种集群模式对比?
redis的集群模式,从前往后分为三个阶段。主从复制、哨兵模式、cluster模式。
- 主从复制:通过执行slaveof命令,可以将一个节点作为另一个节点的从节点。主节点通过同步和命令传播使主从间的数据一致。优点是做到了读写分离,缺点是当主节点挂了,需要手动执行slave no one将从节点升级为主节点。
- 哨兵模式:哨兵也是一种特殊的redis-server。哨兵会监控集群中的主节点,当一个哨兵判断节点下线后,会进入主观下线模式。当所有哨兵都判断节点下线后,会进入客观下线模式。继而选举出一个从节点成为新的主节点。
- cluster模式:哨兵模式实现了集群自动恢复可用,但每个master上保存的都是全量数据,没法做到数据分片。cluster模式使用16384个哈希槽,将每个key映射到不同的槽位上,每个主机负责分担一部分槽位。增加了集群的写能力。同时,cluster是去中心化的,节点间通过ping-pong机制通信,一旦发现某个master下线,就会触发选举机制,从对应的slave中选出一个作为新的master。原master上线后会复制新的master。选举遵循的是先到先得原则,在每个投票周期内,每个master都有一次投票机会,并且会将票投给最先请求的slave。如果一个slave获得的票数超过一半,则选举成功。否则选举失败,进入下一轮投票。
11.缓存穿透、缓存击穿、缓存雪崩
缓存穿透:指的是请求一个不存在的key,会绕过缓存直达数据库,然后数据库也查不到,最终返回null
解决方案:
- 即使结果是null,也要缓存。但这样不能解决大量不同的key,比如同一时间查询很多id为负值的记录;
- 在上层对请求参数校验,过滤掉不符合规则的参数;
- 使用布隆过滤器。布隆过滤器中不存在,则数据库中一定不存在。
缓存击穿:指的是同一时刻大量请求查询一个失效的key,导致请求都落到了数据库
解决方案:
- 热点key不设置过期时间;
- 数据库设置互斥锁,避免大并发访问数据库,等缓存添加后,请求就能恢复。
缓存雪崩:指的是同一时刻大量key到期,导致查询都来到了数据库。缓存击穿是大量相同请求,缓存雪崩是大量不同请求
解决方案:
- 不同的key设置不同的过期时间,比如 固定时长 + 五分钟内的随机数
- 搭建redis集群,提高redis容灾能力
- 使用熔断机制,当流量达到阈值,拒绝后续请求,保证一部分用户可以正常访问
- 提升数据库容灾能力,如分库分表,读写分离
JVM
1.GC要做的三件事
- 哪些内存要回收:引用计数法、可达性分析法(重要)
- 什么时候回收。不可达的对象,并不是立刻就会被回收,而是会经过一次标记:如果对象没有重写finalize()方法,或者finalize()方法已经被调用,虚拟机会判定这个对象没必要执行finalize(),在这一次标记中该对象不会被回收。如果这个对象被标记为有必要执行finalize()方法时,它会被放置在一个名为F-Queue的队列中,稍后由虚拟机进行垃圾回收。但是这个对象还有最后一次逃脱的机会,当在F-Queue时,虚拟机会对F-Queue中的对象作小规模的标记,如果发现此时某个对象又可达了,就会逃过GC的命运。
- 怎么回收:四种回收算法
2.判断哪些对象可以被回收
- 引用计数法。只要一个对象被引用一次,引用计数就会加一。如果一个对象的引用计数为0,则说明这个对象可以被回收。但是引用计数法解决不了对象互相引用的问题。
- 可达性分析法。如果一个对象无法被称为GCRoot的对象访问到,则表示这个对象可以被回收。GCRoot可以是虚拟机栈中的引用对象、方法区静态变量、方法区常量、本地方法栈中的JNI对象。
3.JVM中垃圾回收算法
- 标记清除 — 容易产生内存碎片
- 标记复制 — 内存一份为二,浪费空间
- 标记整理 — 标记存活的对象,向内存的一端移动,并将端外的对象清除。
- 分代回收 — 新生代采用标记复制,老年代采用标记整理。一般新生代很老年代空间是1:2,新生代中分为eden区,survive1区,survive2区。第一次GC时,eden区中存活的对象会移动到s1区,然后清除eden区。第二次GC时,会将eden和s1中的对象移动到s2区,然后清除eden和s1区,以此类推。对象熬过15次GC,会移动到老年代。
4.垃圾收集器
- 串行垃圾回收器。为单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境。新生代(Serial),老年代(Serial Old)。
- 并行垃圾回收器。多个垃圾回收器并行工作,此时用户线程是暂时的,适用于科学计算/大数据处理等弱交互场景。新生代(ParNew、Parallel Scavenge),老年代(Parallel Old)。
- 并发清除回收器(Concurrent Mark Sweep,即CMS)。是一种以获取最短回收停顿时间为目标的收集器。CMS非常适合堆内存大、CPU核数多的服务器端应用,也是G1出现之前大型应用的首选收集器。用户线程和垃圾收集线程可以同时执行。主要在老年代回收。
- G1(Garbage-First)。将内存分割成不同的区域,并发进行回收,属于标记整理算法。同CMS相比,不会产生大量内存碎片,并可以添加预测机制,用户可以指定期望停顿时间。
5.常用JVM命令
- jps 显示系统内所有运行的HotSpot虚拟机进程
- jstat 显示虚拟机运行时状态信息的命令
- -class 类装载、卸载情况
- -gc 垃圾回收堆的行为统计
- imap 用于生成heap dump文件
- jstack 生成虚拟机当前时刻的线程快照
- jinfo 用来查看虚拟机运行参数
6.JVM参数
JVM参数总共分为三类:
- -开头,标准参数,所有JVM都兼容;
- -X开头,非标准参数,不保证所有JVM都兼容;
- -XX开头,非稳定参数,不保证所有JVM都兼容,且可能随时取消。
下面分别举例:
-开头
- -verbose:gc 会输出每次GC的信息
-X开头
-Xms (memory size)初始化堆内存,一般与Xmx相同,避免垃圾回收后重新分配
-Xmx (memory max)最大堆内存
-Xmn (momery new)新生代内存
-Xss (statck size)每个线程栈大小
-XX开头
-XX: +UseG1GC 使用G1垃圾收集器
-XX: -UseConcMarkSweepGC 使用CMS垃圾收集器
-XX:+PrintGCDetails 打印GC详情
- -XX:SurvivorRatio=8 表示eden/survivor空间比为8
7.双亲委派机制是什么?
当要加载一个类时,首先AppClassLoader会判断该类是否加载过,如果没有则转到ExtClassLoader继续检查,如果还没有加载则转到BootstrpClassLoader。BootstrpClassLoader会判断自己是否能加载该类,如果不能加载则委托给ExtClassLoader,如果还不能加载则委托给AppClassLoader,最后还不能加载则抛出ClassNotFoundException异常。因此这是一种向上检查,向下委派的机制。
好处如果有人想替换系统级别的类,如String.java,那么首先会由BootstrpClassLoader来加载,使其它类无法加载,从而避免了危险代码的植入。
8.Java中堆、栈、方法区
堆中存放的是创建的对象;
栈中存放的是基本类型变量,引用类型的变量;
方法区中存放的是class和静态变量。
网络
1.在浏览器输入一个地址后,会发生什么?
- 根据网址去解析ip,分别是浏览器缓存-操作系统缓存-路由器-本地域名服务器-根域名服务器
- 建立TCP连接,三次握手
- 浏览器发送请求
- 服务器处理并响应请求
- 浏览器渲染画面
- 断开TCP连接,4次挥手
2.TCP连接的三次握手
客户端发送SYN(同步)包,序号Seq=x,等待服务器确认;
服务器返回SYN和ACK,ACK序号为Seq=y,确认号Ack=x+1;
客户端发送ACK,确认号为Ack=y+1;
3.TCP通信如果没有第三次握手会怎样?
虽然在前两次握手后,服务端已经准备好接收请求。但如果没有第三次,服务端就会释放资源。如果客户端发送请求,服务端会回复RST,客户端就会知道连接失败。
MQ
1.为什么要用MQ
MQ的三个作用:异步、削峰、解耦
- 异步:比如发送短信,可以通过异步的方式,不影响订单的创建
- 削峰:短时间的大量消息,可以存储在MQ中,让消费者根据消费能力慢慢去消费
- 解耦:比如新接入系统,直接去队列中读取消息即可,不需要修改代码
2.MQ的缺点
系统可用度降低,MQ挂了,整个系统都会受到影响;
系统复杂度增加,需要考虑消息重复消费、消息丢失等问题。
3.MQ技术选型
语言 | 优点 | 缺点 |
---|---|---|
RabbitMQ | Erlang语言编写,天生高并发;支持多种语言接入,如果项目涉及到多种语言,首选;界面功能强大 | 出问题难以维护,难以定制化开发 |
ActiveMQ | Apache出品,Java语言开发,支持多语言 | 已不再维护 |
RocketMQ | 阿里出品,Java语言开发,参考了Kafka,并发高于RabbitMQ,响应快 | 专为电商设计,主要只支持Java语言 |
Kafka | scala编写,支持高并发,性能比RocketMQ和RabbitMQ都要好,用于日志领域 | 延迟比较高,不适合电商场景 |
4.MQ避免消息丢失
生产端:开启confirm,将消息在数据库存一份,只有在收到ack时才删除。
MQ端:配置durable参数为true,持久化消息到磁盘。
- 消费端:开启手动确认,只有在消息消费后才确认。
5.MQ避免消息重复消费
保证消费端幂等。每条消息都携带一个唯一id,消费时将该消息存入数据库,通过唯一性约束来保证,存入失败则必然重复消费了。
6.MQ保证顺序消费
- 生产者保证消息按顺序投递;
- 同一个操作的消息投递到同一个队列;
- 同一个队列只能由同一个消费者消费。
Docker
1.docker常用命令
1 | 运行docker应用程序,输出hello world,-d后台运行 |
ES
ES答的好是绝对加分项,答的不好也是大的扣分项,简历慎重写。
1.倒排索引的理解?
倒排索引是普通索引的逆过程,即从内容→ID
文章数量是无限的→拆分成词,词的数量是有限的→内部维护一个词典→对单词排序,建立索引(底层也是B+树)→每个词对应一个文章列表,记录当前词在对应文章中的偏移量(多个)和权重→权重可以是词出现的频率或出现的数量。
存入文章时,先对文章分词,每个词都维护了一个指向文章的引用。
查询时,对查询的内容分词,获取每个词对应的文章列表,并按权重返回。
2.常用es命令
- curl -X GET localhost:9200/_cat/nodes查看所有节点信息
- curl -X GET localhost:9200/_nodes/nodeName?pretty=true查看指定节点信息
- curl -X GET localhost:9200/_cat/indices?v查看所有索引库,-v显示表头
- curl -X PUT localhost:9200/test新建索引库
- curl -X DELETE localhost:9200/test删除索引库
- curl -X GET localhost:9200/test?pretty查看索引库,pretty表示数据格式化
- curl -X GET localhost:9200/index_name/_search?pretty查看索引库中的全部文档