首页 > 拿到了想去公司的意向书,分享一下自己的秋招总结
头像
peonyX
编辑于 2021-08-29 17:10
+ 关注

拿到了想去公司的意向书,分享一下自己的秋招总结

小c的秋招笔记

本人准备躺平了,分享一下秋招的笔记。这里只是记录了我过程中不熟悉,不会的点,所以可能没有那么基础。

感悟

面试的时候,对于一些框架,kafka,es,mongodb这些我都没用过,这些就会筛选掉我;然后有一些实际业务问题我答得不好;还有就是spring,一旦细问,我就不会;最后就是学历不行。我属于每个方面都会70%,但是有些面试官就喜欢盯着一个方面考你90%,其他的都不问,那我就没招。所以很多中厂我都过不去。大家尽量复习的全面一些吧。

写在前面

这里面一部分是我总结别人面经,还有一部分是自己面试不会的问题。
里面一些问题贴的是链接,更希望大家去看原文章并理解意思,并变成自己的话,我只是简要概括了一下。
我的面试还有很多,所以大家追更的面经不会完结。

文章大概是,操作系统,网络,mysql,redis,jvm,Java语言基础,spring这个顺序。不知道怎么归类的会放到最后的“其他”里面。
一定注意,这里面记录的是我不会的,所以有很多基础知识我这上面没有罗列出来,大家不要光看着我的总结复习

操作系统
网络
Mysql
Redis
JVM
Java
Springboot
其他

算法题

常考题就去看面经总结,lc我刷了六百多道,面试还是会有不会的,算法做不出来肯定寄寄。
我的leetcode:https://leetcode-cn.com/u/kamenrider/

操作系统

死锁

四个条件,预防检测,都必须理解。

死锁四个条件

互斥(每个资源只能被一个进程占用)。不可抢占(进程已获得的资源,在末使用完之前,不能强行剥夺)。占有且等待(一个进程因请求资源而阻塞时,对已获得的资源保持不放)。循环等待(若干进程之间形成环状的等待)。

死锁预防:破坏四个条件其中一个。

  1. 破坏互斥:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。
    只读数据文件、磁盘等软硬件资源均可采用这种办法管理;
    但是许多资源是独占性资源,如可写文件、键盘等只能互斥的占有;
    所以这种做法在许多场合是不适用的。
  2. 破坏占有且等待条件:
    采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。
    实现简单,但是严重的减低了资源利用率。
    因为在每个进程占有的资源中,有些资源在运行后期使用,有些资源在例外情况下才被使用,可能会造成进程占有一些几乎用不到的资源,而使其他想使用这些资源的进程等待。
  3. 剥夺调度能够防止死锁,但是只适用于内存和处理器资源。
    方法一:占有资源的进程若要申请新资源,必须主动释放已占有资源,若需要此资源,应该向系统重新申请。
    方法二:资源分配管理程序为进程分配新资源时,若有则分配;否则将剥夺此进程已占有的全部资源,并让进程进入等待资源状态,资源充足后再唤醒它重新申请所有所需资源。
  4. 破坏循环等待:
    给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
    采用层次分配策略,将系统中所有的资源排列到不同层次中
    一个进程得到某层的一个资源后,只能申请较高一层的资源
    当进程释放某层的一个资源时,必须先释放所占有的较高层的资源
    当进程获得某层的一个资源时,如果想申请同层的另一个资源,必须先释放此层中已占有的资源

死锁检测算法:

每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。

java的检测方式-建议大家动手实践一下

https://blog.csdn.net/weixin_43767015/article/details/104710979
jps查看日志,jconsole可视化界面。

java解决死锁

加锁顺序一致,统一各个线程获取锁的顺序,并强制线程按照指定的顺序获取锁;请求锁超时则返回,尝试获取锁时加一个超时时间,没获取到则不断回退,释放所有已经得到的锁;等待中断,死锁后发送中断信号,利用ReentrantLock的lock.lockInterruptibly()方法接受中断,并释放锁。

进程和线程的区别-重点

进程和线程的区别:进程是调度资源的基本单位,线程是执行任务的基本单位。进程有自己的独立数据空间,程序切换的开销大,线程共享一个进程的数据空间,每个线程有自己独立的运行栈和程序计数器,线程之间开销小。进程之间的资源隔离,共享复杂,线程的共享资源简单。线程的目的是为了并发,因为线程上下文的切换快,可以提高并发效率。

为什么线程切换快

最主要的一个区别在于进程切换涉及虚拟地址空间的切换而线程不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。
有的同学可能还是不太明白,为什么虚拟地址空间切换会比较耗时呢?
现在我们已经知道了进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB(translation Lookaside Buffer,我们不需要关心这个名字只需要知道TLB本质上就是一个cache,是用来加速页表查找的)。由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么当进程切换后页表也要进行切换,页表切换后TLB就失效了,cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。https://blog.csdn.net/qq_34417408/article/details/110393655

