JVM
招银⽹络 Java ⾯试
⼤家好,我是⼩林。
这个⽉跟分享了 很多互联⽹中⼤⼚的校招薪资,有同学留⾔想看看 银⾏⽅⾯薪资待遇。
这次我们来看招商银⾏⽹络科技 Java 后端开发的 25 届校招薪资:

sp offer :18k x 12 + 9w (津贴+绩效)=年包 31w ,同学学 历背景硕⼠ 985 , 城市杭州
sp offer :17.5k x 12 + 9w (津贴+绩效)= 年包 30w ,同学学 历背景硕⼠985 , 城市深圳
普通 offer :15.5k x 12 + 9w (津贴+绩效)= 年包 27w ,同学学 历背景硕⼠双⼀流,城市杭州
公积⾦是按 12% 缴纳,整体年包在 27 〜30w 。
招银⽹络科技是招商银⾏的软件中⼼,⼤部分开发都是在这边 ,上班时间会⽐银⾏多⼯作⼏个⼩
时,深圳这边 是早上8点半到下午5点半,中午两⼩时午休。⼯作⽇有时会有加班,这个也 看项⽬
情况,周末⼀般是正常双休,加班强度⽐互联⽹要低不少的。
看到拿到招银⽹络的 offer 同学的学历都还是⽐较优秀的,估计⼿上或多或少都有互联⽹中⼤⼚的
offer ,不少同学就把招 银⽹络 offer 给鸽了,选择去了⼤⼚。
⽐如,有位训练营同学找我 offer 咨询,他⼿上除了招银 offer 之外,还有很多不错的 offer 。
那招银⽹络的⾯试难度如何?
互联⽹⼤⼚的⾯试强度普遍是⽐较⾼的,动不动就是 1 个⼩时的,⾯下来整体⼈的感受是会精疲
⼒尽的,不过⼀般也只有互联⽹⼤⼚的强度会⽐较⾼,但是像国企和银⾏普遍是 20-30 分钟左右
就结束⼀场⾯试,强度是直接降低了⼀半。
这次,来⼀起看看 ⼀位同学的** 招银⽹络的 Java 后端⾯经,** ⼀⾯主要是问⼋股了,⼋股+算法,⾯试时⻓⼤概 30 分钟,主要拷打了 Java 基础、JVM 、操作系统、⽹络、MySQL 、Redis 的内容。

Java 基础
知道集合有哪些吗?

