常见大厂面试题(持续更新中)

本文整理了阿里Java面试常见问题,涵盖ArrayList与LinkedList区别、HashMap的put方法、ThreadLocal、JVM问题排查、Spring框架等内容。详细介绍了各知识点原理、实现机制及JDK不同版本的变化,还提及线程通讯、事务机制等,为Java开发者面试提供参考。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常见大厂面试题

1. 阿里一面:说一下ArrayList和LinkedList区别

  1. 首先他们底层的数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的。
  2. 由于底层数据结构不同,他们所适用的场景也不同,ArrayList更适合随机查找,LinkedList更适合删除和添加,查询、添加、删除的时间复杂度不同。
  3. 另外ArrayList和LinkedList都实现了List接口,但是LinkedList还额外实现了Deque接口,所以LinkedList还可以当做双端队列来使用。
  4. ArrayList因为底层是数组结构,他在做查询的时候,可以直接根据索引查询 所以查询速度会很快,但是LinkedList因为他是链表结构的.他在查询的时候会使用对半查找(判断离最后一位的距离)所以会遍历查询相对事多比较慢。
  5. ArrayList因为是数组结构.他在执行添加或者删除的时候,可能会涉及到数组扩容的情况,所以相对会执行较长的时间,LinkedList只需要把数据添加到链表的首位就可以了(如果涉及到下标添加的话也会涉及到遍历添加,具体速度的快慢的话也要看插入的位置)。

2. 阿里一面:说一下HashMap的put方法

先说HashMap的Put方法的大体流程:

  1. 根据key通过哈希算法与与运算得出数组下标

  2. 如果数组下标位置元素为空,则将key和value封装为Entity对象(JDK1.7中是Entity对象,JDK1.8中时Node对象)并放入该位置

  3. 如果数组下标位置元素不为空,则要分情况讨论

    a. 如果是JDK1.7,则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entity对象,并使用头插法(注1)添加到当前位置的链表中

    b. 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node,还是链表Node

    i. 如果是红黑树Node,则将key和value封装为一个红黑树节点 并添加到红黑树中去,在这个过程中会判断红黑树中是否存在当前key,如果存在则更新value

    ii.如果此位置上的Node对象时链表节点,则将key和value封装为一个链表Node并通过尾插法(注2)插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中,会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果大于等于8,name则会将该链表转成红黑树

    iii.将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就不扩容。
    注1:
    头插法指每次将所指节点插入在链表的最前端。新来的值会取代原有的值,原有的值就顺推到链表中去。之所以JDK1.7的之前所使用的是头插法是因为作者认为后来插入的值被查找的可能性更大一点,提升查找的效率。
    注2:
    尾插法指每次将所指节点插入在链表的最末端。避免出现环形链表,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。

3. 阿里一面:说一下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经典的应用场景就是连接管理(一个线程持有一个链接,该链接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)

4. 阿里一面:说一下JVM中,哪些是共享区,哪些可以作为GC ROOT?

  1. 堆区和方法区是所有线程共享。栈、本地方法栈、程序计数器是每个线程独有的。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PcRk5OmF-1623914385685)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20210617102020455.png)]

  2. 什么是gc root,JVM在进行垃圾回收时,需要找到"垃圾"对象,也就是没有被引用的对象,但是直接找"垃圾"对象时比较耗时的,所以反过来,先找"非垃圾"对象,也就是正常对象,name就需要从某些"根"开始去找,根据这些"根"的引用路径找到正常对象,而这些"根"有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。

5.阿里一面:你们项目如何排查JVM问题

对于还在正常运行的系统:

  1. 可以使用jmap(注1)来查看JVM中各个区域的使用情况

  2. 可以通过jstack(注2)来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁

  3. 可以通过jstat(注3)命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,name就得进行调优了

  4. 通过各个命令的结果,或者jvisualvm等工具来进行分析

  5. 首先,初步猜测频繁发送fullgc的原因,如果频繁发送fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在yonggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效

  6. 同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存