虚拟内存

虚拟内存 使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上。每个进程有独立的地址空间,这个空间有许多页,这些页不用所有都在内存中才能运行程序,所以一部分存在磁盘中,需要的时候再映射

分段,分页

分段:进程被分为很多的段,要装入一个进程,需要将进程所有的段装入主存中不一定连续的动态分区。;分页:主存被分为很多大小相同的帧,进程被分为很多与帧大小相同的页。要装入一个进程,需要将进程所有的页装入主存,可以是不连续的帧中。https://blog.csdn.net/zouliping123/article/details/8869455

内核态和用户态的区别

内核态可以访问所有数据,包括硬盘,网卡,也可以切换程序。用户态只能访问受限的内存,不能访问外围设备,占用CPU的能力被剥夺。

进程调度算法

先进先出,最短耗时优先,时间片轮转,多级反馈队列。https://www.jianshu.com/p/ecfddbc0af2d

页面置换算法

先进先出,最近最久未使用LRU,opt最佳置换算法(理想情况)

进程间通信方式,6种-重点

1.管道:单向的,所以需要两条管道;只能用于父子进程,兄弟进程(亲缘关系);数据先进先出;缓冲区是一个循环队列,满了之后会扔到阻塞队列里面
2.有名管道:不需要必须父子进程,兄弟进程;也是先进先出
3.信号:比如kill -9会发送SIGKILL信号
4.消息队列:消息队列在内存中,允许多个进程写入或者读取;也是先进先出
5.共享内存:多个进程可以读写同一块空间
6.socket

孤儿进程和僵尸进程

孤儿进程表示父进程退出了,但是子进程还在运行,这些就是孤儿进程,孤儿进程会被init进程(进程号为1)回收,所以基本不会有问题。僵尸进程是子进程正常退出,会给父进程发送信号,告诉父进程释放掉进程号,但是如果父进程不选择接受,不调用wait/waitpid,进程号将不会被释放。https://www.cnblogs.com/anker/p/3271773.html

select,poll和epoll-重点

https://www.cnblogs.com/aspirant/p/9166944.html

epoll两种工作模式

水平触发(没写完不断通知),边缘触发(只通知一次),LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读 => https://www.jianshu.com/p/73e9ef7902e1

网络

Tcp怎么保证可靠性

校验和,序列号,超时重传,拥塞控制,流量控制。答得时候展开说就可以,校验和是什么,超时重传是什么,拥塞控制、流量控制的过程。

哪些应用层协议使用了tcp

ftp(20传数据,21连接)http,smtp(发邮件,25端口),pop3(收邮件,110端口)

哪些应用层协议使用了udp

dns(53端口),snmp(管理网络的,161端口)

Tcp和udp区别

面向连接,可靠,是否多播,报文还是字节流,首部大小

tcp滑动窗口

接收方会在包里面携带窗口大小值,通知发送方不要发送太快。发送窗口分为四种类型,已发送已ack,已发送未ack,待发送未ack,未发送未ack。如果窗口满了,那就不会再移动。还有累计应答机制,该序列号之前的所有的都成功接收了。接收窗口分三种,已接收并且已发送ack,未接收但是可以接受,未接收且不能接收。https://www.cnblogs.com/xiaolincoding/p/12732052.html

如果服务器中含有大量一直处于close_wait的tcp连接,是为什么

close_wait之后会返回一个ack给客户端,如果还在这个状态,说明服务器卡住了,耗时太长。https://www.cnblogs.com/grey-wolf/p/10936657.html

有大量timewait怎么办

timewait快速回收和重用。快速回收:通过修改参数启用快速回收,此时timewait只有一个rto的时间。重用有两个条件,1.新连接的初始序列号比TW老连接的末序列号大。2.如果使用了时间戳,那么新到来的连接的时间戳比老连接的时间戳大。并且同一个ip和端口号的才能重用。

tcp拥塞控制

主要是拥塞控制算法的过程。
https://zhuanlan.zhihu.com/p/133307545

转发和重定向

转发是在服务器内部,把请求又发送给另一个servlet,所以外部看到的url是不变的,而且只能跳转到内部的链接;重定向是服务器向客户端发送一个指令,让客户端再次请求,所以此时地址栏会变化。

https数字证书的验证过程-重点

https://www.cnblogs.com/funny11/p/6978908.html

Mysql

sql优化索引

MySQL索引优化,9条:1.前导模糊查询不能用索引 2.数据出现隐式转换,比如1和'1',不会用索引 3.复合索引只能用最左边的 4.被or分割的条件,or前面有索引,后面没索引,也会全表扫描 5.不等于,not in,not like都不会走索引,可以优化为in 6.数据库执行计算不会用到索引,where age+1>24 7.尽量查询覆盖索引,避免回表 8.更新频繁不能建立索引 9.区分度不大不能建立索引
三层的B+树最多存储两千万数据