List 是有序的Collection ,使⽤此接⼝能够精确的控制每个元素的插⼊位置,⽤⼾能根据索引访问
List 中元素。常⽤的实现List 的类有LinkedList ,ArrayList ,Vector ,Stack 。
ArrayList 是容量可变的⾮线程安全列表,其底层使⽤数组实现。当⼏何扩容时,会创建更⼤的
数组,并把原数组复制到 新数 组。ArrayList ⽀持对元素的快速随机访问,但插⼊与删除速度很
慢。
LinkedList 本质是⼀个双向链表,与ArrayList 相⽐,,其插⼊和删除速度更快,但随机访问速度更
慢。
Set 不允许存在重复的元素,与List 不同,set 中的元素是⽆序的。常⽤的实现有HashSet ,
LinkedHashSet 和TreeSet 。
HashSet 通过HashMap 实现,HashMap 的Key 即HashSet 存储的元素,所有Key 都是⽤相同的
Value ,⼀个名为PRESENT 的Object 类型常量。使⽤Key 保证元素唯⼀性,但不保证有序性。由
于HashSet 是HashMap 实现的,因此线程不安全。
LinkedHashSet 继承⾃HashSet ,通过LinkedHashMap 实现,使⽤双向链表维护元素插⼊顺序。
TreeSet 通过TreeMap 实现的,添加元素到集合时按照⽐较规则将其插⼊合适的位置,保证插⼊
后的集合仍然有序。
Map 是⼀个键值对集合,存储键、值和之间的映射。Key ⽆序,唯⼀;value 不要求有序,允许重
复。Map 没有继承于 Collection 接⼝,从 Map 集合中检索元素时,只要给出键对象,就会返回对
应的值对象。主要实现有TreeMap 、HashMap 、HashTable 、LinkedHashMap 、
ConcurrentHashMap
HashMap :JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主
要为了 解决哈希冲突⽽存在的(“拉链法”解决冲 突),JDK1.8 以后在解决哈希冲突时有了较⼤的
变化 ,当链表⻓度⼤于阈值(默认为 8)时,将链表转化为红⿊树,以减少搜索时间
LinkedHashMap :LinkedHashMap 继承⾃ HashMap ,所以它的底层仍然是基于拉链式散列结
构即由数组和链表或红⿊树组成。另外,LinkedHashMap 在上⾯结构的基础上,增加了⼀条双
向链表,使得上⾯的结构可以保 持键值对的插⼊顺序。同时通过对链表进⾏相应的操作,实现
了访问顺序相关逻辑。
HashTable :数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了 解决哈希冲突⽽
存在的
TreeMap :红⿊树(⾃平衡的排序⼆叉树)
ConcurrentHashMap :Node 数组+链表+红⿊树实现,线程安全的(jdk1.8 以前Segment 锁,
1.8 以后volatile + CAS 或者 synchronized )
arraylist 和linkedlist 的区别是什么 ?
ArrayList 和LinkedList 都是Java 中常⻅的集合类,它们都实现了List 接⼝。
底层数据结构不同:ArrayList 使⽤数组实现,通过索引进⾏快速访问元素。LinkedList 使⽤链表
实现,通过节点之间的指针进⾏元素的访问和操作。
插⼊和删除操作的效率不同:ArrayList 在尾部的插⼊和删除操作效率较⾼,但在中间或开头的
插⼊和删除操作效率较低,需要移动元素。LinkedList 在任意位置的插⼊和删除操作效率都⽐较
⾼,因为只需要调整节点之间的指针。
随机访问的效率不同:ArrayList ⽀持通过索引进⾏快速随机访问,时间复杂度为O(1) 。
LinkedList 需要从头或尾开始遍历链表,时间复杂度为O(n) 。空间占⽤:ArrayList 在创建时需要分配⼀段连续的内存空间,因此会占⽤较⼤的空间。
LinkedList 每个节点只需要存储元素和指针,因此相对较⼩。
使⽤场景:ArrayList 适⽤于频繁随机访问和尾部的插⼊删除操作,⽽LinkedList 适⽤于频繁的中
间插⼊删除操作和不需要随机访问的场景。
线程安全:这两个 集合都不是线程安全的,Vector 是线程安全的
JVM
为什么 java 有跨平台性?
Java 虚拟机定义了 ⼀种Java 内存模型(Java memory model, JMM )来屏蔽掉各种硬件和操作系统
的内存访问差异,简单理解也就是说Java 虚拟机相当于是在源码和平台之间抽象了⼀层出来,专⻔
处理⼀些平台之间访问的兼容问题,使得源码可以 一次编译到处运行 。
Java 源代码先是经过编译器进⾏编译,变成.class ⽂件,由类加载器加载进 内存运⾏,这种字节码⽂件与具体的平台和 机器硬件⽆关。

