常见面试知识整理

391 阅读21分钟

谈谈你对java平台的理解

首先讲一下java语言的三大特性:

跨平台性 书写一次到处运行

我们编写的.java文件 通过javac编译后生成.class字节码文件与机器无关 Oracle 官方为不同操作系统提供了不同版本的JDK对应不同版本的JVM,.class文件通过JVM虚拟机加载到内存中进行 通过JVM虚拟机屏蔽了内存和操作系统的差异。

JRE java运行时环境 JVM虚拟机 java核心类库 只能运行java程序 JDK java开发工具包 JDK包含JRE以及以及javac编译器 监视 调试 调优工具等一系列开发工具

Spring事务

juejin.cn/post/684490…

  • 事务的ACID特性
  • 事务的隔离级别
  • 并发事务带来的问题
  • 事务的传播行为

HTTP 与 HTTPS区别

hhtp常见面试题

  • HTTP :HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。

  • HTTP状态码

  • GET和POST区别,安全和幂等性

  • SSL/TLS

    • 混合加密
    • 摘要算法
    • 数字证书
  • TCP三次握手和四次挥手

建立一个TCP连接需要三次握手 第一次 客户端发送数据包给服务器端 服务端so

关闭一个TCP连接需要四次挥手

HTTP 与 HTTPS 网络层

AOP

juejin.cn/post/684490…

juejin.cn/post/684490…

  • JDK动态代理
  • CGLIB动态代理
  • AOP实现日志过程
  • 两种动态代理区别
术语概念
Before在方法被调用之前执行增强
After在方法被调用之后执行增强
After-returning在方法成功执行之后执行增强
After-throwing在方法抛出指定异常后执行增强
Around在方法调用的前后执行自定义的增强行为(最灵活的方式)

线程池

juejin.cn/post/684490…

www.jianshu.com/p/9710b899e…

  • 池化技术作用

  • 核心参数及作用

  • 线程池运行原理

  • 拒绝策略 4种

  • 线程池中的异常处理方法 3种

  • 线程池的工作队列

  • 几种常用的线程池

  • 线程的状态(6种)

线程状态 线程状态

HashMap

  • 数据结构
  • 重要参数
  • 构造函数
  • put方法解析
  • 索引的计算
  • 扩容机制

mysql

回表查询和覆盖索引

  • 回表查询(查询字段不在索引中,先定位主键值,再通过主键值定位行记录,性能上较之直接查询索引树定位行记录更慢。)
  • 覆盖索引(只需要在一棵索引树上就可以获取sql所需所有的列数据,不需要回表,较之回表速度要更快。将被查询的字段建立到联合索引中)
  • 索引B+树
  • 慢查询优化 常见慢查询优化
  • 聚集索引和非聚集索引

慢查询优化

详述如何优化慢查询? 如果表中没有添加索引,为表中添加索引,考虑在 where 及 order by涉及的列上建立索引。 如果添加了索引首先判断是否是索引失效没有起作用 索引常见的失效原因有:

  • 使用or关键字的字段没有添加索引,索引失效会进行全表扫描
  • 前置%,使用like关键字以%开头索引会失效,以%结尾缩影可以使用
  • where条件中对索引列上有数学运算,则索引失效。
  • 应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描
  • 在为多个字段创建索引后,对于多列索引,只有查询条件使用了这些字段中的第一个字段,索引才会被使用,也就是最左匹配原则。
  • 应尽量避免在 where 子句中对字段进行 null 值判断,where中进行null值判断将导致引擎放弃使用索引而进行全表扫描,
  • 应尽量避免在 where子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描 查询出的数据量过大(可以采用多次查询,其他的方法降低数据量) 可以采用多线程,分段查询 返回了不必要的行和列,尽量少使用select * ,查询尽量选择用得到的字段,没用到的字段不要查出来 尽可能多的使用数字型字段,因为mysql引擎在处理查询的时候对字符串是逐个比较每个字符,而数字型只需要比较一次就够了效率较高。 对表进行横向和纵向切割,降低表的复杂性和数据量大小。 索引

索引失效的情况

