

1、看以下代码回答问题(⼀)
答案:

2、看以下代码回答问题(⼆)

答案:
1. s1 == s2为false
2. s2 == s3为true
String对象的intern⽅法,⾸先会检查字符串常量池中是否存在"abc",如果存在则返回该字符串引⽤,
如果不存在,则把"abc"添加到字符串常量池中,并返回该字符串常量的引⽤。
三、看以下代码回答问题(三)

答案:
1. i1 == i2为true
2. i3 == i4为false
在Interger类中,存在⼀个静态内部类IntegerCache, 该类中存在⼀个Integer cache[], 并且存在⼀
个static块,会在加载类的时候执⾏,会将-128⾄127这些数字提前⽣成Integer对象,并缓存在cache数
组中,当我们在定义Integer数字时,会调⽤Integer的valueOf⽅法,valueOf⽅法会判断所定义的数字
是否在-128⾄127之间,如果存在则直接从cache数组中获取Integer对象,如果超过,则⽣成⼀个新的
Integer对象。
4、String、StringBuffer、StringBuilder的区别
1. String是不可变的,如果尝试去修改,会新⽣成⼀个字符串对象,StringBuffer和StringBuilder是
可变的
2. StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效
率会更⾼
5、ArrayList和LinkedList有哪些区别
1. ⾸先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实
现的
2. 由于底层数据结构不同,他们所适⽤的场景也不同,ArrayList更适合随机查找,LinkedList更适合
删除和添加,查询、添加、删除的时间复杂度不同
3. 另外ArrayList和LinkedList都实现了List接⼝,但是LinkedList还额外实现了Deque接⼝,所以
LinkedList还可以当做队列来使⽤
6、CopyOnWriteArrayList的底层原理是怎样的
1. ⾸先CopyOnWriteArrayList内部也是⽤过数组来实现的,在向CopyOnWriteArrayList添加元素
时,会复制⼀个新的数组,写操作在新数组上进⾏,读操作在原数组上进⾏
2. 并且,写操作会加锁,防⽌出现并发写⼊丢失数据的问题
3. 写操作结束之后会把原数组指向新数组
4. CopyOnWriteArrayList允许在写操作时来读取数据,⼤⼤提⾼了读的性能,因此适合读多写少的应
⽤场景,但是CopyOnWriteArrayList会⽐较占内存,同时可能读到的数据不是实时最新的数据,所
以不适合实时性要求很⾼的场景
7、HashMap的扩容机制原理
1.7版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表上的每个元素
3. 取每个元素的key,并基于新数组⻓度,计算出每个元素在新数组中的下标
4. 将元素添加到新数组中去
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
1. 先⽣成新数组
2. 遍历⽼数组中的每个位置上的链表或红⿊树
3. 如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
4. 如果是红⿊树,则先遍历红⿊树,先计算出红⿊树中每个元素对应在新数组中的下标位置
a. 统计每个下标位置的元素个数
b. 如果该位置下的元素个数超过了8,则⽣成⼀个新的红⿊树,并将根节点的添加到新数组的对
应位置
c. 如果该位置下的元素个数没有超过8,那么则⽣成⼀个链表,并将链表的头节点添加到新数组
的对应位置
5. 所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
8、ConcurrentHashMap的扩容机制
1.7版本
1. 1.7版本的ConcurrentHashMap是基于Segment分段实现的
2. 每个Segment相对于⼀个⼩型的HashMap
3. 每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
64. 先⽣成新的数组,然后转移元素到新数组中
5. 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
1. 1.8版本的ConcurrentHashMap不再基于Segment实现
2. 当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
3. 如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然
后判断是否超过阈值,超过了则进⾏扩容
4. ConcurrentHashMap是⽀持多个线程同时扩容的
5. 扩容之前也先⽣成⼀个新的数组
6. 在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或
多组的元素转移⼯作
9、ThreadLocal的底层原理
1. ThreadLocal是Java中所提供的线程本地存储机制,可以利⽤该机制将数据缓存在某个线程内部,
该线程可以在任意时刻、任意⽅法中获取缓存的数据
2. ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对
象)中都存在⼀个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的
值
3. 如果在线程池中使⽤ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使⽤完之后,应该要
把设置的key,value,也就是Entry对象进⾏回收,但线程池中的线程不会回收,⽽线程对象是通过
强引⽤指向ThreadLocalMap,ThreadLocalMap也是通过强引⽤指向Entry对象,线程不被回收,
Entry对象也就不会被回收,从⽽出现内存泄漏,解决办法是,在使⽤了ThreadLocal对象之后,⼿
动调⽤ThreadLocal的remove⽅法,⼿动清楚Entry对象
4. ThreadLocal经典的应⽤场景就是连接管理(⼀个线程持有⼀个连接,该连接对象可以在不同的⽅
法之间进⾏传递,线程之间不共享同⼀个连接)