对于已经发了OOM的系统:

  1. 一般生成系统中都会设置当系统发生了OOM时,生成当时的dump文件(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
  2. 我们可以利用jsisualvm等工具来分析dump文件
  3. 根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
  4. 然后再进行详细的分析和调试

注1:
jmap常用命令:
jmap -J-d64 -heap pid(pid为java进程的pid)
返回结果:
Heap Configuration: 堆配置情况,也就是JVM参数配置的结果[平常说的tomcat配置JVM参数,就是在配置这些]
MinHeapFreeRatio = 40 最小堆使用比例
MaxHeapFreeRatio = 70 最大堆可用比例
MaxHeapSize = 2147483648 (2048.0MB) 最大堆空间大小
NewSize = 805306368 (768.0MB) 新生代分配大小
MaxNewSize = 805306368 (768.0MB) 最大可新生代分配大小
OldSize = 1342177280 (1280.0MB) 老年代大小
NewRatio = 2 新生代比例
SurvivorRatio = 8 新生代与suvivor的比例
MetaspaceSize = 134217728 (128.0MB) 元空间大小
CompressedClassSpaceSize = 327155712 (312.0MB) 压缩类空间大小
MaxMetaspaceSize = 335544320 (320.0MB) 最大元空间大小
G1HeapRegionSize = 0 (0.0MB) G1堆内存中一个Region

Heap Usage: 堆使用情况【堆内存实际的使用情况】
New Generation (Eden + 1 Survivor Space): 新生代(伊甸区Eden区 + 幸存区survior(1+2)空间)
capacity = 724828160 (691.25MB) 伊甸区容量
used = 598756568 (571.0187606811523MB) 已经使用大小
free = 126071592 (120.23123931884766MB) 剩余容量
82.60669232277068% used 使用比例
Eden Space: 伊甸区
capacity = 644349952 (614.5MB) 伊甸区容量
used = 590824704 (563.454345703125MB) 伊甸区使用
free = 53525248 (51.045654296875MB) 伊甸区当前剩余容量
91.69314006560212% used 伊甸区使用情况
From Space: survior1区
capacity = 80478208 (76.75MB) survior1区容量
used = 7931864 (7.564414978027344MB) surviror1区已使用情况
free = 72546344 (69.18558502197266MB) surviror1区剩余容量
9.855915280817387% used #survior1区使用比例
To Space: survior2区
capacity = 80478208 (76.75MB) survior2区容量
used = 0 (0.0MB) survior2区已使用情况
free = 80478208 (76.75MB) survior2区剩余容量
0.0% used survior2区使用比例
concurrent mark-sweep generation: CMS垃圾回收器区
capacity = 1342177280 (1280.0MB) CMS垃圾回收器区容量
used = 127491768 (121.58562469482422MB) CMS垃圾回收器区已使用容量
free = 1214685512 (1158.4143753051758MB) CMS垃圾回收器区剩余容量
9.498876929283142% used CMS垃圾回收器区使用比例
注2:
在dump中,线程一般存在如下几种状态:
1、RUNNABLE,线程处于执行中
2、BLOCKED,线程被阻塞
3、WAITING,线程正在等待
注3:
jstat -gc pid 命令
显示列名

具体描述

S0C

年轻代中第一个survivor(幸存区)的容量 (字节)

S1C

年轻代中第二个survivor(幸存区)的容量 (字节)

S0U

年轻代中第一个survivor(幸存区)目前已使用空间 (字节)

S1U

年轻代中第二个survivor(幸存区)目前已使用空间 (字节)

EC

年轻代中Eden(伊甸园)的容量 (字节)

EU

年轻代中Eden(伊甸园)目前已使用空间 (字节)

OC

Old代的容量 (字节)

OU

Old代目前已使用空间 (字节)

PC

Perm(持久代)的容量 (字节)

PU

Perm(持久代)目前已使用空间 (字节)

YGC

从应用程序启动到采样时年轻代中gc次数

YGCT

从应用程序启动到采样时年轻代中gc所用时间(s)

FGC

从应用程序启动到采样时old代(全gc)gc次数

FGCT

从应用程序启动到采样时old代(全gc)gc所用时间(s)

GCT

从应用程序启动到采样时gc用的总时间(s)
jstat -gccapacity pid 命令
NGCMN

年轻代(young)中初始化(最小)的大小(字节)

NGCMX

年轻代(young)的最大容量 (字节)

NGC

年轻代(young)中当前的容量 (字节)

S0C

年轻代中第一个survivor(幸存区)的容量 (字节)

S1C

年轻代中第二个survivor(幸存区)的容量 (字节)

EC

年轻代中Eden(伊甸园)的容量 (字节)

OGCMN

old代中初始化(最小)的大小 (字节)

OGCMX

old代的最大容量(字节)

OGC

old代当前新生成的容量 (字节)

OC

Old代的容量 (字节)

PGCMN

perm代中初始化(最小)的大小 (字节)

PGCMX

perm代的最大容量 (字节)

PGC

perm代当前新生成的容量 (字节)

PC

Perm(持久代)的容量 (字节)

YGC

从应用程序启动到采样时年轻代中gc次数

FGC

从应用程序启动到采样时old代(全gc)gc次数

其他指令可查看 https://blog.csdn.net/zhaozheng7758/article/details/8623549

6.阿里一面:如何查看线程死锁

  1. 可以通过jstack命令来进行查看,jstack pid 命令会显示发生了死锁的线程

  2. 或者两个线程去操作数据库时发生了死锁,这时可以查询数据的死锁情况

    1、查询是否锁表
    show OPEN TABLES where IN_use >0;
    2、查询进程
    show processlist;
    3、查看正在锁的事务
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
    4、查看等待锁的事务
    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS
    

7.阿里一面:线程之间如何进行通讯的

  1. 线程之间可以通过共享内存或基于网络来进行通信
  2. 如果是通过共享内存来进行通信,则需要考虑并发问题,什么时候阻塞,什么时候唤醒
  3. 像Java中的wait()、notify()就是阻塞和唤醒
  4. 通过网络就比较简单了,通过网络连接将通信数据发送给对方,当然也要考虑到并发问题,处理方式就是加锁等方式

8.阿里一面:介绍一下Spring,读过源码介绍一下大致流程

  1. Spring是一个快速开发框架,Spring可以帮助程序员来管理对象

  2. 在创建Spring容器,也就是启动Spring时:

    a. 首先会进行扫描,扫描得到所有的BeanDefinition对象,并存入一个Map中

    b. 然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建

    c.利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中

  3. 单例Bean创建完了之后,Spring会发布一个容器启动时间

  4. Spring启动结束

  5. 在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BeanFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的

  6. 在Spring启动过程中还会去处理@Import等注解

9.阿里一面:说一下Spring的事务机制

1.Spring事务底层是基于数据库事务和AOP机制实现的
2.首先对于使用@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
3.当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
4.如果加了,name则利用事务管理器创建一个数据库连接
5.并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步
6.然后执行当前方法,方法中会执行sql
7.当sql都执行完后,如果没有出现异常就直接提交事务
8.如果出现了异常,并且这个异常时需要回滚的就会回滚事务,否则仍然提交事务
9.Spring事务的隔离级别对应的就是数据库的隔离级别
10.Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
11.Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql

10.阿里一面:什么时候@Transactional失效

因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以如果是被代理对象来调用这个方法,那么@Transactional是不会生效的。

同时如果某个方法是private的,那么@Transactional也会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactional失效。

11.阿里一面:还读过哪些框架源码介绍一下你还熟悉的

可以说Mybatis,SpringBoot,SpringCloud,消息队列等开发框架或者中间件,如果这些都不会的话.可以说说HashMap、线程池等JDK自带的源码

12.阿里二面:JDK1.7到JDK1.8 HashMap 发生了什么变化?

  1. 1.7中底层是数组+链表,1.8中底层是数组+链表+红黑树,加红黑树的目的是提高HashMap插入和查询整体的效率

  2. 1.7中链表插入使用的是头插法,1.8中链表插入使用的是尾插法,因为1.8中插入key和value时需要判断链表元素个数,所以需要遍历俩表元素个数,所以正好就直接使用了尾插法,同时也可以解决之前头插法存在的环形链表问题

  3. 1.7中哈希算法比较复杂,存在各种右移与异或运算,1.8中进行了简化,因为负载的哈希算法的目的就是提高三烈性,来提供HashMap的整体效率,而1.8中新增了红黑树,所以可以适当的简化哈希算法,节省CPU资源

13.阿里二面:JDK1.7到JDK1.8 java虚拟机发生了什么变化?

1.7中存在永久代,1.8中没有了永久代,替换它的是元空间,元空间锁占的内存不是在虚拟机内部,而是本地内存空间,这么做的原因是,不管是永久代还是元空间,他们都是方法区的具体时间,之所以元空间所占的内存改为本地内存,官方的说法是为了和JRockit统一,不过额外还有一些原因,比如方法区锁储存的类信息通畅是比较难确定的,所以对于方法区的大小是比较难指定的,太小了容易出现方法区的溢出,太大了又会占用了太多虚拟机的内存空间,而转移到本地内存后则不会影响到虚拟机所占用的内存了。

14.阿里二面:如何实现AOP,项目中哪些地方用到了AOP

利用动态搭理技术来实现AOP,比如JDK动态搭理,或Cglib动态搭理,利用动态搭理技术,可以针对某个类生成代理对象,当调用代理对象的某个方法时,可以任意控制该方法的执行,比如可以先打印执行时间,在执行方法,并且该方法执行完成后,再次打印执行时间。

项目中,比如事务,权限控制,方法执行时长日志都是通过AOP技术来实现的,凡是需要对某些方法做统一处理的都可以用AOP来实现,利用AOP可以做到业务无侵入。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ry7rjBlZ-1623914385687)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20210617145436955.png)]