使用or条件加上没有设置索引的字段(查询没有设置索引的字段会进行全表扫描) 查询字符串类型的字段在写SQL语句的时候一定要用单引号引起来,要不然不走索引。(这是因为不加单引号时,是字符串跟数字的比较,它们类型不匹配,MySQL会做隐式的类型转换,把它们转换为浮点数再做比较。) 模糊查询使用like的情况,如果开头第一个是%则索引失效 解决办法: (1)把%放在后面 (2)使用覆盖索引,即查询的列全部设置了索引,这样也会走索引 联合索引是查询条件列不包含联合索引中第一个列,则不符合最左匹配原则,索引失效 如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则,先会查k1索引,如果k1不存在则没办法以索引方式查 联合索引中如果查询的是范围也会失效 在where条件上对索引列进行了数学运算或者使用了内置函数,索引失效 索引字段上使用(!= 或者 < >,not in)时,会导致索引失效。 索引字段上使用is null, is not null,可能导致索引失效。 mysql在数据量较小时,估计使用全表扫描要比使用索引快,则不使用索引。 mysql索引的最左匹配原则? 例如建立了联合索引(a,b,c),mysql是按照联合索引字段从左到右来进行查询的,会先比较a再比较b然后比较c,如果mysql在查询中没有用到联合索引的第一个字段,那么就无法按照先a后b再c进行索引查询则联合索引就失效了。

加索引为什么能优化慢查询? 因为索引是一种提高查询速度的数据结构,Mysql中的索引使用的是B+树实现,B+树是一种多路平衡搜索树用于提高查找速度,还有一些结构可以提高查询速度,哈希表,平衡二叉树,B树等。

JVM虚拟机

垃圾回收器

[juejin.cn/post/684490…]

  • Serial (单线程,新生代,copy算法)
  • Parallel New(多线程,新生代,copy算法,时间优先)
  • Parallel Scavenge(多线程,新生代,copy算法,吞吐量优先)
  • Serial Old(单线程,老生代, 标记清除)
  • Prallel Old(多线程,老生代, 标记清除)
  • CMS:垃圾收集器跟应用并行(多线程,老生代, 标记清除)分两次清理,第一次清理的时候,工作线程跟垃圾线程并行,第二次STW。(Java 9 被废除)
  • G1 (新生代 + 老生代) CMS处理过程 初始标记,会导致stw,可达性分析法找到与GCroot直接关联的对象 并发标记,与用户线程同时运行,遍历堆内存中所有的垃圾。 预清理,与用户线程同时运行; 可被终止的预清理,与用户线程同时运行; 重新标记 ,会导致swt; 并发清除,与用户线程同时运行;

  • 互斥锁
  • 自旋锁
  • 读写锁
  • 悲观锁
  • 乐观锁

分布式锁

如何保证接口的幂等性

解决问题

  • 前端重复提交表单
  • 用户恶意进行刷单
  • 接口超时重复提交
  • 消息重复消费

参考文章

接口幂等性解决方案总结

  • 数据库唯一主键(唯一索引)使用分布式ID当主键,保证ID的全局唯一性
  • 悲观锁 数据库行锁 innodb引擎
  • 乐观锁 version版本号
  • 状态机判断 根据订单状态以及updateSql语句影响行数,重复操作影响行数为0
  • 下游生成分布式id发起请求,服务端结合setNx判断重复请求
  • 防重令牌Token

如何保证缓存与数据库双写时的数据一致性

redis