10、如何理解volatile关键字
在并发领域中,存在三⼤特性:原⼦性、有序性、可⻅性。volatile关键字⽤来修饰对象的属性,在并发
环境下可以保证这个属性的可⻅性,对于加了volatile关键字的属性,在对这个属性进⾏修改时,会直接
将CPU⾼级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从⽽保证了可⻅
性,底层是通过操作系统的内存屏障来实现的,由于使⽤了内存屏障,所以会禁⽌指令重排,所以同时
也就保证了有序性,在很多并发场景下,如果⽤好volatile关键字可以很好的提⾼执⾏效率。
11、ReentrantLock中的公平锁和⾮公平锁的底层实现
⾸先不管是公平锁和⾮公平锁,它们的底层实现都会使⽤AQS来进⾏排队,它们的区别在于:线程在使
⽤lock()⽅法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,
则当前线程也进⾏排队,如果是⾮公平锁,则不会去检查是否有线程在排队,⽽是直接竞争锁。
不管是公平锁还是⾮公平锁,⼀旦没竞争到锁,都会进⾏排队,当锁释放时,都是唤醒排在最前⾯的线
程,所以⾮公平锁只是体现在了线程加锁阶段,⽽没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重⼊锁,不管是公平锁还是⾮公平锁都是可重⼊的。

12、ReentrantLock中tryLock()和lock()⽅法的区别
1. tryLock()表示尝试加锁,可能加到,也可能加不到,该⽅法不会阻塞线程,如果加到锁则返回
true,没有加到则返回false
2. lock()表示阻塞加锁,线程会阻塞直到加到锁,⽅法也没有返回值
13、CountDownLatch和Semaphore的区别和底层原理
CountDownLatch表示计数器,可以给CountDownLatch设置⼀个数字,⼀个线程调⽤
CountDownLatch的await()将会阻塞,其他线程可以调⽤CountDownLatch的countDown()⽅法来对
CountDownLatch中的数字减⼀,当数字被减成0后,所有await的线程都将被唤醒。
对应的底层原理就是,调⽤await()⽅法的线程会利⽤AQS排队,⼀旦数字被减为0,则会将AQS中
排队的线程依次唤醒。
Semaphore表示信号量,可以设置许可的个数,表示同时允许最多多少个线程使⽤该信号量,通
过acquire()来获取许可,如果没有许可可⽤则线程阻塞,并通过AQS来排队,可以通过release()
9⽅法来释放许可,当某个线程释放了某个许可后,会从AQS中正在排队的第⼀个线程开始依次唤
醒,直到没有空闲许可。
14、Sychronized的偏向锁、轻量级锁、重量级锁
1. 偏向锁:在锁对象的对象头中记录⼀下当前获取到该锁的线程ID,该线程下次如果⼜来获取该锁就
可以直接获取到了
2. 轻量级锁:由偏向锁升级⽽来,当⼀个线程获取到锁后,此时这把锁是偏向锁,此时如果有第⼆个
线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻
量级锁底层是通过⾃旋来实现的,并不会阻塞线程
3. 如果⾃旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
4. ⾃旋锁:⾃旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就⽆所谓唤醒线程,阻塞和唤醒
这两个步骤都是需要操作系统去进⾏的,⽐较消耗时间,⾃旋锁是线程通过CAS获取预期的⼀个标
记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程⼀直在运
⾏中,相对⽽⾔没有使⽤太多的操作系统资源,⽐较轻量。
15、Sychronized和ReentrantLock的区别
1. sychronized是⼀个关键字,ReentrantLock是⼀个类
2. sychronized会⾃动的加锁与释放锁,ReentrantLock需要程序员⼿动加锁与释放锁
3. sychronized的底层是JVM层⾯的锁,ReentrantLock是API层⾯的锁
4. sychronized是⾮公平锁,ReentrantLock可以选择公平锁或⾮公平锁
5. sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识
来标识锁的状态
6. sychronized底层有⼀个锁升级的过程
16、线程池的底层⼯作原理
线程池内部是通过队列+线程实现的,当我们利⽤线程池执⾏任务时:
1. 如果此时线程池中的线程数量⼩于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建
新的线程来处理被添加的任务。
2. 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放⼊
缓冲队列。
3. 如果此时线程池中的线程数量⼤于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数
量⼩于maximumPoolSize,建新的线程来处理被添加的任务。
104. 如果此时线程池中的线程数量⼤于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等
于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
5. 当线程池中的线程数量⼤于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被
终⽌。这样,线程池可以动态的调整池中的线程数
17、JVM中哪些是线程共享区
堆区和⽅法区是所有线程共享的,栈、本地⽅法栈、程序计数器是每个线程独有的