Java 程序员只 需在任意平台将 Java 代码编译成.class ⽂件,就可以在任意平台下的 JVM 中运⾏,从⽽隔绝了平台和 机器硬件的差异,实现了跨平台。
对象存储在哪个区?
对象主要存储在堆区中,不过 Java 对象并不⼀定都是分配在堆内存上,如果JVM 确定⼀个对象不
会逃逸 ,它可以选择将这个对象分配在线程的栈上⽽不是堆上。栈是线程私有的内存区域,当⽅
法执⾏结束时,栈上的数据会被⾃动销毁。
栈上分配依赖于逃逸 分析和标量替换。
标量替换:就是将对 象拆解为其成员变量。例如,如果⼀个对象包含整数 、浮点数和其他基本
数据类型的字段,那么这些字段将被单独分配到栈上。解决不会因为没有⼀⼤块连续空间导致
对象内存不够分配的问题。
标量:标量即不可被进⼀步分解的量,JAVA 的基本数据类型就是标量(如:int ,long 等基本数
据类型以及reference 类型等)。
聚合量:聚合量就是可以被进⼀步分解的量,通常是对象,JAVA 中对象就是可以被进⼀步分解
的聚合量,对象包含多个成员变量。
相关的JVM 参数:
逃逸 分析:-XX:+DoEscapeAnalysis (JDK7 以后默认开启)
标量替换:-XX:+EliminateAllocations (JDK7 以后默认开启)通过逃逸 分析和栈上分配,JVM 可以减少垃圾 回收的频率和开销。这有助于提⾼应⽤程序的性能,
特别是在存在⼤量临时对象的情况下,因为这些对象可以更快地释放,⽽不会给垃圾 回收器带来
过⼤的压⼒。
垃圾 清理的对象的分代介绍⼀下?
JVM 将堆空间分成了新⽣代和⽼⽣代,如下图所⽰:

通过上图, 可以看到新⽣代和⽼年代的对⽐,Minor GC 发⽣在新⽣代,⽽ Full GC 发⽣在⽼年
代。新⽣代分为三个 区,⼀个 Eden 区和两个 Survivor 区。
先来看下 Eden 区的作⽤,⼤部分新⽣成的对象都是在 Eden 区,Eden 区满了之 后便没有内存给新
对象使⽤,Eden 区便会 Minor GC 回收⽆⽤内存,剩下的存活对象便会 转移到 Survivor 区。
那两个 Survivor 区的作⽤分别 是什么 呢?两者其实是对称分布的,⼀个是 From 区,⼀个是 To
区。从 Eden 区存活下来的对象⾸先会被复制到 From 区,当 From 区满时,此时还存活的对象会
被转移到 To 区,经历了多次的 Minor GC 后,还存活的对象就会被复制到 ⽼年代,⽼年代的 GC
⼀般叫作 FullGC 或者 MajorGC 。
我们对⽐下新⽣代垃圾 回收和⽼年代垃圾 回收的区别,如下表所⽰:

操作系统
线程和进程的区别?

本质区别:进程是操作系统资源分配的基本单位,⽽线程是任务调度和执⾏的基本单位
在开销⽅⾯:每个进程都有独⽴的代码和数据空间(程序上下 ⽂),程序之间的切换会有较⼤的
开销;线程可以看做轻量级的进程,同⼀类线程共享代码和数据空间,每个线程都有⾃⼰独⽴
的运⾏栈和程序计数器( PC ),线程之间切换的开销⼩
稳定性⽅⾯:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。⽽进程中的⼦进程崩
溃,并不会影响其他进程。
内存分配⽅⾯:系统在运⾏的时候会为每个进程分配不同的内存空间;⽽对线程⽽⾔,除了
CPU 外,系统不会为线程分配内存(线程所使⽤的资源来⾃其所属进程的资源),线程组之间只
能共享资源
包含关系:没有线程的进程可以看做是单线程的,如果⼀个进程内有多个线程,则执⾏过程不
是⼀条线的,⽽是多条线
同⼀进程内线程不共享的内存知道吗?
每个线程的栈是独⽴的,不共享。
⽹络
tcp 三次握⼿和四次挥⼿过程说⼀下
三次握⼿
TCP 是⾯向连接的协议,所以使 ⽤ TCP 前必须先建⽴连接,⽽建⽴连接是通过三次握⼿来进⾏
的。三次握⼿的过程如下图:

⼀开始,客⼾端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端⼝,处于 LISTEN 状
态