mysql读写分离:有延迟导致数据不一致怎么办

https://time.geekbang.org/column/article/77636
第一种方案强制读主,某些强一致性的业务直接走主库读取,如果一致性要求不高,可以采用,但是如果所有业务都要求强一致性,那么肯定不行。
第二种,判断主备是否有延迟。show slave status语句可以查出从库的情况,有一项是延迟,所以可以通过延迟判断,每次从库执行查询请求前,先判断 seconds_behind_master 是否已经等于 0。如果还不等于 0 ,那就必须等到这个参数变为 0 才能执行查询请求。show slave status还有其他结果,比如位点,可以通过位点判断是否一致。主库有两组值,表示的是读到的主库的最新位点,从库也有两组,表示的是备库执行的最新位点。如果这两组完全一致,那就说明日志已经同步完成。还有gtid(全局事务id),上面语句结果还有gtid集合,一个是备库收到的所有日志的 GTID 集合,另一个是备库所有已经执行完成的 GTID 集合,如果两个集合一样,就说明同步完成。或者可以同时启用semi-sync,当收到从库ack之后才返回给客户端,但一主多从的情况下,可能只收到了一个从节点,此时还可能不一致。还有一个问题是高并发情况下,之前用gtid判断总是不相等。
第三种,mysql提供了一个语句查询当前库是否有对应的gtid,有返回0,超时返回1.等 GTID 的执行流程就变成了:trx1 事务更新完成后,从返回包直接获取这个事务的 GTID,记为 gtid1;选定一个从库执行查询语句;在从库上执行 select wait_for_executed_gtid_set(gtid1, 1);如果返回值是 0,则在这个从库执行查询语句;否则,到主库执行查询语句。这样所有超时的也会到主库访问,这个业务代码需要我们自己来权衡了。

MySQL的可重复读怎么解决的幻读

快照读情况写,用的mvcc;当前读用的是间隙锁

MySQL语句的执行流程

1.tcp连接数据库 2.查询缓存(8.0之后没有这个功能了)3.分析器:词法分析把句子变成单词,语法分析满足是否满足语法 4.优化器 5.执行器:先检查有没有权限,然后调用存储引擎的接口查询每一行。https://www.cnblogs.com/wyq178/p/11576065.html

redolog和binlog

redolog记录的是数据变成了什么,可以用于异常故障宕机恢复数据,binlog记录的是逻辑日志,记录对应的sql语句,可以用于主从复制。https://www.cnblogs.com/wupeixuan/p/11734501.html

Redolog为什么可以crashsafe(数据恢复),但是binlog不行

redolog循环写,保留的都是未刷入磁盘的数据,binlog全量写,分不清哪个在磁盘,哪个不在,https://cloud.tencent.com/developer/article/1757612

事务,acid是怎么实现的

https://www.cnblogs.com/kismetv/p/10331633.html

数据库innodb和myisam区别

https://blog.csdn.net/qq_41706670/article/details/92836395

分表策略

垂直拆分,按照列拆分。水平拆分,根据id取模(方法:雪花算法),时间,地点分表。
此时联合查询怎么办:多次查询,在业务层做关联;做数据冗余

explain语句查看select执行情况

https://blog.csdn.net/qq_15764477/article/details/109602129

binlog的三种格式

row会记录每一行的修改,如果update id>1000那么会记录几百万行的修改。statement则是记录原始的sql语句。mixed由mysql决定使用哪一种。statement减少了日志的数量,但是对于存储过程,触发器,函数一类的不友好。https://blog.csdn.net/qq_34556414/article/details/107425717

间隙锁是怎么锁的

没有索引,锁住整张表。有索引,对于范围查询,锁住整个范围,左闭右闭。等值查询,寻找闭当前值小的一个和大的一个锁住,id=5,封锁(5的上一个,5]和(5,5的下一个],并且不让修改为被锁定的值。对于id>5,锁住的是(5,100]。

Redis

redis的pipeline和lua有什么区别

当多个redis命令之间没有依赖、顺序关系(例如第二条命令依赖第一条命令的结果)时,建议使用pipline;如果命令之间有依赖或顺序关系时,pipline就无法使用,此时可以考虑采用lua脚本的方式来使用。pipeline不是原子的,lua是原子的,都不能回滚。https://www.jianshu.com/p/f06472f537bb

为什么不支持回滚?

对于语法错误,事务将不会被提交。
对于运行时错误,只有对某个键执行不符合其类型的命令时才会发生,也就是程序代码错误,这种错误只有在开发阶段才会发生,很少在生环境中发生。
因此,为了保持Redis的简单性,不提供回滚功能。

Redis实现分布式锁