18、JVM中哪些可以作为gc root
什么是gc root,JVM在进⾏垃圾回收时,需要找到“垃圾”对象,也就是没有被引⽤的对象,但是直接
找“垃圾”对象是⽐较耗时的,所以反过来,先找“⾮垃圾”对象,也就是正常对象,那么就需要从某
些“根”开始去找,根据这些“根”的引⽤路径找到正常对象,⽽这些“根”有⼀个特征,就是它只会引⽤其
他对象,⽽不会被其他对象引⽤,例如:栈中的本地变量、⽅法区中的静态变量、本地⽅法栈中的变
量、正在运⾏的线程等可以作为gc root。
19、你们项⽬如何排查JVM问题
对于还在正常运⾏的系统:
1. 可以使⽤jmap来查看JVM中各个区域的使⽤情况
2. 可以通过jstack来查看线程的运⾏情况,⽐如哪些线程阻塞、是否出现了死锁
3. 可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc⽐较频繁,那么就得进⾏
调优了
4. 通过各个命令的结果,或者jvisualvm等⼯具来进⾏分析
5. ⾸先,初步猜测频繁发送fullgc的原因,如果频繁发⽣fullgc但是⼜⼀直没有出现内存溢出,那么表
示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免
这些对象进⼊到⽼年代,对于这种情况,就要考虑这些存活时间不⻓的对象是不是⽐较⼤,导致年
轻代放不下,直接进⼊到了⽼年代,尝试加⼤年轻代的⼤⼩,如果改完之后,fullgc减少,则证明
修改有效
6. 同时,还可以找到占⽤CPU最多的线程,定位到具体的⽅法,优化这个⽅法的执⾏,看是否能避免
某些对象的创建,从⽽节省内存
对于已经发⽣了OOM的系统:
1. ⼀般⽣产系统中都会设置当系统发⽣了OOM时,⽣成当时的dump⽂件(
-
XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
2. 我们可以利⽤jsisualvm等⼯具来分析dump⽂件
3. 根据dump⽂件找到异常的实例对象,和异常的线程(占⽤CPU⾼),定位到具体的代码
4. 然后再进⾏详细的分析和调试
总之,调优不是⼀蹴⽽就的,需要分析、推理、实践、总结、再分析,最终定位到具体的问题
20、说说类加载器双亲委派模型
JVM中存在三个默认的类加载器:
1. BootstrapClassLoader
2. ExtClassLoader
3. AppClassLoader
AppClassLoader的⽗加载器是ExtClassLoader,ExtClassLoader的⽗加载器是
BootstrapClassLoader。
JVM在加载⼀个类时,会调⽤AppClassLoader的loadClass⽅法来加载这个类,不过在这个⽅法中,会
先使⽤ExtClassLoader的loadClass⽅法来加载类,同样ExtClassLoader的loadClass⽅法中会先使⽤
BootstrapClassLoader来加载类,如果BootstrapClassLoader加载到了就直接成功,如果
BootstrapClassLoader没有加载到,那么ExtClassLoader就会⾃⼰尝试加载该类,如果没有加载到,
那么则会由AppClassLoader来加载这个类。
所以,双亲委派指得是,JVM在加载类时,会委派给Ext和Bootstrap进⾏加载,如果没加载到才由⾃⼰
进⾏加载。
21、Tomcat中为什么要使⽤⾃定义类加载器
⼀个Tomcat中可以部署多个应⽤,⽽每个应⽤中都存在很多类,并且各个应⽤中的类是独⽴的,全类
名是可以相同的,⽐如⼀个订单系统中可能存在com.zhouyu.User类,⼀个库存系统中可能也存在
com.zhouyu.User类,⼀个Tomcat,不管内部部署了多少应⽤,Tomcat启动之后就是⼀个Java进程,
也就是⼀个JVM,所以如果Tomcat中只存在⼀个类加载器,⽐如默认的AppClassLoader,那么就只能
加载⼀个com.zhouyu.User类,这是有问题的,⽽在Tomcat中,会为部署的每个应⽤都⽣成⼀个类加载
器实例,名字叫做WebAppClassLoader,这样Tomcat中每个应⽤就可以使⽤⾃⼰的类加载器去加载⾃
⼰的类,从⽽达到应⽤之间的类隔离,不出现冲突。另外Tomcat还利⽤⾃定义加载器实现了热加载功
能。
22、Tomcat如何进⾏优化?
对于Tomcat调优,可以从两个⽅⾯来进⾏调整:内存和线程。
⾸先启动Tomcat,实际上就是启动了⼀个JVM,所以可以按JVM调优的⽅式来进⾏调整,从⽽达到
Tomcat优化的⽬的。
另外Tomcat中设计了⼀些缓存区,⽐如appReadBufSize、bufferPoolSize等缓存区来提⾼吞吐量。
还可以调整Tomcat的线程,⽐如调整minSpareThreads参数来改变Tomcat空闲时的线程数,调整
maxThreads参数来设置Tomcat处理连接的最⼤线程数。
并且还可以调整IO模型,⽐如使⽤NIO、APR这种相⽐于BIO更加⾼效的IO模型。
23、浏览器发出⼀个请求到收到响应经历了哪些步骤?
1. 浏览器解析⽤户输⼊的URL,⽣成⼀个HTTP格式的请求
2. 先根据URL域名从本地hosts⽂件查找是否有映射IP,如果没有就将域名发送给电脑所配置的DNS进
⾏域名解析,得到IP地址
3. 浏览器通过操作系统将请求通过四层⽹络协议发送出去
4. 途中可能会经过各种路由器、交换机,最终到达服务器
5. 服务器收到请求后,根据请求所指定的端⼝,将请求传递给绑定了该端⼝的应⽤程序,⽐如8080被
13tomcat占⽤了
6. tomcat接收到请求数据后,按照http协议的格式进⾏解析,解析得到所要访问的servlet
7. 然后servlet来处理这个请求,如果是SpringMVC中的DispatcherServlet,那么则会找到对应的
Controller中的⽅法,并执⾏该⽅法得到结果
8. Tomcat得到响应结果后封装成HTTP响应的格式,并再次通过⽹络发送给浏览器所在的服务器
9. 浏览器所在的服务器拿到结果后再传递给浏览器,浏览器则负责解析并渲染
24、跨域请求是什么?有什么问题?怎么解决?
跨域是指浏览器在发起⽹络请求时,会检查该请求所对应的协议、域名、端⼝和当前⽹⻚是否⼀致,如
果不⼀致则浏览器会进⾏限制,⽐如在www.baidu.com的某个⽹⻚中,如果使⽤ajax去访问
www.jd.com是不⾏的,但是如果是img、iframe、script等标签的src属性去访问则是可以的,之所以浏
览器要做这层限制,是为了⽤户信息安全。但是如果开发者想要绕过这层限制也是可以的:
1. response添加header,⽐如resp.setHeader("Access-Control-Allow-Origin", "*");表示可以访问
所有⽹站,不受是否同源的限制
2. jsonp的⽅式,该技术底层就是基于script标签来实现的,因为script标签是可以跨域的
3. 后台⾃⼰控制,先访问同域名下的接⼝,然后在接⼝中再去使⽤HTTPClient等⼯具去调⽤⽬标接⼝
4. ⽹关,和第三种⽅式类似,都是交给后台服务来进⾏跨域访问
25、Spring中的Bean创建的⽣命周期有哪些步骤
Spring中⼀个Bean的创建⼤概分为以下⼏个步骤:
1. 推断构造⽅法
2. 实例化
3. 填充属性,也就是依赖注⼊
4. 处理Aware回调
5. 初始化前,处理@PostConstruct注解
6. 初始化,处理InitializingBean接⼝
7. 初始化后,进⾏AOP
当然其实真正的步骤更加细致,可以看下⾯的流程图
26、Spring中Bean是线程安全的吗
Spring本身并没有针对Bean做线程安全的处理,所以:
1. 如果Bean是⽆状态的,那么Bean则是线程安全的
2. 如果Bean是有状态的,那么Bean则不是线程安全的
另外,Bean是不是线程安全,跟Bean的作⽤域没有关系,Bean的作⽤域只是表示Bean的⽣命周期范
围,对于任何⽣命周期的Bean都是⼀个对象,这个对象是不是线程安全的,还是得看这个Bean对象本
身。
27、ApplicationContext和BeanFactory有什么区别
BeanFactory是Spring中⾮常核⼼的组件,表示Bean⼯⼚,可以⽣成Bean,维护Bean,⽽
ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也
是⼀个Bean⼯⼚,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如
EnvironmentCapable、MessageSource、ApplicationEventPublisher等接⼝,从⽽
ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的
28、Spring中的事务是如何实现的
151. Spring事务底层是基于数据库事务和AOP机制的
2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean
3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解
4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接
5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮
常重要的⼀步
6. 然后执⾏当前⽅法,⽅法中会执⾏sql
7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务
8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
9. Spring事务的隔离级别对应的就是数据库的隔离级别
10. Spring事务的传播机制是Spring事务⾃⼰实现的,也是Spring事务中最复杂的
11. Spring事务的传播机制是基于数据库连接来做的,⼀个数据库连接⼀个事务,如果传播机制配置为
需要新开⼀个事务,那么实际上就是先建⽴⼀个数据库连接,在此新数据库连接上执⾏sql


本文详细介绍了Java后端开发面试中常见的面试题,包括String对象的intern方法、Integer对象的缓存机制、StringBuffer与StringBuilder的区别、ArrayList与LinkedList的不同、CopyOnWriteArrayList的工作原理、HashMap的扩容机制、ConcurrentHashMap的并发扩容、ThreadLocal的底层原理、volatile关键字的作用、ReentrantLock的公平锁与非公平锁、线程池的工作原理、JVM的内存区域、类加载器的双亲委派模型、Tomcat的类加载优化及请求处理流程。此外,还讨论了跨域请求的解决方案和Spring Bean的生命周期。这些问题涵盖了Java性能优化、并发编程、面试准备等多个重要领域。
755

被折叠的 条评论
为什么被折叠?