15.阿里二面:Spring中后置处理器的作用是什么?

Spring中的后置处理器分为BeanFactory后置处理器和Bean后置处理器,他们是Spring底层源码架构设计中非常重要的一种机制,同时开发者也可以利用这两种后置处理器来进行扩展。BeanFactory后置处理器表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利用BeanFactory处理器来加工BeanFactory,比如Spring的扫描就是基于BeanFactory后置处理来实现的,而Bean后置处理器也类似,Spring在创建一个Bean的过程中,首先会实例化一个对象,然后再利用Bean后置处理器来对该对象进行加工,比如我们常说的依赖注入就是基于一个Bean后置处理器来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性自动赋值,还比如我们常说的AOP,也是利用一个Bean后置处理器来实现的,基于原实例对象,判断是否需要进行AOp,如果需要,name就基于原实例对象进行动态代理,生成一个代理对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7advPJkO-1623914385689)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20210617144926596.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3dTtijZI-1623914385691)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20210617144944107.png)]

野路子一面: 说一下Spring中Bean的生命周期

  1. Spring启动,查找并加载需要被Spring管理的bean,进行bean实例化
  2. Bean实例化后对将Bean的引用和值注入到Bean的属性中
  3. 如果Bean实现了BeanNameAware接口的话,Spring将并的Id传递给setBeanName()方法
  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
  5. 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将Bean所在应用上下文引用传入进来。
  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他的postProcessBeforeInitialization()方法。
  7. 如果Bean实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method生命了初始化方法,该方法也会被调用。
  8. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他的postProcessAfterInitialization()方法。
  9. 此时Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
  10. 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method声明销毁方法,该方法也会被调用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oszvMVuM-1623914385692)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20210617134844777.png)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值