https://juejin.cn/post/6844903830442737671
1.setnx+expire。问题1:如果只是代码里面写,代码执行到一半出异常,没有expire,锁无法释放,可以通过lua解决。问题2:a设置过期时间30s,锁自动释放,但是a没执行完,此时b获取到了锁,a使用del,释放的是b的锁。解决方式:需要把value设置为唯一的,比如UUID,标志是否是当前线程持有的,删除之前判断一下。问题3:还是上一个问题,此时a没执行完,b就开始执行了,并发执行肯定是不对的。解决办法:设置足够长的过期时间,为获取锁的线程增加守护线程,快过期时增加有效时间。分布式锁,无法等待锁释放。命令都是立即返回的,不会等待。解决方法1:轮询。解决方法2:redis有发布订阅功能,获取锁失败时订阅释放消息,获取的锁成功释放后,发送锁释放的消息。注意,redis的消息被发送后,没人接受就会丢失。2.redlock原理:假设有5个redis节点,获取锁时候从5个实例,使用相同的key和唯一的UUID获取锁,如果能获取到3个(n/2+1)个锁,并且时间在范围内,就算成功。如果锁获取失败,在所有的redis实例上面解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)

Redis缓存一致性

https://developer.aliyun.com/article/712285 -> 前面是分析,直接拉到最后。更新缓存最大的问题是更新的顺序可能不一致。先更新数据库,再删除缓存好一些。还可以通过定于binlog来异步更新缓存。

  1. 订阅binlog:A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
    B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但B线程只是从数据库中读取想要的数据,并不将这个数据放入缓存中,所以并不会导致缓存与数据库的不一致
    A线程更新数据库后,通过订阅binlog来异步更新缓存
  2. 延时双删: A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
    B线程进行读操作,从数据库中读入旧数据,共耗时N秒
    在B线程将旧数据读入缓存后,A线程将数据更新完成,此时数据不一致
    A线程将数据库更新完成后,休眠M秒(M比N稍大即可),然后再次淘汰缓存,此时缓存中即使有旧数据也会被淘汰,此时可以保证数据的一致性
  3. 先更新数据库,再删除缓存,会有短暂不一致。

如何保证redis中存放的都是热点数据

使用lru算法,从含有过期时间的key中lru,或者所有key中lru

Redis为什么单线程

因为redis没有磁盘操作,一般都是在io操作时才会多线程,而redis是内存存储,切换线程反而更耗时。b+树也是磁盘操作,所以使用跳表而不是b+树。那么redis瓶颈在哪里?两个地方,内存大小,因为关系到存储的数据量,第二个是网络带宽,因为redis客户端服务端可能不在一个机器上。

redis大key问题

如果是单个对象非常大:可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响;如果是set和list存储的元素非常多,可以根据元素类型分类,把key分散到许多集群中。删除时在redis4.0之后可以用lazy-free,异步删除。
这是kaito的redis建议:http://kaito-kidd.com/2021/03/04/redis-best-practice-optimization-road/

redis的lru有什么优化

https://zhuanlan.zhihu.com/p/142893249,
如果redis中每个key都存放在这个链表中,那么内存需要很大!!!
因此redis实现了一个简化版的lru.主要目的是减少 内存占用!.
在Redis 3.0中对近似的LRU算法做了一些优化,Redis中会维护大小是16的一个候选池的内存。
当第一次随机选取的采样数据,数据都会被放进候选池中,并且候选池中的数据会根据时间进行排序。
当第二次以后选取的数据,只有小于候选池内的最小时间的才会被放进候选池中。
当某一时刻候选池的数据满了,那么时间最大的key就会被挤出候选池。当执行淘汰时,直接从候选池中选取最近访问时间最小的key进行淘汰。

redis的lfu算法

https://zhuanlan.zhihu.com/p/142893249,文章后半段
有些数据以前经常被访问到,只是最近的时间内没有被访问到,这样就导致这些数据很可能被淘汰掉,这样一来就会出现误判而淘汰热点数据。
所以有lfu(Least Frequently Used):最近频繁被使用。根据key最近被访问的频率进行淘汰。

redis6.0的多线程

Redis 的多 IO 线程只是用来处理网络请求的,对于读写命令,Redis 仍然使用单线程来处理。通过配置项启用多线程。

redis的hash结构,字典结构

拉链法解决的hash冲突,那么在数据过多时,hash冲突不可避免,为了避免链表太长,直接进行rehash。rehash:Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;释放哈希表 1 的空间。到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。为了避免这个问题,Redis 采用了渐进式 rehash。简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。redis也会每秒扫描一下未迁移的数据,防止一直不被访问就无法迁移到新hash表。

redis如何防止数据丢失

aof和rdb一起用;做集群方案

redis主从复制

一开始是全量复制,建立连接之后,会维护长连接,把每一次命令同步给从节点。

