一. Spring
1. 谈谈你对Spring的理解
关键点
- 企业框架,目前最流行,没有之一
- AOP、IOC、Spring MVC
2. Spring中用到了哪些设计模式
- 工厂模式,比如 BeanFactory
- 代理模式,在Aop实现中用到了JDK的动态代理
- 单例模式,Bean的创建默认就是单利的
3. IoC的启动过程
- Resource文件的定位,即找到bean的配置文件
- 通过特定的reader解析该bean配置文件,抽象成beanDefinition类
- 将beanDefinition向容器注册,写入到一个大的HashMap中
4. BeanFactory 和 ApplicationContext 区别
- 功能,BeanFactory负责读取bean配置,管理bean的加载,实例化,维护bean之间的依赖关系,负责bean的声明周期;ApplicationContext除了提供上述BeanFactory所能提供的功能之外,还提供了国际化支持、资源访问、事件传递、队Web的支持等功能
- BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化;而ApplicationContext则相反,它是在容器启动时,一次性创建了所有的Bean。
- BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册
5. Bean 的生命周期
- 实例化一个Bean,也就是我们通常说的new
- 按照Spring上下文对实例化的Bean进行配置,也就是IOC注入
- 如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的是Spring配置文件中Bean的ID
- 如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(),传递的是Spring工厂本身(可以用这个方法获取到其他Bean)
- 如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文,该方式同样可以实现步骤4,但比4更好,以为ApplicationContext是BeanFactory的子接口,有更多的实现方法
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用After方法,也可用于内存或缓存技术
- 如果这个Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法
- 如果这个Bean关联了BeanPostProcessor接口,将会调用postAfterInitialization(Object obj, String s)方法 注意:以上工作完成以后就可以用这个Bean了,那这个Bean是一个single的,所以一般情况下我们调用同一个ID的Bean会是在内容地址相同的实例
- 当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean接口,会调用其实现的destroy方法
- 最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
6. Bean的作用域
- singleton(默认): 在Spring的IoC容器中只存在一个对象实例,所有该对象的引用都共享这个实例。Spring 容器只会创建该bean定义的唯一实例,这个实例会被保存到缓存中,并且对该bean的所有后续请求和引用都将返回该缓存中的对象实例,一般情况下,无状态的bean使用该scope。
- prototype:每次对该bean的请求都会创建一个新的实例,一般情况下,有状态的bean使用该scope。
- request:每次http请求将会有各自的bean实例,类似于prototype。
- session:在一个http session中,一个bean定义对应一个bean实例。
- global session:在一个全局的http session中,一个bean定义对应一个bean实例。典型情况下,仅在使用portlet context的时候有效。
7. AOP
实现原理:默认,接口基于JDK动态代理,类为cglib
注意事项
- 嵌套失效问题
- final类直接报错问题(动态代理的实现原理)
8. Spring MVC
请求过程
- 用户请求DispatchServlet
- DispatchServlet根据请求路径调用具体HandlerMapping返回一个HandlerExcutionChain
- DispatchServlet调用HandlerAdapter适配器
- HandlerAdapter调用具体的Handler处理业务
- Handler处理结束返回一个具体的ModelAndView给适配器
- 适配器将ModelAndView给DispatchServlet
- DispatchServlet把视图名称给ViewResolver视图解析器
- ViewResolver返回一个具体的视图给DispatchServlet
- 渲染视图,展示给用户
二. JVM
1. 内存划分
JVM规范,将内存分为 程序计数器、Java栈,也叫虚拟机栈、本地方法栈、方法区、堆
- 程序计数器 程序指令保存,从当前指令到下一个指令,从程序计数器获取下一个指令的地址,直到执行所有的指令;线程私有
- Java栈(虚拟机栈) 保存方法栈帧,当调用一个方法,则新创建一个栈帧,当前的方法始终保持在栈帧的顶部;线程私有
- 本地方法栈 保存本地方法栈帧,当调用一个本地方法,则新创建一个栈帧,当前的方法始终保持在栈帧的顶部;线程私有
- 方法区 保存类信息、静态常量、常量;线程共享
- 堆 保存对象,最最主要的垃圾收集之处;线程共享
2. 对象存活
- 引用计数算法:对对象引用进行计数,引用则计数器 +1,引用失效 -1;计数为 0 时候认为不在引用,可以进行回收;不能处理对象循环引用问题
- 可达性分析算法:主要采取此方式,采用虚拟机栈、类引用对象、常量对象、本地方法引用对象作为根,判断对象到根是否存在引用关系,不可达则认为不在引用,可以进行回收;
3. GC回收算法
- 标记-清除算法:对要回收的对象,先进行标志,后进行清除,你懂得;但是,久之,会存在内存不连续,比如,一次垃圾回收,回收了,(0,1)和(0,3)和(0,5)三个位置,但是没有回收(0,2)和(0,4),那下次的内存,就无法使用(0,1)-(0,5)的连续空间;
- 复制算法:为改进上面的内存碎片问题而产生,对内存分为两部分,交替回收其中一部分,存活的对象复制到另一部分空间,你懂得;但是,有点浪费,比如,内存分为,(0,1)-(0,3)和(0,3)-(0,5)两个部分,某次回收(0,1)-(0,3)空间,将存活对象拷贝到(0,3)-(0,5),而后在(0,1)-(0,3)分配对象;
- 标记-整理算法:为改进上面的内存浪费问题而产生,对要回收的对象,先进行标志,后进行清除,你懂得,然后,将存活的对象,进行整理,移动到边界位置,似的剩余空间连续;比如,一次垃圾回收,回收了,(0,1)和(0,3)和(0,5)三个位置,但是没有回收(0,2)和(0,4),然后,将(0,2)和(0,4)移动到(0,1)和(0,2),使得(0,3)-(0,5)的空间连续;
4. 类加载过程
jvm中class类的加载过程,大致分为这几个步骤
- 加载(load)
- 根据全类名,加载类的二进制字节流
- 将字节流转存方法区
- 生成Class对象作为访问入口
- 验证(verify)
- class文件的格式验证,验证是否符合JVM规范
- class中的元数据验证,验证是否符合Java规范
- class的字节码验证,验证数据流控制流不会危害JVM环境
- 准备(prepare)
- 给变量分配内存
- 初始化零值(比如int默认为0,boolean默认为false)
- final变量直接赋值
- 解析
- 符号引用变为直接引用
- 类、字段、方法、接口方法解析
- 初始化
- 初始化变量
- 构造函数
- static块
5. 双亲委派机制
Java中,大概有三种类型加载器,启动类加载器(Bootstrap)<- 标准扩展类加载器(Extension)<- 应用程序类加载器(Application )<- 上下文类加载器(Custom),从右到左,尽量父类进行加载,当父类无法进行加载时候,才会使用子类进行加载
- 意义
- 防止同一个JVM,内存中出现两份class二进制字节码
- 加载过程
- 从已加载的类查找是否已经存在,存在不需要再次加载
- 若不存在,则去parent中查找,存在不需要再次加载
- 若不存在,递归在parent中查找,直到找到为止
- 若找遍所有parent均不存在,且当前加载器已经没有parent加载器,则调用当前类加载器的findClass方法,如果能加载,结束
- 如果不能,则递归返回child类加载器,继续调用findClass方法,如果能加载,结束
- 如果找遍所有child的findClass方法,还是不能加载,则抛出异常
- 破坏双亲委派机制
- 将parent设为null
- 重写load(String,boolean)方法,改变类的查找机制。
三. 多线程
1. 死锁的四个条件
- 互斥
- 请求与保持
- 不剥夺
- 循环等待
2. 检查死锁
- Jconsole查看死锁
- Jstack查看死锁
3. volatile
- JMM
- 内存可见性
- 防止指令重排序
- 不能保证原子性
4. synchronized
作用
- 互斥访问
- 内存可见性
- 防止指令重排序
用法
- 修饰普通方法
- 修饰静态方法
- 修饰代码块
注意点
- 当一个线程在访问对象的 synchronized 方法,因为对象只有一把锁,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法
- 实现原理为,对象监视器,Monitor
5. volatile vs synchronized vs lock
- 来源差异:volatile、synchronized为Java关键字; lock是Java类
- 代价开销:volatile不是锁,代价最小; lock是一般基于AQS,相对比synchronized代价小; synchronized代价最大
- 简单性:volatile、synchronized为Java关键字,JVM全权帮忙维护,只要我们能正确使用,不需要我们太多关心维护; lock是Java类,有很多方法可以调用,灵活性最好,但是需要自己控制锁的获取、释放
6. 进程间通信
- 管道pipe
- 命名管道FIFO
- 消息队列MessageQueue
- 共享存储SharedMemory
- 信号量Semaphore
- 套接字Socket
- 信号 ( sinal )
7. 原子操作类
-
CountDownLatch:
- 某线程需要等待多个线程执行完毕,再执行。
- 实现多个线程共同等待,同时开始执行任务,不可重用。(此类似于CyclicBarrier,可重用)
-
CyclicBarrier:
- CyclicBarrier允许一组线程互相等待,直到到达某个公共屏障点。
- 与CountDownLatch不同的是该barrier在释放等待线程后可以重用,所以称它为循环(Cyclic)的屏障
-
CountDownLatch、CyclicBarrier差别:
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同: CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行; 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
- CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
-
Semaphore:
- Semaphore是一个计数信号量,它的本质是一个”共享锁”。
- 信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
- Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。
-
Exchanger:
- 用于进行线程间的数据交换。
- 两个线程通过exchange方法交换数据
- 该工具类的线程对象是成对的
-
ThreadLocal:
- 每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
- ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
- ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
- ThreadLocal里面使用了一个存在弱引用的map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例。这个Map的确使用了弱引用,不过弱引用只是针对key。每个key都弱引用指向threadlocal。 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。 但是,我们的value却不能回收,而这块value永远不会被访问到了,所以存在着内存泄露。因为存在一条从current thread连接过来的强引用。只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是将调用threadlocal的remove方法,这也是等会后边要说的。
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。
写在最后
限于篇幅,本文只收录Spring、JVM、多线程的部分面试题;完整的面试题包含Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技。笔者已经整理打包好了!
需要的朋友点击下方传送门, 即可免费领取面试资料和视频学习资料
传送门
以下是部分面试题截图