美团一面总结注:部分图片来自网络,如侵必删!
1、看你简历上写熟悉 JVM,你可以跟我讲一下其运行、内存管理的细节吗?首先,当然是自我介绍啦!这次自我介绍也有很多不足的地方:第一,因为太紧张了,导致念稿的语速比较快;第二,在介绍自己的技术栈的时候也有些许的卡顿,但是调整的比较快;第三,整体情绪比较平淡,感情不够强烈。
整体上来说自我介绍这一块也算是差强人意吧。
类加载在回答这个问题的之前,我先介绍了一下 JVM 的内存结构:栈、本地方法栈、方法区、程序计数器、堆;然后,开始聊 JVM 类加载过程(加载、连接、初始化),类加载阶段一块,我讲得不是很好,有点乱。
这一段我从 类加载 到 内存管理 的衔接做的不是很好
在开始说内存管理之前,首先介绍了一下常用的内存回收算法:标记整理、标记清除、复制、分代回收,同时也把这几种垃圾回收算法的优缺点说了一下
这里出了一个小插曲:就是 复制 算法的名字,我突然记不起来了,但是为了面试正常进行,我急中生智的说成了 from-to 算法,希望面试官能懂我的意思吧
再之后,从 JVM 历史角度谈了谈几种垃圾回收器:Serial GC、Parallel GC、CMS、G1等等,介绍每种垃圾回收器的时候,结合当时的时代背景,说了一下其产生原因、以及优缺点、回收的细节。
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个声明周期都将经历 加载(loading)、验证 (Verification)、准备 (Preaparation)、解析 (Resolution)、初始化 (Initialization)、使用 (Using) 、卸载 (Unloading) 七个阶段,其中验证、准备、解析三个部分统称为连接(linking)。
加载 “加载”(Loading)阶段只是整个 “类加载”(Class Loading)过程中的一个阶段,希望不要混淆。而在加载阶段,JVM 需要完成以下三件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流
(2)将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构(静态常量池 -> 动态常量池)
(3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用就是把 klass 暴露给 java 使用_super 即父类_fields 即成员变量_methods 即方法_constants 即常量池_class_loader 即类加载器_vtable 虚方法表(多态就是由其实现的)_itable 接口方法表
如果这个类还有父类没有加载,先加载父类
加载和连接可能是交替运行的
连接 类加载-连接-验证验证:连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《 Java 虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全
文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能够被当前版本的虚拟机处理。包括:魔数是否 0xCAFEBABE 开头;主、次版本号是否在当前 Java 虚拟机接受范围之内……元数据验证:对字节码描述的信息进行语义分析,以保证其符合《 Java 虚拟机规范》的要求。包括:这个类是否有父类;这个类是否继承了被 final 修饰的类……字节码验证:整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法、符合逻辑的。符号引用验证:最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的三阶段——解析阶段中发生。 类加载-连接-准备
准备:为 static 变量分配空间,设置默认值
final 修饰的变量,并不会在初始化阶段赋值,而是在准备阶段赋值
如 final static int a = 10;
final static String b = “hello”;
但是如果是用了 new 关键字声明的 final 字段,则还是在初始化阶段完成的
如 final static Object o = new Object();
staic 修饰的变量,则是在初始化
即,分配空间是在准备阶段完成的,而赋值阶段是在初始化阶段完成的
解析:JVM 将常量池内的符号引用替换为直接引用的过程
符号引用:符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。直接引用:直接引用时可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。 初始化——由 JVM 保证【构造方法】的线程安全
类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。直到初始化阶段,Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值(准备阶段对变量分配地址,对 final 修饰的 基本数据类型、String 对象进行初始化)。
初始化阶段就是执行类构造器
概括来说,类初始化是【懒惰式】
导致类初始化的情况:
main 方法所在的类,总是被首先初始化首次访问这个类的静态变量或静态方法时子类初始化,如果父类还没初始化,会引发父类的初始化Class.forNamenew 会导致初始化
不会导致类初始化的情况:
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化类对象 .class 不会触发初始化创建类的数组不会触发初始化类加载器的 loadClass 方法Class.forName 的参数 2 为 false 时 垃圾回收算法
标记清除
缺点:会造成内存碎片,最后导致内存溢出
标记整理
优点:没有内存碎片
缺点:由于整理牵扯到对象的移动,效率低,而且对象地址也会变
复制
优点:不会产生内存碎片
缺点:会占用双培的内存空间
分代垃圾回收(伊甸园、幸存区、老年代)
对象首先分配在伊甸园(Eden 区)新生代空间不足时,触发 minor gc,伊甸园和 from 存活对象使用 copy 复制到 to 中,存活对象年龄 +1 并且交换 from tominor gc 会引发 stop the world,暂停其他用户线程,等待垃圾回收结束,用户线程才恢复运行当对象寿命超过阈值时,会晋升至老年代,最大寿命是 15(bit)当老年代空间不足,会先尝试触发 minor gc,如果之后空间仍不足,那么触发 full gc,STW(stop the world)的时间更长
Serial GC
新生代:复制
老年代:标记整理
Parallel GC
新生代:复制
老年代:标记整理
CMS
新生代:复制
老年代:标记清除
由于老年代是标记清除算法,所以会产生内存碎片,从而从降低 Serial GC
引用计数法先说答案吧 => 引用计数法、可达性分析算法
在一面开始之前,笔者仔细阅读了一下 CMS 的三色标记算法,故回答这个问题的时候,笔者一时间没有想起来,毕竟 引用计数法淘汰了,还有循环引用的问题。
为了不耽误面试流程,我想了一下,说了我只了解 三色标记算法
面试官说:没有关系(扣分项,笔者也是在做总结的事情突然想起来了,但回答这个问题的时候,还是说到了 三色标记算法,应该不会留下太差的印象)
只要有一个对象被其他变量所引用,则让它的计数 +1,当你的计数为 0 时,则可以被垃圾回收,但是存在 循环引用 的问题。
可达性分析算法 Java:确定一系列的跟对象(即,GC Roots,那些肯定不能当成垃圾被回收的对象)。在垃圾回收之前,我们会对堆中的数据进行一遍扫描,看看每一个对象是否被 GC Roots 直接或者简介引用。如果是,那么这个对象不能被回收,否则,被回收。
JVM 中的垃圾回收器采用可达性分析来探索所有存活的对象
扫描堆中的对象,看能否沿着 GC Roots 对象为起点的引用链找到该对象,找不到,表示可以回收
那些对象可以作为 GC Roots?
3、你有了解 synchronized 和 Lock 锁吗?虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量引用的对象本地方法栈中 JNL (即,一般说的 native 方法)中引用的对象
4、Synchronized 是公平锁还是非公平锁首先,先回答一下什么是 synchronized 锁,毕竟 synchronized 锁经过这几年优化,其性能已经不逊色与 Lock 锁了。
笔者先是给面试官介绍了以前的 synchronized 是一上来就加重量级锁,以及 synchronized 底层 monitor 的实现(owner、wait set、entry set)等等。
然后介绍了一下,Lock 锁(ReentrantLock 锁)底层使用到了 AQS 锁,而 AQS 锁底层使用的是 Unsafe 类的 CAS 操作。(这里笔者还做好了面试官深入聊下去的准备,但是面试官没有继续,比如:ABA 问题等等)
非公平锁
5、你有说到 hotspot 对 Synchronized 进行了优化,你知道在 JDK 1.6 以后 Synchronized 锁做了那些优化吗?所谓非公平和公平锁,意思就是持有锁的对象,会维护一个阻塞队列:即每一个想要获取锁的线程。
非公平锁:就是持锁的线程释放锁对象的时候,又有新的线程想要请求该锁,则可能该新的线程成为 owner
公平锁:就是持锁的线程释放锁对象的时候,会从锁对象的阻塞队列中取队首作为下一个 owner,如果这个时候有新的线程也想请求该锁,只能添加到阻塞队列尾排队等待
在这道面试题的时候,面试官在我回答完几种优化机制后,没有深入问我偏向锁、轻量级锁、重量级锁、自旋的相关细节
偏向、轻量级锁、重量级锁、自旋
6、你可以跟我说说 ArrayList 和 LikedList 区别吗?相同点:
- ArrayList 和 linkedList 都实现了 List 接口ArrayList 和 linkedList 都不是线程安全的
不同点:
- ArrayList 和 linkedList 的内部实现的数据结构不同。ArrayList 内部是由数组实现的,而 linkedList 内部是由循环双链表实现的。ArrayList 是有数组实现的,所以 ArrayList 在进行查找操作时,速度要优于由链表实现的 linkedList,但是在进行删除添加操作时,linkedList 速度要优化 ArrayList。所以当进行查找操作更多时,使用 ArrayList,而如果进行插入和删除操作更多时,使用 linkedList。linkedList 需要使用更多的空间,因为它除了要存储数据之外,还需要存储该节点前后信息,而 ArrayList 索引处就是存的数据。
笔者这里回答 2 倍,回答错误,这个点以前看到过,但是没有印象了。扣分扣分!!
1.5 倍
ArrayList 的扩容机制 总的来说就是把数组复制到另一个内存空间更大的数组中,对 ArrayList 的来说,如果我们在创建对象的时候没有指定大小,默认大小为 10
扩容时机 如果当前数组大小大于数组初始容量(比如初始容量为10,当添加第 11 个元素时)就会进行扩容,新的容量为旧的容量的 1.5 倍
扩容方式 扩容时,会以新的容量创建一个原数组的拷贝,将原数组的数据拷贝过来,原数组就会被抛弃,会被 GC 回收
8、HashMap 有了解吗? HashMap 底层数据结构
JDK 1.7 及之前:数组 + 链表
扩容:头插法(容易出现逆序且环形链表死循环问题)
JDK 1.8:数组 + 链表 + 红黑树
扩容:尾插法
Hash 公式HashMap 扩容机制index = HashCode (key) & (Length - 1)
HashMap 的初始容量是 16,加载因子为 0.75,扩容增量是原容量的 1 倍。如果 HashMap 的容量为 16,一次扩容后容量为 32。HashMap 扩容是指元素的个数(包括数组、链表、红黑树)超过了 16 * 0.75 = 12 之后开始扩容。
HashMap 的长度为什么是 2 的幂次方?加载因子 = 填入表中的元素个数 / 散列表的长度
为什么选择 0.75 作为 HashMap 的加载因子呢?
泊松分布是统计学和概率学常见的离散概率分布,适用于描述单位时间内随机时间发生的次数的概率分布,选择 0.75 作为 HashMap 的加载因子,这个和泊松分布有关。
我们讲一个键值插入 HashMap 中,通过 将 key 的 hash 值与 length - 1 进行 & 运算,实现了当前 key 的定位,2 的幂次方可以减少(碰撞)次数,提高 HashMap 的查询效率
如果 length 为 2 的幂次方,则 length - 1 转化为二进制必定是 1111…… 的形式,与 h 的二进制 & 运算效率会非常高,而且空间不浪费
如果 length 不是 2 的幂次方,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在与 h 进行 & 运算,最后一位都为 0,而且 0001,0011,0101,1001,1011,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这就意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
9、你能跟我说说 ConcurrentHashMap 吗?在这个问题中,笔者先从 ConcurrentHashMap 的 1.7 和 1.8 底层数据结构入手,而对 ConcurrentHashMap 其最重要的就是线程安全性,故后面聊了其 1.8 对 1.7 锁方面的优化。
后面面试官问我,那 jdk 1.8 以后 ConcurrentHashMap 锁的是哪里?
这里,突然语塞了,就是和面试官都知道是哪个位置,但是我只能说出模糊概念。哎,最后面试官告诉我,叫 哈希槽。
**HashMap:**先说 HashMap,HashMap 是 线程不安全的
HashTable:HashTable 和 HashMap 的实现原理几乎一样,差别无非是 1. HashTable 不允许 key 和 value 为 null;2. HashTable 是线程安全的。但是 HashTable 线程安全的策略实现代价却太大,简单粗暴的,get/put 所有相关的操作都是 synchronized 的,这相当于给整个哈希表加了一把大锁
HashTable 性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的时,就不会存在锁竞争了,这样便可以有效地提高并发效率,这就是 ConcurrentHashMap 所采用的 ”分段锁“ 思想。
但是 jdk 1.7 中采用 Segment + HashEntry 的方式进行实现,其中 Segment 在实现上继承了 ReetrantLock,这就自带了锁的功能,结构如下:
而在 jdk 1.8 之后,放弃了 Segment 臃肿的设计,取而代之的是采用 Node + CAS + Synchronized 保证并发安全进行实现,采用和 HashMap 一样的数据结构:数组 + 红黑树 + 链表
10、线程池,你有了解过吗?线程池!对于这个问题,理应从 ThreadPoolFactory 创建 ThreadPool 的那几个参数入手,跟面试官详解的,但是我挑了其中最重要的几个参数和面试官详细的说了下我的理解,如:阻塞队列、核心线程、拒绝策略等等,向面试官介绍了 BlockingQueue、SynchronousQueue 以及几种拒绝策略。
public ThreadPoolExecutor(int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数(核心线程数 + 救急线程数)
long keepAliveTime, // 线程存活时间
TimeUnit unit, // 单位:秒、分、时
BlockingQueue workQueue, // 阻塞队列
ThreadFactory threadFactory, // 线程工厂,默认使用 defaultTreadFactory
RejectedExecutionHandler handler) { // 拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
11、跟我讲一讲你的项目吧
- 介绍为什么要做这个项目结合遇到的问题,引出使用到的技术、框架:即使用 xxx 框架,解决了 xxx 问题 的形式谈谈在项目过程中遇到的问题、bug、以及如何解决的回顾项目,对项目提出新的优化解决方案,如何优化现有项目
实例化 Bean 对象:通过反射,getDeclaredConstructor、newInstance
设置对象属性:populateBean(循环依赖问题、三级缓存)
检查 Aware 相关接口并设置相关依赖:invokeAwareMethod(完成 BeanName、BeanFactory、BeanClassLoader 对象的属性设置)
调用 BeanPostProcessor 中的前置处理方法:使用比较多的有(ApplicationContextPostProcessor,设置 ApplicationContext、Environment、ResourceLoader、EmbeddValueResolver等对象)
BeanPostProcessor 同时也可以实现装饰器模式 , 对 Bean 的处理进行加强 BeanPostProcessor 接口:是对 bean 的统一前后置处理而不是基于某一个 bean 其应用场景如下: 1. 可以解析 bean 中的一些注解转化为需要的属性 2. 注入处理一些统一的属性,而不用在每个 bean 中注入 3. 甚至可以做一些日志打印时间等 其 BeanPostProcessor 的后置处理器,去实现了 AOP 相关的操作,核心类:AbstractAutoProxyCreator
Spring 获取 properties 文件单个属性值,一般使用 @Value 属性值。
下面提供另一种基于Spring解析获取 properties 文件单个属性值的方式,使用 EmbeddedValueResolverAware 。
调用 init-method 方法:invokeInitMethod(判断是否实现了 initializingBean 接口,如果有,则调用 afterPropertiesSet 方法,没有就不调用)
调用 BeanPostProcessor 的后置处理方法:Spring 的 aop 就是在此处实现的,AbstractAutoProxyCreator
注册 Destruction 相关的回调接口:钩子函数
获取到完整的对象,可以通过 getBean 的方式来进行对象的获取
销毁流程:(容器销毁才会销毁)
- 判断是否实现了 DisposableBean 接口调用 destroyMethod 方法
MyBatis 中的一级缓存对于这个问题,其实我印象不是深了,但还是尽力把我知道的尽力串起来,组成完整的知识面,一级缓存就是会话级别的,而二级缓存是全局的。并且二级缓存默认是不开启的,需要手动开启二级缓存。
但是最后面试官问了我一个问题,你可以跟我说一说,在项目中需不需要开启 MyBatis 的二级缓存,以及为什么不建议开启 二级缓存?
对于这个问题,笔者确实是没有什么思路,但是最后根据个人的理解,说出了我的想法,面试官表示对我的回答说:大概的意思是对的。因为如果开启了二级缓存,不同的 SqlSession 执行的 SQL 语句,可能会导致二级缓存失效的问题,也就是缓存不是最新的。
一级缓存是 SqlSession 级别 的缓存,在操作数据库时需要构造 sqlSession 对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的 sqlSession 之间的缓存数据区域(HashMap)是互相不影响的,其中每一个 SqlSession 的内部都会有一个以缓存。
在应用运行过程中,我们有可能再一次数据会话中,执行多次查询条件完全相同的 SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据进行查询,提高性能。
MyBatis 二级缓存 14、跟我说一说 Redis 底层的数据结构吧15、你说你熟悉 MySQL,知道什么是 ACID 吗?回答这个问题的时候,笔者说了 Redis 常用的五中数据结构:string、list、set、hash、zset,还说了 bitmap,最后描述底层使用哈希表进行实现的
但面试官似乎不太满意,我最后只能强行拉回一波好感,说我可能不太了解,只知道其底层是使用了哈希表
原子性是由 undolog 日志保证的,它记录了需要回滚的日志信息,事物回滚时撤销 已经执行成功的 SQL
一致性是由其他三大特征保证的,程序代码需要保证业务上的一致性
隔离性是由 MVCC(多版本并发控制,依赖于记录中的三个隐藏字段
DB_TRX_ID 事务 ID
DB_ROLL_PTR 回滚指针
DB_ROW_ID 隐藏的主键
持久性是由 redolog 来保证的,MySQL 修改数据时会在 redolog 中记录一份日志数 据,就算数据没有保存成功,只要日志保存好了,数据仍然不会丢失
这个问题,我只答出了两个优化方案:1. 考虑加索引;2. 分库分表。并同时解释了这两种优化方法的运用场景和作用。
- 加索引避免返回不必要的数据适当分批量进行优化 SQL 结构分库分表读写分离
17、谈一谈你在大学期间遇到的一些困难、或者有成就感的事情扩充:
SQL 优化一般步骤:
通过慢查询日志等定位那些执行效率低的 SQL 语句explain 分析 SQL 的执行计划show profile 分析trace确定问题并采取响应的措施
优化索引优化 SQL 语句:修改 SQL、IN 查询分段、时间查询分段、基于上一次数据过滤改用其他实现方式:ES、数仓等数据碎片处理
18、反问这里我粗略的说了下,我初次接触 Java 做的项目,帮助同学,让我对 Java 产生兴趣
以及接触 JavaWeb,逐渐明确自己的方向
19、总结略
整个面试持续了 半个小时,过程中,还是犯了很多错误,比如:对一些比较基础的知识点缺乏巩固,导致很多名词卡住,回答不上来。但还是有做的好的一面,可以较为灵活的处理面试过程中的各种问题,不让面试官感到不愉快。面试官人很好,在我回答不出来的时候,会给我一点提示,让我能圆满的把知识点说完。
对我来说,这是第一次正经的面试,比如:说话语速过快,回答问题逻辑跳跃比较大,部分问题衔接不流畅等等。最重要的还是一定要灵活应对面试过程中的各种突然情况,就我个人而言,这次面试过程中,有一些知识点我是知道的,可是突然就是想不起来了,但是你不能一直卡着去想这个知识,毕竟面试官的时间是宝贵的。这个时候,就要灵活的把你想表达的意思传递给对方。或者,直接摊牌,我不会,哈哈哈。
明明是一个小时的笔试,最后用了半个小时就面完了,当时面完了,心里还感觉要凉了,脑子一片空白,完全回忆不起刚刚半个小时我说了什么、干了什么。浑浑噩噩的突然就收到了通知我二面的电话,整个人惊魂未定的。现在想想,面试的时候为了缓解我的紧张,我说话的语速一直都是非常快的,这确实有点为难面试官了。
加油!继续努力!