redis过期键删除策略:惰性删除和定期删除

定期删除:每100ms扫描20个key(配置文件配置的),删除过期的,如果25%的过期了,就继续扫描删除操作。4.0之前会阻塞主进程,4.0之后可以选择异步执行。如果有大量的key同时过期了,就会阻塞住。

为什么redis用跳表不用红黑树

跳表有五种操作,插入删除,查找,有序输出所有元素,按照范围区间查找元素,比如[100,600]的元素。红黑树最后一种操作,效率没有跳表高。树的插入删除也比跳表实现复杂。

缓存雪崩

缓存大面积失效,过期时间为随机就行,加上熔断策略,限制某些业务的访问,双缓存:一级缓存失效后使用二级缓存,双缓存。另一种情况是redis宕机,此时只能熔断,限流,所以建议事前预防,集群部署。

缓存穿透

查询数据库里面没有的数据,不合规的数据,导致每次都直接查询数据库,接口增加违规校验,把数据库中获取不到的数据存入redis里面,布隆过滤器(原理:https://zhuanlan.zhihu.com/p/43263751)。

缓存击穿

热点数据失效,导致大部分请求直接查数据库,热点数据不过期,或者互斥锁就可以。

redis默认的持久化方式

rdb

JVM

jvm内存结构

https://www.cnblogs.com/ityouknow/p/5610232.html
程序计数器,私有,表示当前线程执行的字节码的行号。
虚拟机栈,私有:它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈,私有。
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
jvm模型:jdk1.8 不存在永久代,实现形式是元空间,字符串常量池和静态变量仍然在堆当中,运行时常量池、类型信息、常量、字段、方法被移动都了元空间中。

Java创建的对象都在堆区吗

虚拟机栈一般是用来存储基本数据类型、引用和返回地址的,但是也可以存储实例数据。
涉及到逃逸分析:在编译程序优化理论中,逃逸分析是一种确定指针动态范围的方法——分析在程序的哪些地方可以访问到指针。当一个变量(或对象)在子程序中被分配时,一个指向变量的指针可能逃逸到其它执行线程中,或是返回到调用者子程序。
如果一个子程序分配一个对象并返回一个该对象的指针,该对象可能在程序中被访问到的地方无法确定——这样指针就成功“逃逸”了。如果指针存储在全局变量或者其它数据结构中,因为全局变量是可以在当前子程序之外访问的,此时指针也发生了逃逸。
简单来讲,JVM中的逃逸分析可以通过分析对象引用的使用范围(即动态作用域),来决定对象是否要在堆上分配内存,也可以做一些其他方面的优化。
此时,方法内的对象不逃出作用域时,并且是基本类型组成的(int,double等),就直接在栈上面分配,编译后类似于。

public void method{
//不逃出作用域,因为没有返回值
    MyObject o = new MyObject();
    o.a=1;
    o.b=2
    // 编译后变成
    // a = 1;
    // b = 2;
}

逃逸分析还能用于锁消除:

private void someMethod() {
    Object lockObject = new Object();
    synchronized (lockObject) {
      System.out.println(lockObject.hashCode());
    }
}

此时会检测到根本不需要锁,锁清除:

private void someMethod() {
    Object lockObject = new Object();
    System.out.println(lockObject.hashCode());
}

类卸载,类回收时机

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。
  • 加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如 OSGi、JSP的重加载等,否则通常是很难达成的。
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

G1收集器-深入理解jvm148页

G1从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
在jdk9就已经是默认的垃圾收集器了。 基于Region的堆内存布局收集。 G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。 Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。 而对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待.
让G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台维护一 个优先级列表,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis(项目里面也是200)指定,默 认值是200毫秒),优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。 这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获 取尽可能高的收集效率 .
初始标记(短暂停顿,可以忽略)-并发标记(可以与用户线程并发执行)-最终标记(短暂暂停用户线程)-筛选回收(必须暂停用户线程)

什么时候会fullgc

调用system.gc();未指定老年代和新生代大小,堆伸缩时会fullgc,所以要配置-Xmx和-Xms;老年代空间不足;统计得到的minor gc存活对象超过了老年代剩余空间;jdk1.7之前的永久代满了

GCROOT有哪些

在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: ·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。 ·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 ·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 ·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 ·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 ·所有被同步锁(synchronized关键字)持有的对象。 ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

强软弱虚四种引用(强引用,弱引用,软引用,虚引用)

https://blog.csdn.net/qq_15764477/article/details/109366574

类加载时机,类加载过程

加载,验证,准备,解析,初始化。深入理解jvm359页。 1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始 化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有: ·使用new关键字实例化对象的时候。 ·读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) 的时候。 ·调用一个类型的静态方法的时候。 2)使用java.lang.reflect包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需 要先触发其初始化。 3)当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类。 5)当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。 6)当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