客⼾端会随机初始化序号(client_isn ),将此序号置于 TCP ⾸部的「序号」字段中,同时把
SYN 标志位置为 1,表⽰ SYN 报⽂。接着把第⼀个 SYN 报⽂发送给服务端,表⽰向服务端发起
连接,该报⽂不包含应⽤层数据,之后客⼾端处于 SYN-SENT 状态。

服务端收到客⼾端的 SYN 报⽂后,⾸先服务端也随机初始化⾃⼰的序号(server_isn ),将此序
号填⼊ TCP ⾸部的「序号」字段中,其次把 TCP ⾸部的「确认应答号」字段填⼊ client_isn + 1,接着把 SYN 和 ACK 标志位置为 1。最后把该报⽂发给客⼾端,该报⽂也不 包含应⽤层数据,之
后服务端处于 SYN-RCVD 状态。

客⼾端收到服务端报⽂后,还要向服务端回应最后⼀个应答报⽂,⾸先该应答报⽂ TCP ⾸部
ACK 标志位置为 1 ,其次「确认应答号」字段填⼊ server_isn + 1 ,最后把报 ⽂发送给服务
端,这次报⽂可以携带客⼾到服务端的数据,之后客⼾端处于 ESTABLISHED 状态。
服务端收到客⼾端的应答报⽂后,也进⼊ ESTABLISHED 状态。
从上 ⾯的过程可以发现第三次握⼿是可以携带数据的,前两次握⼿是不可以携带数据的,这也是
⾯试常问的题。
⼀旦完成三次握⼿,双⽅都处于 ESTABLISHED 状态,此时连接就已建⽴完成,客⼾端和服务端就
可以相互发送数据了。
TCP 四次挥⼿过程

具体过程:
客⼾端主动调⽤关闭连接的函数,于是就会发送 FIN 报⽂,这个 FIN 报⽂代表客⼾端不会再发
送数据了,进⼊ FIN_WAIT_1 状态;服务端收到了 FIN 报⽂,然后⻢上回复⼀个 ACK 确认报⽂,此时服务端进⼊ CLOSE_WAIT 状
态。在收到 FIN 报⽂的时候,TCP 协议栈会为 FIN 包插⼊⼀个⽂件结束符 EOF 到接收缓冲区
中,服务端应⽤程序可以通过 read 调⽤来感知这个 FIN 包,这个 EOF 会被放在已排队等候的
其他已接收的数据之后,所以必须要得继续 read 接收缓冲区已接收的数据;
接着,当服务端在 read 数据的时候,最后⾃然就会读到 EOF ,接着 read() 就会返回 0,这时服务端应⽤程序如果有 数据要发送的话,就发完数据后才调⽤关闭连接的函数,如果服 务端应⽤
程序没有数据要发送的话,可以直接调⽤关闭连接的函数,这时服务端就会发⼀个 FIN 包,这
个 FIN 报⽂代表服务端不会再发送数据了,之后处于 LAST_ACK 状态;
客⼾端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客⼾端将进⼊ TIME_WAIT
状态;
服务端收到 ACK 确认包后,就进⼊了最后的 CLOSE 状态;
客⼾端经过 2MSL 时间之后,也进⼊ CLOSE 状态;
mysql
事务四⼤特性有哪些?
原⼦性(Atomicity ):⼀个事 务中的所有操作,要么全部完成,要么全部不完成,不会结束在
中间某个环节,⽽且事 务在执⾏过程中发⽣错误,会被回滚到事务开始前的状态,就像这个事
务从来没有执⾏过⼀样,就好⽐买⼀件商品,购买成功时,则给商家付了 钱,商品到⼿;购买
失败时,则商品在商家⼿中,消费者的钱也没花出去。
⼀致性(Consistency ):是指事务操作前和操作后,数据满⾜完整性约束,数据库保持⼀致性
状态。⽐如,⽤⼾ A 和⽤⼾ B 在银⾏分别 有 800 元和 600 元,总共 1400 元,⽤⼾ A 给⽤⼾ B
转账 200 元,分为两个 步骤,从 A 的账⼾扣除 200 元和对 B 的账⼾增加 200 元。⼀致性就是
要求上述步骤操作后,最后的结果是⽤⼾ A 还有 600 元,⽤⼾ B 有 800 元,总共 1400 元,⽽
不会出现⽤⼾ A 扣除了 200 元,但⽤⼾ B 未增加的情况(该情况,⽤⼾ A 和 B 均为 600 元,
总共 1200 元)。
隔离性(Isolation ):数据库允许多个并发事务同时对其数据进⾏读写和修改的能⼒,隔离性可
以防⽌多个事 务并发执⾏时由于交 叉执⾏⽽导致数据的不⼀致,因为多个事 务同时使⽤相同的
数据时,不会相互⼲扰,每个事 务都有⼀个完整的数据空间,对其他并发事务是隔离的。也就
是说,消费者购买商品这个事 务,是不影响其他消费者购买的。
持久性(Durability ):事务处理结束后,对数据的修改就是永久的,即便系统故障也不 会丢
失。
MySQL InnoDB 引擎通过什么 技术来 保证事务的这四个特性的呢?
持久性是通过 redo log (重做⽇志)来保证的;
原⼦性是通过 undo log (回滚⽇志) 来保证的;
隔离性是通过 MVCC (多版本并发控制) 或锁机制来保证的;
⼀致性则是通过持久性+原⼦性+隔离性来保证;
事务并发会产⽣什么 现象?
MySQL 服务端是允许多个客⼾端连接的,这意味着 MySQL 会出现同时处理多个事 务的情况。
那么在同时处理多个事 务的时候,就可能出现脏读(dirty read )、不可重复读(non-repeatable read )、幻读(phantom read )的问题。
接下来,通过举例⼦给⼤家说明,这些问题是如何发⽣的。
脏读
如果⼀个事 务「读到」了另⼀个「未提交事 务修改过的数据」,就意味着发⽣了「脏读」现象。
举个 栗⼦。
假设有 A 和 B 这两个事 务同时在处理,事务 A 先开始从数据库中读取⼩林的余额数据,然后再执
⾏更新操作,如果此时事务 A 还没有提交事 务,⽽此时正好事务 B 也从 数据库中读取⼩林的余额
数据,那么事 务 B 读取到的余额数据是刚才事务 A 更新后的数据,即使没有提交事 务。