redis面试题 redis面试题

  • 基础数据类型
  • 单线程模型 (epoll多路复用IO技术www.cnblogs.com/yrjns/p/125…
  • 过期键值删除策略
  • 键值淘汰策略
  • 缓存穿透
  • 缓存击穿
  • 缓存雪崩
  • 主从复制
    • 复制机制(全量复制 增量复制)
    • 复制原理(版本号伪随机串+偏移量offset衡量数据的变化情况)
  • 哨兵模式
    • 监控各节点状态
    • 故障时通知其他节点
    • 故障转移(确认master下线 断开下线master与其他slave连接 确认一个新的master)

数据库读写分离

数据读写分离&分库分表

  • 读写分离作用
  • shardingJdbc
  • 数据库主从复制原理
  • 读写分离存在的问题

JVM之1.7于1.8内存区别

jvm面试题 jvm面试题2

jdk1.7的堆内存

  • Young 年轻代

Young区被划分为三个部分,Eden区和两个大小严格相同的Survivor区,其中,Survivor区间中,某一时刻只有其中一个是被使用的,另一个留做垃圾收集时复制对象用,在Eden区间变满的时候,GC就会将存活得的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移到Tenured区间。

  • Tenured 老年代

Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定次数以后,对象就会被转移到Tenured区,一般如果系统中用了Application级别的缓存,缓存中的对象往往会被转移到这一区间。

  • Perm 永久代

Perm代主要保存class,method,filed对象,这部分的空间一般不会溢出,除非一次性加载了很多的类,不过在涉及热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError:PermGen space的错误,造成这个错误的很大原因就有可能是每次都重新部署,但是重新部署后,类的class没有被卸载掉,这就造成了大量的class对象保存在Perm中,这种情况下,一般重新启动应用服务器可以解决问题。

Virtual

最大内存和初始内存的差值,就是Virtual区。

jdk1.8的堆内存

由上图可以看出,jdk1.8的内存模型是由两部分组成,年轻到 + 老年代。

  • 年轻代:Eden + 2 * Survivor。

  • 老年代:OldGen。

区域比例划分

image.png

新生代:老年代 1:2 新生代中 eden s0 s1=8:1:1

在jdk1.8中变化最大的就是Perm区,用Metaspace(元数据空间)进行替换。 注意:Metaspace所占用的内存空间不是在虚拟机内部,而是在本地内存空间中,这也是与1.7的永久代最大的区别所在。

现实使用中,由于永久代内存经常不够用或者发生内存泄漏,爆出异常:java.lang.OutOfMemoryError:PermGen。基于此,将永久代废弃,而改用元空间,改为使用本地内存空间。  元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:

image.png

JVM调优

为什么要进行JVM调优

如何保证线程安全

线程安全总体来说要从三个方面考虑保证内存模型的原子性 可见性 有序性

原子性

保证代码执行的原子性,借助于synchronized和Lock锁的方式来保证整块代码的原子性了。线程在释放锁之前,必然会把i的值刷回到主存的。

可见性

Java就是利用volatile来提供可见性的。 当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。

实现原理

  • Lock前缀的指令会引起处理器缓存写回内存;
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存失效;
  • 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。

有序性

JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。那么当我读变量时,总是能读到它的最新值,这里最新值是指不管其它哪个线程对该变量做了写操作,都会立刻被更新到主存里,我也能从主存里读到这个刚写入的值。也就是说volatile关键字可以保证可见性以及有序性。

volatile关键字

volatile面试详解

volatile两大特性

volatile修饰的共享变量,就具有了以下两点特性: 1 . 保证了不多线程情况下对该变量操作的内存可见性; 保证内存可见性首先要解释JMM内存模型,线程操作的都是将主内存的变量拷贝一份到工作内存中对拷贝变量进行修改后写回主内存,这样就导致多线程情况下每个线程都操作的是自己的变量副本,存在主内存变量的不可见性,Volatile关键字主要保证线程对变量副本进行修改后能立刻刷新到主内存中,保证线程对变量的操作在主内存中及时可见。 2 . 禁止指令重排序,JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。 happens-before原则;

volatile底层的实现机制

如果把加入volatile关键字的代码和未加入volatile关键字的代码都生成汇编代码,会发现加入volatile关键字的代码会多出一个lock前缀指令。 lock前缀指令实际相当于一个内存屏障,内存屏障提供了以下功能:

  • jvm进行重排序的时候不能把后面的指令排序到内存屏障之前的位置
  • 将volatile修饰的修改后变量的副本写入主内存中,cpu中有缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存变量对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,

volatile使用举例

单例模式的实现,典型的双重检查锁定(DCL)

强引用、软引用、弱引用、幻象引用,引用队列总结

引用总结

强引用

  • 强引用:形如Object object = new Object();这样就是典型的强引用,被强引用引用的对象不会被垃圾收集器主动回收,JVM宁愿抛出OutOfMemoryError运行时错误(OOM),使程序异常终止,也不会靠随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应强引用赋值为 null,这个被引用的对象就是可以被垃圾回收器回收的(具体回收时机还是要看垃圾收集策略)。

引用(Reference类)

  • 软引用(SoftReference)、弱引用(WeakReference)、幻象引用(PhantomReference)都是java.lang.ref.Reference的子类,这个Reference类主要有4个方法 void clean();清除此参考对象。(此方法仅由Java代码调用; 当垃圾收集器清除引用时,它直接执行,而不调用此方法。) boolean enqueue();将此引用对象添加到其注册的队列(如果有)。 T get();返回此引用对象的指示。(通过这个方法可以返回Reference所引用的对象,可以重新变成强引用)

软引用(SoftReference)

  • 软引用通过SoftReference类实现。软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用(注意是引用本身这个对象(就是Reference自己,并不是引用所引用的对象)加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收(因为在这个队列里面的引用所指向的对象都被回收了)。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。

  • 应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

弱引用(WeakReference)

  • 弱引用通过WeakReference类实现。 弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中(和引用队列一起使用同上面的软引用)。

  • 应用场景:弱应用同样可用于内存敏感的缓存。

虚引用(PhantomReference)

  • 虚引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动(和引用队列一起使用同上面软引用跟弱引用)。

  • 应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知

JDBC连接过程

BIO NIO AIO

juejin.cn/post/695908… 一个进程的地址空间划分为用户空间和内核空间。像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。 我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。

BIO Blocking IO

image.png 同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。

NIO 同步非阻塞IO

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。相比于同步阻塞 IO 模型,同步非阻塞 IO 模型通过轮询操作,避免了一直阻塞。 image.png

AIO 异步IO模型

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

image.png

总结

image.png

WebSocket

HTTP协议是半双工通信协议 ,只能客户端向服务端请求数据,服务端返回请求数据给客户端,服务器不能主动推送,每次获取数据都需要创建http请求耗费带宽和服务器资源。 WebSocket实现了全双工通信,它基于TCP传输协议,并复用HTTP的握手通道。允许服务端主动向客户端发送数据,在客户端和服务端上建立了一个长久的连接,两边可以任意发送数据,实时性很强可以用来做qq 微信,在客户端和服务端连接创建后数据包封装的包头较少极大控制开销。

HashMap和Hashtable的区别

HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。

ConcurrentHashMap

hashMap线程不安全的问题 put方法不安全多线程的时候导致数据不一致 A B线程put同一数据可能导致B插入的数据消失 1.7采用头插法两个线程检测到需要进行扩容的时候可能造成链表的循环成环。

ConcurrentHashMap

JDK1.7 使用segments分段锁的方式相当于一层二级HashMap,ConcurrentHashMap 下面是一个Segment[] 数组,每个Segment 下面是一个 HashEntry[] 数组 segment 继承ReentrantLock ,Segment 的put方法进行加锁 tryLock unlock

JDK1.8 移除Segment,使锁的粒度更小 put方法 CAS(拿到节点后判断为空则插入) + Synchronized + Node + Unsafe

抽象类和接口的区别

首先先说目的上的区别 抽象类是对事物进行抽象,接口是对能力/行为进行抽象 在语法上的区别 抽象类是单继承 接口可以多实现 抽象类和接口都不能被实例化,抽象类中可以包含非抽象的普通方法,接口中的所有方法必须都是抽象的,不能有非抽象的普通方法。

group by having limit order by

group by 分组 having 对where和group by处理的结果进一步筛选 , 得到我们想要的数据

mysql常见语法

  • left join(左联接)以左表为主 返回包括左表中的所有记录和右表中联结字段相等的记录。
  • right join(右联接) 返回包括右表中的所有记录和左表中联结字段相等的记录。
  • inner join(等值连接) 只返回两个表中联结字段相等的行。
  • 去重 distinct

怎么定位生成环境问题

常见排序算法

  • 选择排序
  • 冒泡排序
  • 快速排序
  • 堆排序

String对象的创建过程

String s1 = "abc";
判断该字符串对象是否存在于字符串常量池,不存在则在字符串常量池中创建对象,将其引用给s1,存在s1直接指向字符串常量池

String s = new String("abc"); 不存在的常量池中的话,在字符串常量池(编译器解析class文件时创建)和堆中新建对象(new关键字在堆中创建) 存在字符串常量池中的话则不用在字符串常量池中创建,但是堆中仍然new一个新对象

当执行String s = new String(“abc”);时,JVM首先在String Pool中查看是否存在字符串对象“abc”,如果不存在该对象,则先在String Pool中创建一个新的字符串对象“abc”,然后执行new String(“abc”)构造方法,在Heap里又创建一个新的字符串对象“abc”(new出来的对象都放在Heap里面),并将引用s指向Heap中创建的新对象;如果已存在该对象,则不用创建新的字符串对象“abc”,而直接使用String Pool中已存在的对象“abc”, 然后执行new String(“abc”)构造方法,在Heap里又创建一个新的字符串对象“abc”,并将引用s指向Heap中创建的新对象。

创建一个类在内存中的过程

JUC

线程创建的几种方式

线程创建方式

  1. 继承Thread类并重写run()方法,只能单继承
  2. 实现Runnable接口 可以多实现
  3. 实现Callabe接口 可以获取线程执行的结果 futureTask
  4. 线程池创建线程

设计模式

简单工厂 工厂模式 抽象工厂模式

简单工厂 工厂模式 抽象工厂模式

简单工厂模式

  • 一个抽象产品类、具体产品类、一个工厂把类的实例化交给工厂,易于解耦。

工厂方法模式

  • 实现了开放封闭原则 - 每次添加子产品 不需要修改原有代码
  • 多个具体工厂 - 每一个具体产品对应一个具体工厂

image.png

抽象工厂模式

  • 具体工厂类 - 生成(一组)具体产品
  • 实现多个产品族(相关联产品组成的家族),而工厂方法模式的单个产品,可以满足更多的生产需求

image.png

为什么用红黑树不用普通的二叉平衡树