什么是JIT

当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。
通常javac将程序源码编译,转换成java字节码,JVM通过解释字节码将其翻译成相应的机器指令,逐条读入,逐条解释翻译。非常显然,经过解释运行,其运行速度必定会比可运行的二进制字节码程序慢。为了提高运行速度,引入了JIT技术。
在执行时JIT会把翻译过的机器码保存起来,以备下次使用,因此从理论上来说,采用该JIT技术能够,能够接近曾经纯编译技术。

常用工具

jps,jstack,jconsole。jps可以查看Java进程,jstack查看死锁,jconsole还可以查看到对象分配情况。具体自己试一下就可以。

Java

jdk1.7和1.8的区别

接口可以default;lambda表达式;可以用::传递方法;Stream API是把真正的函数式编程风格引入到Java中。其实简单来说可以把Stream理解为MapReduce,当然Google的MapReduce的灵感也是来自函数式编程。她其实是一连串支持连续、并行聚集操作的元素。从语法上看,也很像linux的管道、或者链式编程,代码写起来简洁明了,非常酷帅!https://www.cnblogs.com/aspirant/p/8617201.html

hashmap的key是按照什么排序的

看是否实现compre方法,没实现就按照类名字排序,如果名字一样,按照hashcode排序。

ClassNotFind和NoClassDefine的区别-alibaba

https://www.cnblogs.com/zabulon/p/5893053.html

为什么反射的效率低

  1. Method#invoke 方***对参数做封装和解封操作
  2. 需要检查方法可见性
  3. 需要校验参数
  4. 反射方法难以内联
  5. JIT 无法优化

创建线程,4种

1.new Thread(new runnable) 2.继承Thread new Thread(){run()}重写run方法 3.创建一个futureTask对象,里面重写call方法,再放入Thread里面 4.线程池

线程5种状态

新建,就绪(调用了start之后),运行(获得了时间片之后),阻塞,死亡

Java异常体系

父类是Throwable,然后两个继承类error和exception,分为错误和异常。错误一般会导致线程停止,错误分为VirtualMachineError(比如oom,stackOverFlow)和其他error。异常分为RuntimeException(NPE)和其他。

Hashmap扰动函数

为了用上高位的特征,右移再异或混合了高位和低位的特征,增大随机性,放的时候会执行hash&(length-1),为了取出低位,直接就是table的index

hashmap为什么2倍扩容

迁移的时候旧节点会放在i或者oldCap+i的位置,红黑树的节点小于6,会变成链表。扩容时,数组长度大于64,链表长度大于8才转换。

synchronized底层原理

sycnchroized底层存在一个monitor,每个被加锁的对象都会有一个,字节码获取锁时执行monitorenter,如果计数器为0说明可以成功获取锁,计数器现在是1,否则就阻塞。

红黑树和平衡二叉树的区别

平衡二叉树的追求的是全局均衡,如在做插入,删除操作时,需要调整整棵树,显然这是费时的,因此希望在做调整时,是局部调整,因此提出了红黑树。

为什么hashmap加载因子是0.75

和统计学里面的泊松分布有关。

@transactional注解实现原理

利用threadlocal保证数据源一致

volatile怎么保证可见性

mesi缓存一致性协议,当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。发现数据无效是嗅探机制,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。嗅探也会引起总线风暴,因为不断循环嗅探,所以不能大量使用volatile。https://zhuanlan.zhihu.com/p/137193948

synchronized怎么保证可见性

synchronized在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。 1)线程解锁前,必须把共享变量的最新值刷新到主内存中 2)线程加锁时,将清空工作内存***享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值(注意:加锁与解锁需要是同一把锁)

红黑树的性质

二叉查找树。叶子节点都是黑色的null。根节点是黑的。不能有两个连续的红色节点,红色的子节点必须是黑的。从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

threadpoolexecutor怎么知道提交任务,哪个先完成

ThreadPoolExecutor有两个protected的方法,beforeExecutor和afterExecutor,继承ThreadPoolExecutor自己实现这两个方法就可以。

单例模式有几种实现

5种。饿汉,懒汉,双重锁,静态内部类,枚举。https://www.cnblogs.com/ngy0217/p/9006716.html,七种:https://cloud.tencent.com/developer/article/1497592 ,实现了饿汉改版,懒汉的线程安全版本

双亲委派

bootstrap加载JavaHome下的lib目录的文件,extension加载JavaHome/lib/ext目录的文件,application加载用户自己的类(classpath上的)

打破双亲委派

https://www.cnblogs.com/looyee/articles/13954722.html

ThreadLocal结构,为什么弱引用

每个线程都有一个ThreadLocalMap。ThreadLocalMap的key是ThreadLocal,value是我们要存储的值。https://www.cnblogs.com/-beyond/p/13125195.html

synchronized和ReentrantLock的区别

