Java后端面试题汇总2023

这是2023年年初换工作面试时总结的,汇总了面试时的高频题。每一题都是在搞懂的情况下,用自己的语言回答的。目前内容还不完善,一是技术面不够广,二是研究的还不深入。后面会不断丰富。写这个既是对自己学习的总结,也希望对正准备面试的小伙伴能有所帮助。


Mysql

1.mysql索引有哪些类型?

  • 按数据结构来分,有B+树hash全文索引。哈希索引不支持排序,不支持范围查询,只支持等值查询。复杂度O(1);
  • 按物理存储来分,有聚簇索引非聚簇索引

    • 前者叶子节点存储的是数据页,后者叶子节点存储的是索引和主键。
    • 主键索引是聚簇索引,其它字段建的索引都是非聚簇索引。
    • 非聚簇索引在查询时,可能需要回表。
    • 聚簇索引在增删改的时候需要更新索引树。
  • 按字段特性来分,有主键索引唯一索引普通索引前缀索引。前缀索引就是当字段比较长的时候,建索引会很大,不建索引又很慢。可以只对字符串前面一部分建立索引,达到时间和空间的均衡。

  • 按字段数量来分,有单列索引联合索引

2.Innodb支持hash索引吗?

Innodb不能手动创建哈希索引,Innodb支持自适应哈希索引。

当Innodb判断建立哈希索引可以提升查询效率时(由于哈希索引查询复杂的是O(1),B+树索引查询复杂度是O(logn),当树的高度比较大时,B+树查询效率较低),就会建立哈希索引。

由于哈希索引底层是哈希表(数组+链表)结构,适合存在内存中,因此哈希索引不会生成磁盘文件。

3.联合索引的作用?

  1. 节约空间。索引本身会存在空间开销,联合索引有助于降低开销。
  2. 索引覆盖。查询多个条件时,如果查询的字段都在联合索引内,就可以避免回表。

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.使用索引一定能提升效率吗?

不一定。因为索引本身要占空间,索引有创建成本,然后还有每次增删改时的维护成本。

比如下面两种情况:

  1. 查询中使用少的字段不要加索引;
  2. 数据种类少的字段也不要加索引,如性别。

6.InnoDB索引和MyIsam索引有什么区别?

  1. innodb使用聚簇索引和非聚簇索引,其中聚簇索引叶子节点存储的是主键id;
  2. myIsam只有非聚簇索引,但叶子节点存储的是数据行地址。

7.为什么说MyIsam索引查询速度更快?

  1. 因为MyIsam的非聚簇索引叶子节点直接存的是数据行地址,不需要回表,因此查询更快;
  2. MyIsam不支持事务,InnoDB支持事务,即使没有用到,但也少不了检测的步骤,这就影响了效率。

8.什么是MVCC?

MVCC(Multi Version Concurrency Control),即多版本并发控制,主要是为了解决数据库读写时的并发问题。

  • 读读,没有并发问题;
  • 写写,可以加锁;
  • 读写,要用到MVCC,为每条记录维护一个版本链,使读写对应不同的版本。

原理

每张表都隐藏了trx_idroll_pointer字段,即事务id回滚指针。每次修改,都会逆向生成一条undo日志,表示一个版本。回滚指针总是指向上一个版本,形成版本链。

MVCC支持在RC/RR隔离级别中生效。对于RC,事务中每个快照读都会创建ReadView,因此读取的是已提交的最新的记录,或者是当前事务修改但未提交的记录。对于RR,只在第一次快照读时创建ReadView,在之后都不再创建ReadView,因此读取的是在当前事务之前提交的记录,或是自己修改但未提交的记录。对于RU,只有快照读,因此存在脏读的问题。

9.什么情况会索引失效?

  1. 隐式类型转换。对字段加减操作、类型转换、使用函数都会造成索引失效。理解:因为以上操作都会造成字段的索引树结构变化,导致走索引代价太大。比如原先’1’在’a’之前,类型转换后’1’→1,’a’→0,导致’1’要移动到’a’之后,造成索引结构变化。

  2. 不符合最左前缀原则。联合索引要符合最左前缀原则,即左边第一个字段必须出现,无关顺序。对于(a,b,c)联合索引,(a,b,c),(a,b),(a,c),(a)四种情况可以走索引。

  3. 引擎判断全表扫描效率更高。如select * from t order by a,b,c;尽管走abc联合索引避免了重新排序,但由于查询的是*,会导致回表多次。而全表扫描虽然需要重新在内存中排序,但不需要回表,相比之下全表扫描效率更高。

  4. 模糊查询,%在左的情况。

10.Innodb中的三层B+树可以储存多少数据?

B+树中每个节点都是数据页,一页的大小默认是16KB。其中叶子节点存储的是数据行,以一行数据1KB计算,一页可以存储16KB/1KB=16条记录。一层和二层节点存的是索引和指向子节点的指针。int类型的索引是4字节,指针是6字节,因此一个节点是10字节,一页可以存储16KB/10B=1638个索引,两层节点可以存储1638x1638=268w个索引,三层节点可以存储268wx16=4300w条记录。超过这个数B+树层高就会增加,导致IO次数增加,表现为查询变慢。