因为事 务 A 是还没提交事 务的,也就是它随时可能发⽣回滚操作,如果在上⾯这种情况事务 A 发
⽣了回滚,那么事 务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
不可重复读
在⼀个事 务内多次读取同⼀个数据,如果出现前后两次读到的数据不⼀样的情况,就意味着发⽣
了「不可重复读」现象。
举个 栗⼦。
假设有 A 和 B 这两个事 务同时在处理,事务 A 先开始从数据库中读取⼩林的余额数据,然后继续
执⾏代码逻辑处理,** 在这过 程中如果事务 B 更新了这条数据,并提交了事 务,那么当事务 A 再
次读取该数据时,就会发现前后两次读到的数据是不⼀致的,这种现象就被称为不 可重复读。

幻读
在⼀个事 务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不
⼀样的情况,就意味着发⽣了「幻读」现象。
举个 栗⼦。
假设有 A 和 B 这两个事 务同时在处理,事务 A 先开始从数据库查询账⼾余额⼤于 100 万的记录,
发现共有 5 条,然后事务 B 也按相同的搜索条件也是查询出了 5 条记录。

接下来,事务 A 插⼊了⼀条余额超过 100 万的账号,并提交了事 务,此时数据库超过 100 万余额
的账号个数就变为 6。
然后事务 B 再次查询账⼾余额⼤于 100 万的记录,此时查询到的记录数量有 6 条,发现和前⼀次
读到的记录数量不⼀样了,就感觉发⽣了幻觉⼀样,这种现象就被称为幻读。
可重复读如何解决幻读?
当同⼀个查询在不同的时间产⽣不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果
SELECT 执⾏了两 次,但第⼆次返回了第⼀次没有返回的⾏,则该⾏是“幻像”⾏。
按理论来说,只有到 可串⾏化 的最⾼隔离级别才能解决幻读问题,但是 MySql 在可重复读的隔离
级别下就已经通过⼀些⼿段解决了幻读问题:
针对快照读(普通 select 语句),是通过 MVCC ⽅式解决了幻读。⽆锁化,⽣成 ReadView 时版
本链上已经提交的事务可⻅。
针对当前读(select ... for update 等语句),是通过 next-key lock (记录锁+间隙 锁)⽅式解决了幻读。每次 都读最新记录,通过锁来控制并发。
这两个 解决⽅案是很⼤程度上解决了幻读现象,但是还是有个别的情况造成的幻读现象是⽆法解
决的。
如果要在⼀个表⾥⾯加⼊⼀个新的字段应该注意什么 ?
要注意这个表是否有被其他事 务正在读写,如果有 其他事 务正在读写这张表,这时候在表增加字
段可能会造成阻塞的问题。因为读写⼀张表的时候,加的是MDL 读锁(表级锁),⽽且对⼀个表增
加字段的时候会产⽣MDL 写锁,这时候就发⽣了MDL 读写锁冲突的问题。
redis
redis 和mysql 数据库如何保 证⼀致性?
对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache 。对于写数
据,我会选择更新 db 后,再删除缓存。