ReentrantLock的特点,synchronized只能是非公平锁,ReentrantLock可以指定;ReentrantLock有condition,condition可以唤醒指定线程;lock.interrupt方法可以等待线程中断

synchronized和lock在发生异常时的区别

synchronized会自动释放锁,lock不会自动释放锁,需要在finally释放锁

Jdk8默认的垃圾回收器

Parallel Scavenge + Parallel Old

零拷贝----没有公司考过,不过应该也是个重点东西

最原始的读数据,四步:read由用户态切换到内核态;CPU利用dma把数据从硬盘copy到内核空间;CPU将内核空间的数据拷贝到用户空间;上下文切换为用户态。写数据也是4步:write由用户态切换为内核态;把用户空间的数据拷贝到内核空间的网络缓冲区;CPU利用dma将数据从内核空间的网络缓冲区拷贝到网卡;上下文由内核态切换回用户态。原理1是通过mmap,发送数据时会先把文件dma到操作系统内核缓冲区中,应用程序和操作系统共享缓冲区,这样就是共享内存,write时就可以直接从内核复制到socket中,最后再把socket的数据dma发给网卡;原来是四次copy,现在只用三次。原理2是使用sendfile,三次copy和两次用户态切换。原理3,sendfile+dma gather,把中间的一次copy也去掉,socket缓冲区知道了数据的内存地址和大小,就不用再进行一次复制了。原理4,splice,建立了管道,减少CPU复制,其他一样。Java只有mmap和sendfile。https://www.cnblogs.com/rickiyang/p/13265043.html
nio和bio:https://www.jianshu.com/p/8ad464ed516e,https://juejin.cn/post/6844903975448215560

jdk8的流原理

stream流原理:中间操作与结束操作,中间操作只是对操作进行了记录,只有结束操作才会触发实际的计算(即惰性求值),这也是Stream在迭代大集合时高效的原因之一。这些Stream对象以双向链表的形式组织在一起,构成整个流水线。由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。https://zhuanlan.zhihu.com/p/93323794

Callable和Runnable区别

前者能够拿到返回值,后者没有。这两种线程池都可以submit方法调用。对于抛出的异常,call方法上有throws Exception,外层需要try-catch异常,如果不get,内层会吃掉异常。而runnable只能try-catch。

JUC大全----一般大公司才会问,建议把aqs学明白

操作系统的阻塞,加锁怎么实现的

https://blog.csdn.net/weixin_44367006/article/details/101637239
阻塞:进程是有时间片调度算法的,一个进程阻塞了,会放入等待队列里面,此时调度算法就不会再调度他。等他收到信号或者等待时间过了之后,再放入执行队列里面。
加锁:linux底层有futex函数可以放入操作系统的等待队列,同时告诉操作系统解锁的条件。具体到底层,还是cpu指令比如xchg可以保证原子性的比较,总线mesi协议可以保证各个cpu的值都是最新的。

unsafe的cas在汇编的实现

https://phantomvk.github.io/2018/02/08/CAS/
通过lock锁住总线,让其他cpu不能访问内存,和volatile的一样

unsafe.park和unpark

unpark是发放许可,park调用时会检查是否有许可,如果没有就阻塞

阻塞队列原理,记住源码

以arrayblockingqueue举例:https://segmentfault.com/a/1190000016311925,内部用ReentrantLock和condition实现的,notEmpty和notFull两个condition
put时候的阻塞:lock.lock()再操作,如果队列满了会调用notEmpty.await(),这里还用了while循环防止假唤醒,await这里就是aqs的等待队列了,如果没满直接入队,然后释放锁。在入队方法最后一行,需要调用notEmpty.signal,通知阻塞等待的队列过来放元素。
take删除时候的阻塞:删除的时候,先上锁,然后判断如果队列为空,也需要等待,即notEmpty.wait(),也是while循环防止假唤醒。在出队方法最后一行,需要notFull.signal,通知取元素。

AQS-AbstractQueuedSynchronizer

https://segmentfault.com/a/1190000015804888
加锁:把线程包装成节点加入队列。其他线程加锁时会cas插入到队列尾部,并Locksupport.park。a,b,c三个竞争锁,A先获取,此时没有竞争,直接获取,不进入队列;b获取时发现竞争,需要进入队列,aqs队列的特点是前一个节点表示后一个节点的状态,所以有一个伪头节点,b放入伪头节点之后,并修改伪头节点的标志位为-1,表示后面节点被阻塞。c同理。
所以现在队列是-1=>-1=>0
解锁:unlock的时候,会调用release唤醒首节点。此时需要设置首节点状态为0,表示下一个节点将要被唤醒。然后把后继节点找出来,准备unpark节点。对于节点的状态还有一种情况是超时或者取消,也就是status=1的状态,此时需要从后向前查找第一个不是被取消的节点(为什么从后往前?考虑并发入队,首先node的next和prev指针都是volatile的,具体没想明白)
此时B节点被唤醒了,需要设置头节点是自己。
现在是0=>-1=>0
无论哪种状态,B都会被唤醒,然后把自己变成头节点(如果是取消节点,会在此时进行遍历,修改队列结构)。然后c唤醒,释放锁,此时后面没有节点了,所以只剩下一个伪头节点,是当前节点。