11.如何提高insert的效率?

  1. 一次插入多条,可以减少日志量(如binlog)、减少sql解析次数。如insert into t values(a,b,c) (d,e,f)...
  2. 调大bulk_insert_buffer_size,调大缓存,仅作用于MyISAM。
  3. 开启手动提交事务。

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.创建线程的三种方式与对比?

三种方式

  1. 集成Thread,重写run方法。
  2. 实现Runnable接口,重写run方法,并将runnable传入thread中。
  3. 实现Callable接口,重写call方法,并将callable传入futureTask中,将futureTask传入thread中。

    对比

  4. Runnable相比Thread,适合多个线程资源共享,避免Java中单继承的限制,线程池只能接受Runnable类型。

  5. 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为什么快

  1. redis使用IO多路复用,只有一个线程去处理socket连接,避免线程上下文切换,也不会导致死锁
  2. redis完全基于内存
  3. redis提供的高效的数据结构,比如字典(哈希表)、跳跃表(链表+多级索引)、压缩列表、简单动态字符串(记录长度、空间预分配、惰性空间释放)、双向链表

6.redis的IO多路复用

包含四个部分:与客户端建立的socket套接字、IO多路复用程序、文件事件分派器、文件事件处理器(命令请求、命令回复、连接应答)。

IO多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字。尽管多个文件事件可能会并发地出现,但I/O多路复用程序总是会将所有产生事件的套接字都放到一个队列里面,然后通过这个队列,以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕),I/O多路复用程序才会继续向文件事件分派器传送下一个套接字。

7.redis是单线程的吗?

redis不是严格意义的单线程。redis的单线程是指IO线程和读写线程。而如持久化是多线程的。

8.redis的内存淘汰策略?

  1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
  2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
  3. volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
  4. allkeys-random:加入键的时候如果过限,从所有key随机删除
  5. volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
  6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
  8. 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

解决方案

  1. 即使结果是null,也要缓存。但这样不能解决大量不同的key,比如同一时间查询很多id为负值的记录;
  2. 在上层对请求参数校验,过滤掉不符合规则的参数;
  3. 使用布隆过滤器。布隆过滤器中不存在,则数据库中一定不存在。

缓存击穿:指的是同一时刻大量请求查询一个失效的key,导致请求都落到了数据库

解决方案

  1. 热点key不设置过期时间;
  2. 数据库设置互斥锁,避免大并发访问数据库,等缓存添加后,请求就能恢复。

缓存雪崩:指的是同一时刻大量key到期,导致查询都来到了数据库。缓存击穿是大量相同请求,缓存雪崩是大量不同请求

解决方案

  1. 不同的key设置不同的过期时间,比如 固定时长 + 五分钟内的随机数
  2. 搭建redis集群,提高redis容灾能力
  3. 使用熔断机制,当流量达到阈值,拒绝后续请求,保证一部分用户可以正常访问
  4. 提升数据库容灾能力,如分库分表,读写分离

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参数总共分为三类:

  1. -开头,标准参数,所有JVM都兼容;
  2. -X开头,非标准参数,不保证所有JVM都兼容;
  3. -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.在浏览器输入一个地址后,会发生什么?

  1. 根据网址去解析ip,分别是浏览器缓存-操作系统缓存-路由器-本地域名服务器-根域名服务器
  2. 建立TCP连接,三次握手
  3. 浏览器发送请求
  4. 服务器处理并响应请求
  5. 浏览器渲染画面
  6. 断开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避免消息丢失

  1. 生产端:开启confirm,将消息在数据库存一份,只有在收到ack时才删除。

  2. MQ端:配置durable参数为true,持久化消息到磁盘。

  3. 消费端:开启手动确认,只有在消息消费后才确认。

5.MQ避免消息重复消费

保证消费端幂等。每条消息都携带一个唯一id,消费时将该消息存入数据库,通过唯一性约束来保证,存入失败则必然重复消费了。

6.MQ保证顺序消费

  1. 生产者保证消息按顺序投递;
  2. 同一个操作的消息投递到同一个队列;
  3. 同一个队列只能由同一个消费者消费。

Docker

1.docker常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 运行docker应用程序,输出hello world,-d后台运行
$ docker run ubuntu:15.10 /bin/echo "Hello world"

# 查看运行容器
$ docker ps

# 查看容器内的标准输出
$ docker logs {容器id}

# 启动一个已停止的容器
$ docker start {容器id}

# 停止容器
$ docker stop {容器id}

# 重启容器
$ docker restart {容器id}

# 获取ubuntu镜像
$ docker pull ubuntu

# 进入一个后台运行的容器
$ docker attach

# 退出容器终端,但不停止容器
$ docker exec

# 导出容器快照到本地文件
$ docker export 1e560fca3906 > ubuntu.tar

# 导入容器快照
$ cat docker/ubuntu.tar | docker import - test/ubuntu:v1

# 列出本机镜像
$ docker images

# 下载镜像
$ docker pull ubuntu:13.10

# 查找镜像,httpd镜像作为web服务
$ docker search httpd

# 删除镜像
$ docker rmi hello-world

# 设置镜像标签为dev
$ docker tag 860c279d2fec runoob/centos:dev

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查看索引库中的全部文档
文章作者: SongGT
文章链接: http://www.songguangtao.xyz/2023/02/28/28.Java后端面试题汇总2023/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 SongGuangtao's Blog
大哥大嫂[微信打赏]
过年好[支付宝打赏]