缓存是通过牺牲 强⼀致性来提⾼性能的。这是由CAP 理论决定的。缓存系统适⽤的场景就是⾮强⼀
致性的场景,它属于CAP 中的AP 。所以,如果需要数据库和缓存数据保持强⼀致,就不适合使⽤
缓存。
所以使 ⽤缓存提升性能,就是会有数据更新的延迟。这需要我们在设计 时结合业务仔细思考是否
适合⽤缓存。然后缓存⼀定要设置过期时间,这个时间太短、或者太⻓都不好:
太短的话请求可能会⽐较多的落到数据库上,这也意味着失去了缓存的优势。
太⻓的话缓存中的脏数据会使 系统⻓时间处于⼀个延迟的状态,⽽且系统中⻓时间没有⼈访问
的数据⼀直存在内存中不 过期,浪费内存。
但是,通过⼀些⽅案优化处理,是可以最终⼀致性的。
针对删除缓存异常的情况,可以使 ⽤ 2 个⽅案避免:
删除缓存重试策略(消息队列)
订阅 binlog ,再删除缓存(Canal+ 消息队列)
消息队列⽅案
我们可以引⼊消息队列,将第⼆个操作(删除缓存)要操作的数据加⼊到消息队列,由消费者来
操作数据。
如果应⽤删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试
机制。当然,如果重试超过的⼀定次数,还是没有成功,我们就需要向业务层发送报错信息
了。
如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个 例⼦,来说明重试机制的过程。

重试删除缓存机制还可以,就是会造成好多 业务代码⼊侵。
订阅 MySQL binlog ,再操作缓存
「先更新数 据库,再删缓存」的策略的第⼀步是更新数 据库,那么更新数 据库成功,就会产⽣⼀
条变更⽇志,记录在 binlog ⾥。
于是我们就可以通过订阅 binlog ⽇志,拿到具体要操作的数据,然后再执⾏缓存删除,阿⾥巴巴
开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从 复制的交互 协议,把⾃⼰伪装成⼀个 MySQL 的从节点,向 MySQL 主节点
发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal ,Canal 解析 Binlog 字节流
之后,转换为便于读取的结构化数据,供下游程序订阅使⽤。
下图是 Canal 的⼯作原理:

将binlog ⽇志采集发送到MQ 队列⾥⾯,然后编写⼀个简单的缓存删除消息者订阅binlog ⽇志,根
据更新log 删除缓存,并且通过ACK 机制确认处理这条更 新log ,保证数据缓存⼀致性。
算法
找出⼀些数中第⼆⼤数