AQS的condition

https://segmentfault.com/a/1190000015807209
//ThreadA先调用lock方法获取到锁,然后调用con.await()
//ThreadB获取锁,调用con.signal()唤醒ThreadA
//ThreadB释放锁
condtion复用了aqs的node节点类型,condition的队列我们称作条件队列,原来的队列称作等待队列。a先获取到锁,然后await。await释放锁,然后把线程包装成节点入条件队列。
b获取锁,然后signal。signal会删除条件队列的头结点,并添加到等待队列,此时等待队列是-1=>0,在释放锁之后,这个等待队列的节点会被唤醒。
b释放锁,等待队列的a会被唤醒。

CountdownLatch

await都会加入到等待队列里面。在countdown时,如果计数器为0,会唤醒(unpark)等待队列的头结点。并且会修改当前节点的状态为0,再调用propagate,向后传播状态,来唤醒队列里面的所有节点。

ConcurrentHashMap

Java多线程进阶(二三)—— J.U.C之collections框架:ConcurrentHashMap(1) 原理:https://segmentfault.com/a/1190000016096542
put会判断三种情况,get不会加锁,对于链表,直接遍历,红黑树,遇到写修改时,会直接利用list的next,当做链表查找。
Java多线程进阶(二四)—— J.U.C之collections框架:ConcurrentHashMap(2) 扩容:https://segmentfault.com/a/1190000016124883

Springboot

Springboot怎么处理接口请求的

收到请求后,会遍历一个列表,找到满足url的handler。然后是参数解析,会有很多种参数,所以会遍历找出来一个可以解析的解析器进行解析。最后处理返回值并返回。

Bean的生命周期

主要有四个,实例化Instantiation,属性赋值Populate,初始化Initialization,销毁Destruction。在实例化前后有一个接口InstantiationAwareBeanPostProcessor,分别对应于实例化之前postProcessBeforeInstantiation方法和postProcessAfterInstantiation方法;之后是属性赋值,然后是初始化,初始化前后也有一个接口BeanPostProcessor,前后各有两个方法和之前的两个名字一样。最后销毁。https://www.jianshu.com/p/1dec08d290c1。
Bean生命周期详细版:实例化,填充属性,name,factory,applicationContext,before-init,init,after-init,调用,destory。https://www.zhihu.com/question/38597960。

factoryBean和beanFactory

beanFactory是IOC容器的接口,factoryBean是自定义实例化bean的一个工厂接口,给bean加上了一个简单工厂和装饰模式。

Aop

如果我们想在每个方法前面都加上一个参数校验,需要在方法中编写代码,但是这个代码和业务代码没有关系,所以可以把他们抽象出来,作为aop切面,可以注解实现切面。

Bean的作用域

五种:singleton:全局只有一个。prototype:每次调用bean,都是new。request:请求处理时创建,请求完成后销毁。session:所有http请求共享同一个请求bean。globalSession:全局session,portlet环境才生效,否则相当于session。

Spring的bean循环依赖怎么解决

https://www.zhihu.com/question/276741767/answer/1192059681 , 创建时会加入到三级缓存,这个时候还没有赋值,循环依赖的对象直接从三级缓存里面找就可以。构造函数的循环依赖怎么办,用@lazy注解,注入代理对象,当使用这个对象的时候再创建真实的对象。
https://blog.csdn.net/Revivedsun/article/details/84642316

为什么是三级缓存不是二级缓存

https://www.cnblogs.com/grey-wolf/p/13034371.html#_label5https://blog.csdn.net/weixin_49592546/article/details/108050566 。二级缓存可以解决aop动态代理的情况,代理对象会被放到二级缓存,解决代理对象的循环依赖。

其他

限流器---大厂必考

限流器怎么设计:固定窗口,滑动窗口,漏桶(阻塞队列),令牌桶(算数和时间差实现)。https://www.infoq.cn/article/qg2tx8fyw5vt-f3hh673和http://dockone.io/article/10137 (第二个有具体实现)
令牌桶算法Java实现:https://blog.csdn.net/xxxxssss12/article/details/107359477
简单点还可以用AtomicInteger:https://cloud.tencent.com/developer/article/1165247

排序算法的复杂度

快排,归并,堆排序一定要会写,可能会直接手撕。

全部评论

(30) 回帖
加载中...
话题 回帖

相关热帖

近期精华帖

热门推荐