b站-Java后端or数开-日常实习-一/二面
线下面试,没录音,很多题目记不得了
题目
- jvm的内存模型?String s = new String("abc")时发生了什么?
- jvm的垃圾回收器及使用场景。
- 介绍下依赖注入DI
- 介绍下HashMap
- instanceof实际是怎么完成判断的?
- 为什么选择Kafka?为什么能够支持大流量场景?如何保证数据不丢失?
- apollo的使用场景和原理
- 爬虫后数据清洗的方法有哪些?
- redis的使用场景?介绍下zset使用的数据结构?
- binlog日志的格式?介绍下redo log和undo log?
- 介绍下回表查询
算法
给了两道比较简单的题
复盘
String s = new String("abc")时发生了什么?
首先是去方法区的字符串常量池中寻找"abc"字符串,如果没有,则创建。
在堆中创建一个String对象,指向字符串常量池中的地址。
栈上指向堆中对象的地址。
所以看该字符串常量池中是否存在“abc”,若存在,则只需要在堆中创建一个对象,否则创建两个。
如果对于String a = "abc"?
则无需在堆上创建对象,栈上直接存储指向字符串常量池的地址。
jvm的垃圾回收器及使用场景
为什么需要垃圾回收器?
- 释放内存供其他对象使用
- 减轻程序员负担,自动管理内存
- 减少内存碎片
垃圾回收器及使用场景
注:图中的实线表示新生代垃圾回收器和老年代垃圾回收器的组合使用情况,其中G1和ZGC都是即可新生又可老年。
新生代
从新生代开始说起,Serial是单线程垃圾回收器,其不支持并发,垃圾回收时会停止所有工作线程,且在对大内存进行处理时,但效率效率较低。
由此引出两个改进方向:
- 垃圾回收和工作线程并发执行,减少Stop the World(STW)现象
- 使用多线程进行垃圾回收,加快对大内存的垃圾回收速度。
针对方向一,专门针对新生代的垃圾回收器均未做调整,这可能是因为年轻代的回收相对较快?引入并发执行可能带来更多的风险?
针对方向二,则引出了Serial的多线程版本:ParNew。单核上与Serial效果一致,多核上效率更高,默认线程数和cpu数量相等。
垃圾回收时,有一个trade off,即停顿时间和吞吐量。有可能面临的问题是:每次的停顿时间减少了,但是停顿次数更多,导致最后的总吞吐量可能更少。根据不同的场景,可以确定目标是max(减少停顿时间, 增大吞吐量)。一般对于web服务,响应用户请求通常需要减少停顿时间,对于长时间的计算任务,交互较少的任务,那可能需要增大吞吐量。
对于新生代的垃圾回收,如果是专注于吞吐量,可以选择Parallel Scavenge,该垃圾回收器的特点是吞吐量优先,且可控。
对于新生代来说,实际垃圾清除时,都是用标记-复制算法,其特点是高效,不会产生内存碎片,缺点就是费内存,但是新生代一般都比较小,所以这么选择也合理。
老年代
和新生代中Serial对应的老年代垃圾回收器是Serial old,也是单线程,优缺点基本一致。
和新生代Parallel Scavenge对应的老年代垃圾回器是Parallel old,优缺点基本一致。
二者均采用标记-整理算法,特点是无内存碎片,但相对效率不高。
在新生代中提到一个改进方向,就是通过工作线程和垃圾线程并发执行,来降低STW时间。老年代中的CMS即从这个角度解决问题,以获取最短停顿时间。
当然这里的并发不是完全并发,是在并发标记和并发清理阶段并发,而在处理标记和重新标记时进行STW,不过这两个阶段很快,最终STW时间很短。
优点上就是低停顿时间,缺点:
- 对cpu资源敏感,cpu不足4个时,对用户影响大
- 无法处理浮动垃圾,出现Concurrent Mode failure时,会导致full gc,这时候会采用Serial old来进行垃圾收集,Serial old可是一个单线程的垃圾回收器,这次会产生长时间的STW。
- 每次垃圾回收阶段,用户线程还在运行,并产生垃圾,所以需要预留空间,不能像其他垃圾回收器那样等老年代满了再回收,这个比较可以通过-XX:CMSInitiatingOccupancyFraction修改。
- 使用标记-清除算法,有内存碎片。
老少通吃
G1回收器基于分区的思想,每个小区可以属于新老代。
G1有计划的避免在全区域进行垃圾收集,其根据每个区中垃圾堆积的价值(回收后的空间大小 vs 回收所需要的时间),维护优先队列,来确定收集目标。
每个区维护区域内引用类型与其他区域数据的引用关系,使用Remembered Set,目的是在做GC Roots Tracing时避免扫描全堆。(之前的垃圾回收器在新老之间也要记录)
不同的代所占据的区块数也是动态调整。
整体上使用标记-整理,对于每个小块来说,回收时将其复制到另一小块,是标记-复制。高效且避免了内存碎片。
好处就是STW时间可控(通过参数设置停顿时间不超过N毫秒),由于分区策略,对大内存应用也好使。
至于其如何去解决G1的每个问题,这里就没看了。
ZGC暂时没看。
Kafka高吞吐量的原因
Kafka在普通机械硬盘下也可以达到每秒几百万的处理量。
从以下四个方面考虑其高性能的原因:
- 磁盘顺序读写:虽然在随机读写下,机械硬盘 < 固态硬盘 << 内存,但是顺序读写时,访问速度差距小了很多。
- 页缓存:数据首先写入到文件系统的页缓存中,依赖操作系统进行flush时异步刷盘(可以调整producer.type来修改,sync or async),降低磁盘I/O次数。
- 零拷贝:考虑消费者从kafka消费数据,Kafka将数据发送到网络上的过程
- 不使用零拷贝的情况,4次传输,CPU参与
- 使用零拷贝,2次传输,全程DMA控制,缩短时间65%。
- 批量处理:发送端进行批量压缩和发送,降低网络IO带来的影响。消费者要一条,实际发送了n条,同时通过offset来控制消费进度。
Binlog的日志格式
binlog不同于redo log,undo log,其实在Server层实现的日志。用于备份恢复和主从复制。
如果数据库数据全部被删了,需要通过binlog恢复,redo log因为是循环写,所以数据不全。
Binlog的日志格式分为三种
- STATEMENT(默认):记录逻辑操作,sql语句。在有uuid,now等动态函数式,可能导致数据不一致。
- ROW:记录数据最终被修改的样子,但是针对一条update语句,可能会记录n条修改记录,导致binlog过大。
- MIXED:根据不同情况自动使用STATEMENT和ROW模式。
相比较而言,redo log记录的是物理日志,即在实际的物理地址上进行了什么更新。
instanceof实际是怎么完成判断的?
对 A instanceof B来说,
if A为null: return True if A == B: return True switch A: case 接口类型: 遍历A实现的接口,若有与B一致的,return True case 类类型: 遍历A的super链直到Object,若有与B一致的,return True case 数组类型: 看B,B如果是类,只能是Object B如果是接口,必须是数组实现的接口之一。 B如果是TC类型的数组,则与A的SC类型的数组,相比,二选一: TC和SC是相同的基本类型 TC和SC都是引用类型,但是可以在运行时强制转换。
参考资料
https://blog.csdn.net/weixin_54232686/article/details/126862579
https://blog.csdn.net/weixin_63020134/article/details/131357852
https://blog.csdn.net/Mr_YanMingXin/article/details/121451254