Java春招提前批
1、介绍项目
2、如何实现的缓存,项目的瓶颈在哪(跟数据库的交互),如何解决,缓存除了放在redis中还有哪些方法?
3、了解使用过消息队列吗
4、乐观锁并发冲突在实际中怎么解决
5、bean的生命周期
6、hashmap put方法的实现流程(equals方法和hashcode方法重写的问题)
1、put(key, value)中直接调用了内部的putVal方法,并且先对key进行了hash操作;
2、putVal方法中,先检查HashMap数据结构中的索引数组表是否位空,如果是的话则进行一次resize操作;
3、以HashMap索引**数组表的长度减一与key的hash值进行与运算,得出在数组中的索引,如果索引指定的位置值为空,则新建一个k-v的新节点;
4、如果不满足的3的条件,则说明索引指定的数组位置的已经存在内容,这个时候称之碰撞出现;
5、在上面判断流程走完之后,计算HashMap全局的modCount值,以便对外部并发的迭代操作提供修改的Fail-fast判断提供依据,于此同时增加map中的记录数,并判断记录数是否触及容量扩充的阈值,触及则进行一轮resize操作;
6、在步骤4中出现碰撞情况时,从步骤7开始展开新一轮逻辑判断和处理;
7、判断key索引到的节点(暂且称作被碰撞节点)的hash、key是否和当前待插入节点(新节点)的一致,如果是一致的话,则先保存记录下该节点;如果新旧节点的内容不一致时,则再看被碰撞节点是否是树(TreeNode)类型,如果是树类型的话,则按照树的操作去追加新节点内容;如果被碰撞节点不是树类型,则说明当前发生的碰撞在链表中(此时链表尚未转为红黑树),此时进入一轮循环处理逻辑中;
8、循环中,先判断被碰撞节点的后继节点是否为空,为空则将新节点作为后继节点,作为后继节点之后并判断当前链表长度是否超过最大允许链表长度8,如果大于的话,需要进行一轮是否转树的操作;如果在一开始后继节点不为空,则先判断后继节点是否与新节点相同,相同的话就记录并跳出循环;如果两个条件判断都满足则继续循环,直至进入某一个条件判断然后跳出循环;
9、步骤8中转树的操作treeifyBin,如果map的索引表为空或者当前索引表长度还小于64(最大转红黑树的索引数组表长度)**,那么进行resize操作就行了;否则,如果被碰撞节点不为空,那么就顺着被碰撞节点这条树往后新增该新节点;
10、最后,回到那个被记住的被碰撞节点,如果它不为空,默认情况下,新节点的值将会替换被碰撞节点的值,同时返回被碰撞节点的值(V)。
7、redis持久化方法
8、除了redis还有哪些缓存
9、concurrenthashmap底层实现
10、一条sql语句执行慢怎么分析
11、mysql的优化手段、思路及工具
12、mysql选择索引的方式(走主键索引还是非主键索引)的判断方法:通过索引的区分度
13、jvm内存区域
14、堆分区中的各个区域,哪些对象会放在老年代区
15、介绍CMS垃圾收集器
16、线程状态的变化
17、volatile关键字
18、redis中有哪些数据结构,使用过哪些
19、synchronized关键字的底层实现
20、介绍锁及底层实现
21、threadlocal介绍以及底层实现
22、用过线程池嘛,线程池的原理和过程
23、AQS
24、http四次挥手
25、spring boot循环依赖怎么解决(A依赖B B依赖A)怎么创建对象
26、spring boot的整个流程
27、mysql索引B树和B+树区别
28、自己开发过程中遇过什么Java异常,自己怎么解决的
Java 中的常见集合:
- List
- ArrayList
- LinkedList
- ArrayList
- Set
- HashSet
- TreeSet
- HashSet
- Map
- HashMap
- TreeSet
HashMap 的底层数据结构:
数组、链表、红黑树(二叉查找树)红黑树的具体原理
红黑树在数据结构课上就没看明白,现在还是一头雾水,截止到现在还是研究中,如果有大佬能够指点,还请不吝赐教![抱拳]HashMap 的线程不安全问题
网上的一些博客我都没看明白(菜),这个我能看懂,画的图也很清晰,直接贴上连接,下面的分析就是自己照着葫芦画瓢,不准确的地方还请请指出 HashMap面试题,看这一篇就够了!)
- HashMap
数据覆盖问题
执行 put 操作时,可能会导致出现数据覆盖的问题:
JDK1.7 版本下,插入某个节点采用的是头插法。设有线程A 和线程B同时进行 put 操作,A 和 B 的 key 同时都指向同一个数组下标 table[i]。A先获取table[i]的头节点,将自己插入的节点作为新头节点准备插入时,时间片使用完,轮到B 执行插入完成。这时候再轮到A插入,就会覆盖B插入的节点,从而导致数据覆盖。
扩容导致死循环(基于 JDK1.7)
首先我们要知道在 JDK1.7 中 HashMap 的扩容过程是通过头插进行的。这个过程需要记住当前节点 e 和下一个节点 e.next。看完原博客内容就差不多能明白了,我只是重新以我的思路说一遍。
复制代码
void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; for (EntryK,V> e : table) { while(null != e) { EntryK,V> next = e.next; if (rehash) { e.hash = null == e.key ? 0 : hash(e.key); } int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } } } |
---|
对原博客的图进行解析:
- 扩容之前,线程B在遍历 table 时获取到一个节点 e(e = a),并且获取到下一个节点 next(next=b)。B的时间片使用完毕
- 这时A获取到时间片,完成扩容。这时他的节点链表变为 c->b->a->null
- B获得时间片,进行扩容。这时 B 的 e 引用依旧是 a ,同理 next 引用的是 b。这时我们执行 newTable[i] = e;这一句,则会直接指向a节点,而且b节点就是下一个 e 节点。
- 循环到下一个节点,e引用的为 b 节点,b节点的下一个节点还是a节点,newTable[i]= e将头节点设置为 b 节点。
- 又循环到 a 节点,这时将 a 节点作为头节点再次插入,a.next = b 。形成环形链表。
写到后面这个逻辑我觉得没问题,但是表述不清晰,看原博会更好点。
循环链表问题在 JDK1.8 得到解决,将头插法改进为尾插法,从而保证了安全性。
数据丢失
一些博客里会说 JDK1.8 中多线程HashMap有数据丢失,我的理解其实就是多线程下对链表的插入操作,多个线程同时获取到尾节点 tail,某个线程插入后没来得及将插入节点A改为尾节点,就被另一个线程获取到时间片。将节点B插入到尾节点tail,这时就出现了一个孤立的节点A。
加一个引申的问题:HashMap rehash 过程是什么样的?各个变量(capacity、size、threshold、loadFactor)意义,他们和扩容的关系?
创建线程的几种方式
- 继承Thread类创建线程类,重写 run 方法
- 通过 Runnable 接口创建线程类,重写 run 方法
- 通过 Callable 和 FutureTask 创建线程
- 采用线程池的方案,将线程重复利用,从而保证系统的效率,避免过多资源浪费在创建销毁线程上。
- 提高相应速度,请求或者任务到达可以直接相应处理
- 将任务提交和执行分离,降低耦合
- 提高线程的可管理性。使用线程池统一分配,调优,监控。
设计一个线程池的思路:
面试时候就问我让我讲一讲设计一个线程池的具体思路,我面试前几天还有好好的阅读了一些 ThreadPoolExecutor 的源码,但是一紧张全忘了。下面的思路是我自己认认真真思考,并参考了好多博客得到的思路,不一定保证正确,但是确实是我这次面经中我最花心思总结的。 顺便贴一些博客和书籍供参考: 《Java并发编程的艺术》 https://cloud.tencent.com/developer/article/1673042 https://dunwu.github.io/javacore/concurrent/Java%E7%BA%BF%E7%A8%8B%E6%B1%A0.html https://mp.weixin.qq.com/s/q0Qt-ha9ps12c15KMW7NfA https://www.jianshu.com/p/94852bd1a283
从一个线程池的生命周期进行思考:
后面的8、9、10 点 我都没有把握,我主要在线程池销毁线程的时机,线程空闲的处理上没有搞懂。
- 8 中的“空闲线程”是不是只指的第二次创建的线程;怎么样才算是“空闲”,是不是工作队列中的任务都执行完,才算呢?
- 10 中的所有任务执行完毕后,所有线程该何去何从
- 启动线程池时,我们需要预先创建并启动若干个线程以用来接收传入的任务。
- 创建的线程名称,优先级等待属性需要统一设置,我们可以通过一个特定或默认的工厂进行批量生产
- 启动线程后,我们还要对线程进行重复利用,那么需要容器来存取线程。
- 启动的线程数必须要有限制,不然无尽的线程数会使cpu频繁切换上下文,从而使cpu资源严重浪费,同时大量的线程也会占用大量的内存空间,导致 OOM
- 如果传入新任务,但所有线程都在执行任务中,无暇顾及传入的任务。需要将其缓存下来,等待任务完毕的某个线程接收该任务继续工作
- 继续传入新任务,导致超出缓存大小,或者缓存过大占用大量内存空间进而 OOM。那么可以增加若干个线程,加快处理任务的进度
- 如果还是不停的有任务加入,但是依照现在的资源状况,既不允许将任务缓存,也不能允许增加线程进行处理。就只好寻找一个策略来处理这些的任务。
- 任务逐渐处理完毕,不需要新创建的线程就能够应对了,可以将这些线程销毁来降低资源的消耗。不过考虑到万一销毁后马上又有大批新任务处理不过来,于是设置一个空闲等待的销毁时间,这段时间里这些线程还是空闲,则销毁,反之则继续运行。(这里我搞不清到什么时候就判定新创建线程是空闲线程)
- 如果运行过程中,需要将线程池停下来,要么所有线程马上停止,要么正在运行的线程运行完毕后停止
- 所有任务全部都处理完毕了,所有线程都处于空闲状态了,需要将所有线程封存起来,以尽力降低空闲线程的消耗。
根据上面的10点,就可以得知线程池中比较重要的一些属性和方法:
- prestartAllCoreThreads() 方法预热线程池,不断向线程池中加入新的worker线程,直到达到核心线程数 corePoolSize
- 线程工厂 threadFactory ,批量创建线程,定制线程的名称。
- 工作线程池 workers,使用 HashSet 用于存取 work 线程。
- 核心线程数 corePoolSize,维持线程最基本的线程数,注意尽量不要设置数值很大,如 Integer.MAX_VALUE。不然创建大量线程导致CPU和内存资源过于紧张浪费。
- 工作队列 workQueue,是一个阻塞队列,保存等待执行的任务。有很多类,但是注意尽量不要使用无界队列 (界限最大值为Integer.MAX_VALUE),任务入队过多会导致内存紧张进而 OOM,并且会使 maximumPoolSize失效
- 最大线程数 maximumPoolSize,用于在阻塞队列满了后,继续创建新线程执行任务。同理不得设置过大
- 拒绝策略、饱和策略 handler,在线程池和队列都满的情况下,需要采取一种策略处理新提交的任务。Java 提供的策略有:
- AbortPolicy :直接抛异常
- CallerRunPolicy:只用调用者所在的线程来运行任务
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
- DiscardPolicy:丢弃任务,但是不抛出异常。
- AbortPolicy :直接抛异常
- 空闲线程存活时间 keepAliveTime,在线程空闲后如果超过这个时间就将其销毁。我认为是销毁的新创建的线程。
- shutDown 和 shutDownNow 方法
依旧是 8 9 10点有些拿不准,就不写了。如果某位大佬对这个地方熟悉,请评论区指出!谢谢!
HTTP 的结构
HTTP消息 - HTTP | MDN (mozilla.org))
- 起始行:用于区分是响应报文还是请求报文。在请求报文就是请求行,在响应报文就是状态行
请求行 = 方法 + url + 版本
状态行 = 版本 + 状态码 + 短语 - 首部行:说明浏览器、服务器、或者报文主题的一些信息。首部行有好几行,每行都有换行和回车
- 数据体:包括请求携带的信息或者和服务器返回的HTML页面。
三次握手、四次挥手
具体过程具体过程把课本上的过程走一遍即可
为什么握手要三次,二次、四次不行吗?
主要是防止已经失效的链接请求报文段突然又传到了服务器,因为产生了错误。假设出现一种异常:A发出的第一个连接的请求报文段没有丢失,而是在网络中的某个结点滞留了,一直延迟到连接释放以后的某个时间才到达B,本来这是一个早已经失效的报文段,但是B接收到失效的连接请求后,就会误以为A又发送了一次新的连接请求,于是就向A发出确认报文段,同意建立了连接。如果没有三次握手,新的连接就建立了,之后会一直等A发送数据,资源就会被白白的浪费了。
四次挥手中服务器为何会出现 time-wait 现象?
- 为了保证A发送的最后一个ACK报文能够到达B.因为这个ACK报文可能会丢失,然后B收不到A发出的ACK报文段的确认。B会超时重传这个FIN+ACK的报文段。A在2MSL重新传一次确认,重新启动2MSL计时器,最后AB都进入CLOSED状态。
- 防止出现已失效的连接请求报文段。A在发送完最后一个ACK报文段后,在经过2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
设计模式-单例模式
我并没有完全了解Java的设计模式,遇到这问题就只答出了下面思路,但是后面的多线程安全问题是一点都不会
私有的构造方法 + 通过构造方法创建私有的静态成员变量 + 公有的静态方法返回成员变量
我这个总结就是按照这篇博客总结的,如果不明白可以看看
用单例模式来讲讲线程安全)
懒汉模式: 懒加载,需要时创建
复制代码
public class Singleton { private static Singleton singleton = null; private Singleton() { } public staic Singleton getSingleton() { if (singleton == null) { singleton = new Singleton(); } return singelton; } } |
---|
饿汉模式:类加载时就将实例创建在堆中,浪费内存
public class Singleton { private static Singleton singleton = new Singleton; private Singleton() { } public staic Singleton getSingleton() { return singelton; } } |
---|
单例模式的线程安全问题:
如上所示,如果使用饿汉模式,则没有线程安全问题,获取的是同一个 singleton。但是对于懒汉模式,则会出现安全问题。
例如线程A已经获取到锁,并进入 if 判断语句,如果没有创建对象,需要创建一个对象。但是此时时间片用尽。线程B运行。此时依旧是没有对象,进入 if 语句中,创建一个对象,并返回。时间片交给A,A仍需要创建一个新的对象,并返回。这样就导致出现两个实例,从而出现安全问题。
解决方案:
synchronized 同步方法:
通过对获取方法加锁,保证安全性。
public staic synchronized Singleton getSingleton() { if (singleton == null) { singleton = new Singleton(); } return singelton; } |
---|
但是这样的并发效率低,同一时间只允许一个线程访问。限制 cpu 资源,性能差。
双重同步锁单例模式
不在方法中添加 synchronized 关键字,而是在 if 判断为空时加锁,加锁后再次判断是否为空。通过双重判断和加锁既能够保证为懒汉模式,有能够保证在多线程同时判断实例为空时,声明变量的安全。
复制代码
public static Singleton getSingleton() { if (singleton == null) { synchronized(Singleton.class) { if (singelton == null) { singleton = new Singleton(); } } } return singleton; } |
---|
但是,在创建变量singleton = new Singleton();时,还是有线程安全问题的:
jvm 在执行该行代码时,会进行以下工作:
- memory = allocate(); 分配对象的内存空间
- ctorInstance(memory); 初始化对象
- singleton = memory; singleton 引用分配好的空间
对于上述操作,jvm有可能会对2 和 3 进行重排序,从而变成:
- memory = allocate(); 分配对象的内存空间
- singleton = memory; singleton 引用分配好的空间
- ctorInstance(memory); 初始化对象
这样重排后,执行到 2 就可以得到 singleton 不为空的结果,但其实singleton 还并没有进行初始化。假如线程A在进入第二个 if 后,重排序导致 singleton 引用到了未初始化的对象,恰好cpu被线程B抢占,在判断第一个 if 过程中就发现 singleton 不为空,直接返回了一个没有初始化的对象,从而导致线程安全问题的出现。
volatile + 双重检测机制
改进很简单,由于这是因为jvm的重排序导致的,那么我们只需要将重排序取消掉即可。那么将单例对象加上 volatile 关键字修饰即可。
具体代码不多展示了。
枚举模式-最安全
直接贴原帖子的代码:
复制代码
public class Singleton { // 私有构造函数 private Singleton() { } public static Singleton getInstance() { return SingletonEnum.INSTANCE.getInstance(); } private enum SingletonEnum { INSTANCE; private Singleton singleton; // JVM保证这个方法绝对只调用一次 SingletonEnum() { singleton = new Singleton(); } public Singleton getInstance() { return singleton; } } } |
---|
redis 数据结构
- SDS 以及和 C 的字符串比较
- 链表
- 字典 以及扩容、rehash
- 集合
- 有序集合
redis 根据场景设计:如果一个热点数据,被大量访问怎么办?
这个问题直接给我干碎了,本来就没怎么用过 redis,看过一本书,具体设计还是要靠经验了。。。我没有经验呀。
这个博客的链接我给忘记了,找到就给贴上
- 利用二级缓存:
利用 ehcache(不清楚不了解),或者一个 HashMap,将 key - value 直接存储到 jvm 堆内存中,访问时,直接通过查询 map 中的 key 取出,不需要走到 redis 层。 - 备份热 key:
设置集群,请求时,根据一定规则访问集群中某个的机器。
单点登录
单点登录(SSO)看这一篇就够了) 这篇下面的一些讨论挺有参考价值的 什么是单点登录(SSO))
聊聊我做的项目
这个就是看个人项目了
作者:Only”LLSH
链接:https://www.nowcoder.com/discuss/602692?source_id=discuss_experience_nctrack&channel=-1
来源:牛客网
实习
- 问之前的实习经历,在实习中遇到的挑战和困难,怎么解决的
- 聊到MySQL的分表分库,讨论一下
docker容器技术的原理(这里只是简单答了下namespace和cgroup,没有深入问)
Redis
简历里提到Redis,聊聊怎么使用的,为什么选择用Redis
- 切片集群和哨兵集群的区别
- 主从切换的过程,哨兵选举过程
-
Java
Java内存模型(JMM)
- Java内存区域有哪些,堆和栈的区别,什么情况会导致栈内存溢出
- 引出可见性问题,列举一个可见性导致线程不安全的场景
- 聊一聊Java垃圾回收(答了常见的垃圾回收算法,分代理论)
- 不可以分代可不可以,聊一聊看法
volatile关键字,既然volatile关键字可以解决可见性,为什么不所有变量都使用
MySQL
说一说平时MySQL查询的调优思路(回答explain查看查询日志,看是否命中索引)
- 聊一聊设计索引时需要注意些什么(回答索引的数据结构B+树,字段区别度要大,最左匹配原侧)
-
计网
说说网络分层
- TCP属于哪层协议,TCP如何保证的可靠性,为什么进行3次握手
- HTTP属于哪层协议,应用层应用到UDP传输的协议有哪些(回答DNS)
- 详细说说DNS工作过程(如何通过hostname查询对应IP)
作者:Only”LLSH
链接:https://www.nowcoder.com/discuss/602692?source_id=discuss_experience_nctrack&channel=-1
来源:牛客网
笔试(45min)
电话面结束后就是笔试,笔试地址直接发邮箱,用阿里自己的答题系统
题目1: 给定任意一个二叉树,计算这个树的最大深度。(递归和非递归实现)
题目2: 一台火星探索车一个地面指挥台,面向对象设计考虑 地面指挥台向火星探索车发送 移动,拍照等多个指令。
要求1、面向对象建模,
要求2、用命令模式
要求3、指令状态并发控制或者同步交互考虑额外加分。
- 项目经历
- 项目是否是实际使用的?会考虑使用吗?
- 这个项目完成的程度如何?
- 有部署这个项目吗?是在本地部署的还是服务器部署
- 项目里使用的Redis解决了什么问题?
- 讲一下项目中用到的消息中间件kafka
- 我在回答第5题的时候说,服务器会在空闲的时候消费队列,于是面试官追问怎么判断服务器是处于空闲的(于是我回答说空闲的说法有误,应该是在CPU没那么忙的情况下消费队列)
- 接着面试官追问怎么判断CPU什么时候快,什么时候慢
- 怎么衡量服务器的性能
- Java基础知识
- HashMap是怎么实现的
- 为什么要用红黑树,作为HashMap的实现
- 为什么要用红黑树,不用AVL树
- 什么情况下使用红黑树,什么情况下使用AVL树,怎么权衡,为什么JDK的开发者最终放弃了AVL树而选择了红黑树、
- 数据库的索引使用了什么数据结构
- 为什么用B+树而不用红黑树
- 你知道什么排序算法,简单说一下
- 快排的时间复杂度,堆排序的时间复杂度
- 简单讲一下快排是如何实现的
- 有了解JVM的GC吗
- 有了解JVM的标记清除算法吗
- 在线编程题
// 要求:
// 1. 排序(升序)三个链表并分别打印结果
// 2. 合并三个升序链表,并打印合并后的降序结果
// 3. 写一个函数判断上面的单链表是否有环
一面(60min)
常规问题
自我介绍
基础知识
(JVM)java垃圾回收说一下
(JVM)使用jvm的类加载器,和自己定义的类加载加载一个类,会产生几个对象?
(JVM)java会存在内存泄露吗?为什么?举例?
(Docker)常用的docker命令说一下?
(Java)介绍下hashmap
(Java)hash冲突怎么办
(Java)hash冲突,hashmap是怎么put的
(Java)== 和equals的关系
(Java)为什么hashmap的put要用equals呢
(Java)hashmap为什么用红黑树
项目
(阿里实习项目)你项目中的数据是用OSS存储的,如果必须用Mysql,怎么降低存储?
(金山云实习项目)说说你的跨链项目是怎么实现的?
二面(60min)
项目
(阿里实习项目)说说你在阿里实习的这个项目的整个过程?
(阿里实习项目)系统是怎么设计的,有哪些优缺点?
(阿里实习项目)该哪一块是你负责的?
基础知识
(Java)线程安全怎么解决
(Java)简单介绍一下你知道的锁?你提到了自旋锁,操作系统是怎么实现的?
(Java)java中的锁是怎么实现的
操作系统
进程和线程的区别
进程间通信方式
进程线程和协程
不同机器进程间通信,答socket,问socket的优点
cpu的调度算法
线程的可能状态
什么情况下线程会处在阻塞,答阻塞,问还有吗?
计算机网络
三次握手说一下
为什么要随机初始化seq
除了seq还有什么
拥塞控制和流量控制的区别
拥塞控制是怎么实现的,说下快恢复
说下https
算法
相交链表
思维题
火车运煤问题?你是山西的一个煤老板,你在矿区开采了有3000吨煤需要运送到市场上去卖,从你的矿区到市场有1000公里,你手里有一列烧煤的火车,这个火车最多只能装1000吨煤,且其能耗比较大——每一公里需要耗一吨煤。请问,作为一个懂编程的煤老板的你,你会怎么运送才能运最多的煤到集市?
结束语
蚂蚁金服金融云
项目
Java基本类型
8种基本类型, byte, short, int, long, float, double, boolean, char
==和equals的区别
==,对于基本类型,比较的是值,对于引用类型比较的是引用
深克隆和浅克隆
深拷贝是对object的每个属性深拷贝,而浅拷贝只是复制引用。
了解反射吗
反射是相对正射(直接new)而言的, 在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
常用API
在 Java API 中,获取 Class 类对象有三种方法:
第一种,使用 Class.forName 静态方法。当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
Class clz = Class.forName(“java.lang.String”);
第二种,使用 .class 方法。
这种方法只适合在编译前就知道操作的 Class。
Class clz = String.class;
第三种,使用类对象的 getClass() 方法。
String str = new String(“Hello”);
Class clz = str.getClass();
jvm垃圾回收
- 引用计数
- 优点就是快,适合实时环境
- 缺点是循环引用
- 优点就是快,适合实时环境
可达性分析
- 从GC ROOT开始遍历
- 可作为GC ROOT的对象:
- 虚拟机栈中引用的对象(栈帧中的本地变量表);
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中JNI(Native方法)引用的对象。
- 虚拟机栈中引用的对象(栈帧中的本地变量表);
- 需要经过两次标记才会清除,第一次是标记不可达对象,第二次标记,会去看看finalize()方法,如果在方法内仍然没有建立引用,才会被回收。
- 从GC ROOT开始遍历
- 四种引用
- 强引用, 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 软引用,描述有用但不是必须的对象。 在抛出内存溢出异常之前,会先尝试回收软引用对象。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
- 弱引用,垃圾收集一旦发生,一定会回收弱引用
- 虚引用, 无法通过虚引用来取得一个对象实例。它的作用是能在这个对象被收集器回收时收到一个系统通知。
- 强引用, 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
- 方法区回收
- 废弃常量,可达性算法
- 无用的类
- 该类所有的实例都已经被回收
- 加载该类的ClassLoader已经被回收
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
- 该类所有的实例都已经被回收
- 废弃常量,可达性算法
回收算法
- 标记-清除算法,不需要移动对象,高效,有内存碎片
- 复制算法
- 标记-整理
- 分代收集
- 年轻代
- a) 所有新生成的对象首先都是放在年轻代的eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
- b) 新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
- c) 当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
- d) 新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
- a) 所有新生成的对象首先都是放在年轻代的eden区。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
- 老年代
- 1) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
- 2) 内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
- 1) 在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
- 年轻代
Java中StringBuilder和StringBuffer的区别
StringBuilder是非线程安全的,性能更高,StringBuffer是线程安全的。
Java程序为什么刚开始很慢,之后越来越快?
答: 由于JVM的混合执行机制,在程序运行的时间够久以后,频繁调用的方法都被编译成了本地机器码,调用效率自然就变高了。
但是需要注意的是,应用占用的内存也会升高。
jvm的执行策略
现在大多数开发环境中都是用是HotSpot虚拟机,而HotSpot虚拟机的默认执行策略是混合式模式的。也就是解释执行和编译执行一起工作。
其中mixed mode就代表的混合执行策略。
JVM解释执行
虚拟机将编译好的字节码一行一行地翻译为对应平台的机器码执行顺序执行。代码没有优化,需要花费额外的解释时间,执行速度较慢
JVM编译执行
虚拟机以方法为单位,将所属字节码一次性翻译为机器码后执行,代码经过虚拟机优化,执行速度块
JIT即时编译器
JIT: Just-In-Time Compiler
Hot-Spot虚拟机采用即时编译器,会将字节码中热点代码编译成本地机器码,提高执行速度
热点代码
包括两种:
- 频繁调用的方法
- 多次执行的循环体
热点代码检测机制
- 采样:定期检查所有线程jvm栈的栈顶方法
- 计数器:每个方法都持有一个计数器,调用一次则累加一次
- 基于踪迹热点探测的检测机制(Dalvik虚拟机)
内存调优
1、缓存
缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。
2、排行榜
很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
3、计数器
什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
4、分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
5、分布式锁
在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
6、 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。
7、最新列表
Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
8、消息系统
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
mysql索引
#B+树
插入 删除 平衡
#volatile
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
实现机制:
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
保证线程安全的方法
乐观锁悲观锁
线程池的创建方法
spring的bean注入方式
- 属性注入方法, 要求bean提供一个默认的无参构造函数,并且得为需要注入的属性提供set方法。
- 构造函数注入方法
- 按类型匹配入参
- 按索引匹配入参
- 联合使用类型和索引匹配入参
- 通过自身类型反射匹配入参
- 按类型匹配入参
- 工厂方法注入方法
- 静态工厂
- 非静态工厂
- 静态工厂
构造函数注入理由:
- 构造函数保证重要属性预先设置
- 无需提供每个属性Setter方法,减少类的方法个数
- 可更好的封装类变量,避免外部错误调用
属性注入理由:
- 属性过多时,构造函数变的臃肿可怕
- 构造函数注入灵活性不强,有时需要为属性注入null值
- 多个构造函数时,配置上产生歧义,复杂度升高
- 构造函数不利于类的继承和扩展
- 构造函数注入会引起循环依赖的问题
@autowired和@resource的区别
1、@Autowired注解是按类型装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它required属性为false。
2、@Resource注解和@Autowired一样,也可以标注在字段或属性的setter方法上,但它默认按名称装配。
3、@Resources按名字,是JDK的,@Autowired按类型,是Spring的。
dockerFile内容
#基础镜像,如果本地没有,会从远程仓库拉取。
FROM openjdk:8-jdk-alpine
#镜像的制作人
MAINTAINER xzxiaoshan/365384722@qq.com
#工作目录
WORKDIR /app/
#在容器中创建挂载点,可以多个
VOLUME [“/tmp”]
#声明了容器应该打开的端口并没有实际上将它打开
EXPOSE 8080
#定义参数
ARG JAR_FILE
#拷贝本地文件到镜像中
COPY ${JAR_FILE} app.jar
#指定容器启动时要执行的命令,但如果存在CMD指令,CMD中的参数会被附加到ENTRYPOINT指令的后面
ENTRYPOINT [“java”,”-Djava.security.egd=file:/dev/./urandom”,”-jar”,”app.jar”]
ENTRYPOINT和CMD的区别
8个球,一个是轻的,用天秤最少几次可以找出来
两个4,两个10,使用加减乘除得到24
(10*10-4)/4=24
约瑟夫环
钉钉:
问本科、专业;
介绍项目;
项目难点;
化工前端用的什么;
项目是否投入使用;
访问量大吗;
数据量扩充100倍,会遇到什么问题;
提高查询效率方法;
ORM内存;
Innodb和MyISAM;
1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
项目中遇到的技术难点(解决思路);
redis的数据一致性问题;
如果redis存的数据上百g怎么办;
开源框架研究过吗,看过源代码吗;
了解过哪些框架的原理;
做项目有意思吗;
项目实现时有没有把结构更优雅更好一些;
阿里的代码手册有哪些疑问;
阿里的代码手册有没有印象深刻的;
成绩排名;
奖项中哪个更重要;
浏览器输入www.baidu.com整个流程;
JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区(需要回收)
浮点数存储 IEEE754
对于 32 位的浮点数,最高的 1 位是符号位 s,接着的 8 位是指数 E,剩下的 23 位为有效数字 M。
V=(-1)^s(1+M)2^(E-127)(单精度)
为什么计算机存的是补码?
因为用补码存将 A-B 转换成 A+B的补码,因为计算机自动取模的特性,求出A-B
ThreadLocal内存泄漏
ThreadLocalMap中的Entry的Key是弱引用,当Key被回收以后变成null,剩下value。
为了避免内存泄露,需要在用完ThreadLocal以后执行 getEntry、set、remove、rehash等方法,这些方法会主动清理key为null的entry。
为什么使用弱引用?
官方回答: 为了应对非常大和长时间的用途,哈希表使用弱引用的 key。
下面我们分两种情况讨论:
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
#红包算法
二倍均值法: 每次抢到的金额 = 随机区间 [0.01,m /n × 2 - 0.01]元
Spring如何解决循环依赖 #循环依赖
在初始化A的时候,发现A依赖B,就马上先创建一个B,然后将B设置到A中,在发现B也依赖A,然后将A注入到B。
Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。
构造器不能循环依赖,字段/setter注入可以。
#Bean的生命周期
首先Spring在实例化Bean的时候,会先调用它的构造函数,进行Bean的实例化,然后进行Bean的初始化,Bean的初始化经过三个阶段初始化之前(applyBeanPostProcessorsBeforeInitialization),其次是进行初始化(invokeInitMethods),最后是初始化之后(postProcessAfterInitialization),这就是Bean的初始化过程;然后就开始利用Bean进行业务逻辑处理,最后容器正常关闭,Spring开始销毁Bean,Bean的销毁过程相对比较简单,调用DisposableBeanAdapter.destroy()方法,该方法中有三个地方比较重要,分别触发Bean的生命周期方法,它们是:processor.postProcessBeforeDestruction(this.bean, this.beanName);
((DisposableBean) bean).destroy();
invokeCustomDestroyMethod(this.destroyMethod);
SpringBoot自动装配过程
springboot启动时,是依靠启动类的main方法来进行启动的,而main方法中执行的是SpringApplication.run()方法,而SpringApplication.run()方法中会创建spring的容器,并且刷新容器。而在刷新容器的时候就会去解析启动类,然后就会去解析启动类上的@SpringBootApplication注解,而这个注解是个复合注解,这个注解中有一个@EnableAutoConfiguration注解,这个注解就是开启自动配置,这个注解中又有@Import注解引入了一个AutoConfigurationImportSelector这个类,这个类会进过一些核心方法,然后去扫描我们所有jar包下的META-INF下的spring.factories文件,而从这个配置文件中取找key为EnableAutoConfiguration类的全路径的值下面的所有配置都加载,这些配置里面都是有条件注解的,然后这些条件注解会根据你当前的项目依赖的jar包以及是否配置了符合这些条件注解的配置来进行装载的。
精简:
SpringBoot在启动的时候会调用run()方法,run()方法会刷新容器,刷新容器的时候,会扫描classpath下面的的包中META-INF/spring.factories文件,在这个文件中记录了好多的自动配置类,在刷新容器的时候会将这些自动配置类加载到容器中,然后在根据这些配置类中的条件注解,来判断是否将这些配置类在容器中进行实例化,这些条件主要是判断项目是否有相关jar包或是否引入了相关的bean。这样springboot就帮助我们完成了自动装配。
如何解决缓存不一致问题?
- 通过在总线加LOCK#锁的方式。 因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。
- 通过缓存一致性协议。 MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
并发编程三个问题:
- 原子性。 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 可见性。 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
- 有序性。 程序执行的顺序按照代码的先后顺序执行
Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
下面就来具体介绍下happens-before原则(先行发生原则):
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
一面
业务面(50分钟左右)
按顺序问了 操作系统,计算机网络,golang,mysql,docker,算法
操作系统
进程和线程的区别
- 进程是资源分配的最小单位,线程是程序执行的最小单位。
- 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
- 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
- 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
进程间通信 和 线程间通信
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号、信号量、共享内存(最快的)、Socket、Streams等。
线程间通信,锁机制,信号量,信号机制,共享内存,wait/notify
虚拟内存,共享内存,常驻内存
虚拟内存: 它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
共享内存: shmget函数,共享内存就是使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
常驻内存: 实际使用的物理内存,注意不包括共享内存和swap。常驻内存一般会换算成占系统总内存的百分比,也就是进程的内存使用率。
swap内存: Swap 换出到磁盘的内存
缺页异常:
- 直接从物理内存中分配时,被称为次缺页异常
- 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常
软连接和硬连接
原理上,硬链接和源文件的inode节点号相同,两者互为硬链接。软连接和源文件的inode节点号不同,进而指向的block也不同,软连接block中存放了源文件的路径名。 实际上,硬链接和源文件是同一份文件,而软连接是独立的文件,类似于快捷方式,存储着源文件的位置信息便于指向。 使用限制上,不能对目录创建硬链接,不能对不同文件系统创建硬链接,不能对不存在的文件创建硬链接;可以对目录创建软连接,可以跨文件系统创建软连接,可以对不存在的文件创建软连接。
文件和目录的区别
普通文件:存储普通数据,一般就是字符串。
目录文件:存储了一张表,该表就是该目录文件下,所有文件名和inode的映射关系。
计算机网络
- TCP 握手为什么是三次
- TCP 和 UDP 的区别
- 让你基于 UDP 再设计一个可靠的协议,如何设计?
- 了解过 QUIC 吗
- http 和 https 的区别
- http 和 https 的应用场景
- https 最耗时的环节
mysql
- innodb 和 myisam 的区别
- 主键和索引的联系和区别
docker
- docker 和 虚拟机 的区别
算法
- 数组平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序排序。
输入:nums = [-4,-1,0,3,10] 输出:[0,1,9,16,100] 解释:平方后,数组变为 [16,1,0,9,100] 排序后,数组变为 [0,1,9,16,100] - 无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
输入: s = “abcabcbb” 输出: 3 解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
输入: s = “bbbbb” 输出: 1 解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
二面
leader面(55分钟)
主要是问了 项目,基础,学习生活 等
- 介绍一下最有挑战的项目
- 研究的方向
- 实习时长
- 软链接和硬链接
- mysql 索引 B+树
- 索引优化
- explain 执行计划
- extra的 using indexing 和 using where
- 覆盖索引
- 聚簇索引和辅助索引
- redis 了解吗
- nosql 说下
- golang gc
- ipv4 和 ipv6
- tcp 的阻塞控制
- 拥塞避免后再次慢开始还会从1开始吗
- 为什么 tcp 需要拥塞控制
- 学习生活
- 算法:和可被k整除的子数组(这里用了35min,要了若干次提示QAQ)
给定一个整数数组 A,返回其中元素之和可被 K 整除的(连续、非空)子数组的数目,时间要求O(n)。 输入:A = [4,5,0,-2,-3,1], K = 5 输出:7 解释:有 7 个子数组满足其元素之和可被 K = 5 整除:[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3] 提示:1 <= A.length <= 30000 -10000 <= A[i] <= 10000 2 <= K <= 10000
三面
三面-HR面(55分钟)
- 聊天,为何转服务端,为何自学go
- go 的优势
- gmp 及该模型好处
- 聊聊最满意的项目,亮点,难度
- 进程和线程区别
- 多进程和多线程注意要点和适用场景
- 多进程通信为何比多线程通信麻烦
- 地址空间是什么
- 虚拟地址和物理地址的区别和关系
- 堆区和栈区的区别
- 垃圾回收 相比程序员直接 free 和 delete 之类的,有什么优化(内存碎片)
- cookie 和 session 的区别
- session 一般存在哪里
- sessionId 如何维护(cookie 里带有 sessionId)
- xss 和 csrf
- 数据库查数据慢,怎么解决?
- sql 优化
- 哈希索引 和 B+树索引 的区别
- 哈希索引在内存还是在硬盘
- 哈希算法
- 哈希表实现
- 除了 go 还有学别的吗
- 技术方面最熟悉的
- Linux 了解吗
- shell 编程会吗(不太会 - -||
- 如何看 Linux 程序是多进程还是多线程的(ps PPID, PID?
- 算法:合并 n 个 m 长的数组,分析时间复杂度
- 反问,问了挺多,面试官挺耐心
数据雪崩
定时刷新清理缓存中的key时,Redis缓存中大面积key失效,从而导致大量请求直接访问了数据库,导致数据库崩盘。
设置失效时间。
热点数据均匀分布。
取消设置热点数据有一个失效时间,用更新缓存代替。
数据穿透
缓存穿透:缓存和数据库中都没有的数据,而用户(黑客)不断发起请求。
1)增加校验。比如用户鉴权,参数做校验,不合法的校验直接 return,比如 id 做基础校验,id<=0 直接拦截;
2)布隆过滤器。Redis 里还有一个高级用法布隆过滤器(Bloom Filter)这个也能很好的预防缓存穿透的发生。
数据击穿
缓存击穿是指一个 Key 非常热点,在不停地扛着大量的请求,大并发集中对这一个点进行访问,当这个 Key 在失效的瞬间,持续的大并发直接落到了数据库上,就在这个 Key 的点上击穿了缓存。
1)设置热点数据永不过期;
2)或者加上互斥锁就搞定了
void mmap{
void addr; //映射区首地址,传NULL
size_t length; //映射区的大小
//会自动调为4k的整数倍
//不能为0
//一般文件多大,length就指定多大
int prot; //映射区权限
//PROT_READ 映射区比必须要有读权限
//PROT_WRITE
//PROT_READ | PROT_WRITE
int flags; //标志位参数
//MAP_SHARED 修改了内存数据会同步到磁盘
//MAP_PRIVATE 修改了内存数据不会同步到磁盘
int fd; //要映射的文件对应的fd
off_t offset; //映射文件的偏移量,从文件的哪里开始操作
//映射的时候文件指针的偏移量
//必须是4k的整数倍
//一般设置为0
}
腾讯面试,55分钟
自我介绍,项目
讲一下Dubbo的理解
四个事务隔离级别
hashmap的内部存储以及插入和删除的过程
MySQL索引,以及有哪些索引
MySQL索引除了B+树还有什么
Java线程状态
死锁的必要条件
平时有用到单元测试吗,用过Mock没
算法题,给你一个只包含 ‘(‘ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
锁升级
无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
流程:
阿里巴巴供应链平台:
1.介绍项目,项目难点;
2.B+树,索引;
3.研究方向并介绍,研究方向的最前沿;
4.参加过竞赛没有?
5.介绍下本科的经历吧;
6.问什么当时觉得一个人完不成一个系统?
7.为什么当时不能采用先完成核心再逐步完善的方案?
8.软件架构考虑过嘛?(我开始扯化工、扯到redis、扯到hashmap、扯到红黑树和B+树)
9.红黑树和B+树区别;
10.redis数据量过大怎么办;
11.redis缓存击穿;
12.问区块链当前进展和我的研究;
13.(不是问题)总结了一通我的优势和劣势;
@SpringBootApplication注解
- @SpringBootConfiguration, 标注当前类是配置类,并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中。
- @EnableAutoConfiguration。 @Import注解用于导入配置类,我们看下导入类EnableAutoConfigurationImportSelector。容器刷新时,会调用AutoConfigurationImportSelector类的selectImports方法,扫描META-INF/spring.factories文件自动配置类(key为EnableAutoConfiguration),然后Spring容器处理配置类。
- @ComponentScan注解
SpringBoot 为什么可以用jar包直接运行?
spring-boot-maven-plugin 插件
打包以后 它的Main-Class是org.springframework.boot.loader.JarLauncher,当我们使用java -jar执行jar包的时候会调用JarLauncher的main方法,而不是我们编写的SpringApplication。
类加载器
- Bootstrap
- ExtClassLoader
- AppClassLoader
SpringMVC执行流程:
1.用户发送请求至前端控制器DispatcherServlet
2.DispatcherServlet收到请求调用处理器映射器HandlerMapping。
3.处理器映射器根据请求url找到具体的处理器,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
4.DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作
5.执行处理器Handler(Controller,也叫页面控制器)。
6.Handler执行完成返回ModelAndView
7.HandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet
8.DispatcherServlet将ModelAndView传给ViewReslover视图解析器
9.ViewReslover解析后返回具体View
10.DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
11.DispatcherServlet响应用户。
布隆过滤器场景:
- 网页爬虫对 URL 去重,避免爬取相同的 URL 地址;
- 反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱;
- Google Chrome 使用布隆过滤器识别恶意 URL;
- Medium 使用布隆过滤器避免推荐给用户已经读过的文章;
- Google BigTable,Apache HBbase 和 Apache Cassandra 使用布隆过滤器减少对不存在的行和列的查找。
SpringBoot启动流程
- 从spring.factories配置文件中加载EventPublishingRunListener对象,该对象拥有SimpleApplicationEventMulticaster属性,即在SpringBoot启动过程的不同阶段用来发射内置的生命周期事件;
- 准备环境变量,包括系统变量,环境变量,命令行参数,默认变量,servlet相关配置变量,随机值以及配置文件(比如application.properties)等;
- 控制台打印SpringBoot的bannner标志;
- 根据不同类型环境创建不同类型的applicationcontext容器,因为这里是servlet环境,所以创建的是AnnotationConfigServletWebServerApplicationContext容器对象;
- 从spring.factories配置文件中加载FailureAnalyzers对象,用来报告SpringBoot启动过程中的异常;
- 为刚创建的容器对象做一些初始化工作,准备一些容器属性值等,对ApplicationContext应用一些相关的后置处理和调用各个ApplicationContextInitializer的初始化方法来执行一些初始化逻辑等;
- 刷新容器,这一步至关重要。比如调用bean factory的后置处理器,注册BeanPostProcessor后置处理器,初始化事件广播器且广播事件,初始化剩下的单例bean和SpringBoot创建内嵌的Tomcat服务器等等重要且复杂的逻辑都在这里实现,主要步骤可见代码的注释,关于这里的逻辑会在以后的spring源码分析专题详细分析;
- 执行刷新容器后的后置处理逻辑,注意这里为空方法;
- 调用ApplicationRunner和CommandLineRunner的run方法,我们实现这两个接口可以在spring容器启动后需要的一些东西比如加载一些业务数据等;
- 报告启动异常,即若启动过程中抛出异常,此时用FailureAnalyzers来报告异常;
- 最终返回容器对象,这里调用方法没有声明对象来接收。
Spring Boot、Spring MVC 和 Spring 有什么区别?
Spring 框架就像一个家族,有众多衍生产品例如 boot、security、jpa等等。但他们的基础都是Spring 的ioc和 aop,ioc 提供了依赖注入的容器, aop解决了面向横切面的编程,然后在此两者的基础上实现了其他延伸产品的高级功能。
Spring MVC提供了一种轻度耦合的方式来开发web应用。它是Spring的一个模块,是一个web框架。通过Dispatcher Servlet, ModelAndView 和 View Resolver,开发web应用变得很容易。解决的问题领域是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等。
Spring Boot实现了自动配置,降低了项目搭建的复杂度。它主要是为了解决使用Spring框架需要进行大量的配置太麻烦的问题,所以它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),Spring Boot应用中这些第三方库几乎可以零配置的开箱即用(out-of-the-box)。
https认证流程
1)客户端发起一个http请求,连接到服务器的443端口。
2)服务端把自己的信息以数字证书的形式返回给客户端(证书内容有密钥公钥,网站地址,证书颁发机构,失效日期等)。证书中有一个公钥来加密信息,私钥由服务器持有。
3)验证证书的合法性
客户端收到服务器的响应后会先验证证书的合法性(证书中包含的地址与正在访问的地址是否一致,证书是否过期)。
4)生成随机密码(RSA签名)
如果验证通过,或用户接受了不受信任的证书,浏览器就会生成一个随机的对称密钥(session key)并用公钥加密,让服务端用私钥解密,解密后就用这个对称密钥进行传输了,并且能够说明服务端确实是私钥的持有者。
5)生成对称加密算法
验证完服务端身份后,客户端生成一个对称加密的算法和对应密钥,以公钥加密之后发送给服务端。此时被黑客截获也没用,因为只有服务端的私钥才可以对其进行解密。之后客户端与服务端可以用这个对称加密算法来加密和解密通信内容了。
SSL四次握手的过程
1、客户端请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA公钥加密,此时是明文传输。
2、服务端回复一种客户端支持的加密方法、一个随机数–Server random、授信的服务器证书和非对称加密的公钥。
3、客户端收到服务端的回复后利用服务端的公钥,加上新的随机数–Premaster secret 通过服务端下发的公钥及加密方法进行加密,发送给服务器。
4、服务端收到客户端的回复,利用已知的加解密方式进行解密,同时利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key – session key。
nginx负载均衡 健康检查
在实际应用当中:
1)如果后端应用是能够快速重启的应用,比如nginx的话,自带的模块是可以满足需求的。
但是需要注意,如果后端有不健康节点,负载均衡器依然会先把该请求转发给该不健康节点,然后再转发给别的节点,这样就会浪费一次转发。
2)如果当后端应用重启时,重启操作需要很久才能完成的时候就会有可能拖死整个负载均衡器。
此时,由于无法准确判断节点健康状态,导致请求handle住,出现假死状态,最终整个负载均衡器上的所有节点都无法正常响应请求。
G1也能并发进行垃圾回收,与CMS相比,其优点如下:
G1在GC过程中会进行整理内存,不会产生很多内存碎片
G1的STW更可控,可以指定可期望的GC停顿时间
.class和getClass()的区别
它们二者都可以获取一个唯一的java.lang.Class对象,但是区别在于:
1、.class用于类名,getClass()是一个final native的方法,因此用于类实例
2、.class在编译期间就确定了一个类的java.lang.Class对象,但是getClass()方法在运行期间确定一个类实例的java.lang.Class对象
双亲委派模型的工作过程如下:
(1)当前类加载器从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
(2)如果没有找到,就去委托父类加载器去加载(如代码c = parent.loadClass(name, false)所示)。父类加载器也会采用同样的策略,查看自己已经加载过的类中是否包含这个类,有就返回,没有就委托父类的父类去加载,一直到启动类加载器。因为如果父加载器为空了,就代表使用启动类加载器作为父加载器去加载。
(3)如果启动类加载器加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),则会抛出一个异常ClassNotFoundException,然后再调用当前加载器的findClass()方法进行加载。
双亲委派模型的好处:
(1)主要是为了安全性,避免用户自己编写的类动态替换 Java的一些核心类,比如 String。
(2)同时也避免了类的重复加载,因为 JVM中区分不同类,不仅仅是根据类名,相同的 class文件被不同的 ClassLoader加载就是不同的两个类。
自定义一个类加载器,需要继承ClassLoader类,并实现findClass方法。其中defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class(只要二进制字节流的内容符合Class文件规范)。
什么情况下需要自定义类加载器?
(1)隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如,Spring框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。(jar包之间的冲突的消除)。
(2)修改类加载方式:类的加载模型并非强制,除Bootstrap外,其它的加载并非一定要引入,或者根据实际情况在某个时间点进行按需动态加载。
(3)扩展加载源:比如从数据库、网络,甚至是电视机机顶盒进行加载。
(4)防止源码泄露。java代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。
正向代理与反向代理的区别:
正向代理: 客户端必须设置正向代理服务器,当然前提是要知道正向代理服务器的IP地址,还有代理程序的端口。
用途:
- 访问原来无法访问的资源,如google
- 可以做缓存,加速访问资源
- 对客户端访问授权,上网进行认证
- 代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息
反向代理: 反向代理对外都是透明的,访问者者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。
作用:
- 保证内网的安全,可以使用反向代理提供WAF功能,阻止web攻击
- 负载均衡,通过反向代理服务器来优化网站的负载
nginx通过proxy_pass(端口转发)配置代理站点,upstream实现负载均衡。
nginx负载均衡策略
- 轮询。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,会自动剔除;
- 指定权重。指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况;
- ip_hash(客户端绑定)。每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端的服务器,可以解决session问题;
- least_conn (最少连接)。下一个请求将被分配到活动连接数量最少的服务器;
- fair (第三方)。按后端服务器的响应时间来请求分配,响应时间短的优先分配;
- url_hash (第三方)。按访问url的hash结果来分配请求,按每个url定向到同一个后端服务器,后端服务器为缓存时比较有效;
看过的书籍
- Redis深度历险
- MySQL技术内幕
- 深入理解Java虚拟机
- Java并发编程实战
- 图解HTTP
- 算法
- 图解TCP/IP
SQL执行过程:
当执行一条查询的SQl的时候大概发生了一下的步骤:
- 客户端发送查询语句给服务器。
- 服务器首先检查缓存中是否存在该查询,若存在,返回缓存中存在的结果。若是不存在就进行下一步。
- 服务器进行SQl的解析、语法检测和预处理,再由优化器生成对应的执行计划。
- Mysql的执行器根据优化器生成的执行计划执行,调用存储引擎的接口进行查询。
- 服务器将查询的结果返回客户端。
管道和有名管道的区别:
管道一般用于父子进程,有名管道可以在无亲缘关系的进程间使用