首页 > 基础资料 博客日记
2024 Java 后端工程师面试题 50 道及答案解析
2024-09-13 00:00:07基础资料围观84次
一、JVM 相关
- 介绍 Java 内存模型的五个部分及特点
-
- 程序计数器
程序计数器是一块较小的内存空间,线程私有。它用于存储当前线程正在执行的字节码指令地址。具有存储空间小、线程独有、随线程创建和销毁、不会出现OutOfMemoryError等特点。主要作用是保证线程切换后的执行顺序,在多线程场景中确保线程能恢复到正确位置。
-
- 栈
Java 虚拟机栈也是线程私有的,生命周期与线程同步。它用于描述 Java 方法执行的线程内存模型,为每个即将运行的 Java 方法创建栈帧来存储信息,如局部变量表、操作数栈等。可能会出现 StackOverflowError 和 OutOfMemoryError 异常。局部变量表创建随方法执行,大小在编译期确定且运行时不变。
-
- 本地方法栈
本地方法栈与虚拟机栈类似,线程私有,区别在于它服务于虚拟机使用的本地方法。当本地方法被执行时,会创建栈帧存放相关信息,方法执行完毕后栈帧出栈释放内存。同样可能抛出 StackOverflowError 和 OutOfMemoryError 异常。
-
- 堆
Java 堆是虚拟机管理的最大内存区域,线程共享,用于存放对象实例。通过 -Xms 和 -Xmx 参数设置大小,默认分别为物理内存的 1/64 和 1/4。堆内存分为新生代和老生代,新生代采用复制算法回收,老生代通常采用标记 - 清除或标记 - 整理算法。
-
- 方法区
方法区线程共享,用于存储已被虚拟机加载的类信息、常量、静态变量等数据。在 JDK 1.6 中由永久代实现,1.8 后由元空间实现,使用本地内存。方法区大小决定可保存的类数量,若定义过多类可能导致方法区溢出。
- 类加载的过程及每个阶段的详细描述
-
- 加载
通过类的全限定名获取对应的二进制字节流,主要由类加载器实现。将字节流的静态存储结构转化为方法区的运行时数据结构,并在堆中为该类生成一个 Class 对象。
-
- 验证
进行文件格式、元数据、字节码和符号引用等多方面的验证,确保 Class 文件符合虚拟机要求,不会威胁虚拟机安全。
-
- 准备
为类变量(被 static 修饰)在方法区分配内存并设置初始值,如 int 型初始化为 0,boolean 型初始化为 false 等。但 final 修饰的静态变量在编译时分配,不在此阶段初始化。
-
- 解析
将常量池中的符号引用转换为直接引用,针对类、字段、类方法、接口方法等进行解析。此操作通常在初始化之后执行。
-
- 初始化
执行类构造器,进行静态代码块中的操作和静态变量的赋值。先初始化父类,再初始化当前类,只有在对类主动使用时才会初始化。
- 如何判断对象和类的死亡
判断对象死亡主要有引用计数法和可达性分析法。引用计数法因无法解决循环引用问题,主流虚拟机未采用。可达性分析法通过从 GC Roots 对象向下搜索,若对象到 GC Roots 无引用链相连则判定为死亡。对象被判定死亡后,并非立即回收,还需经过两次标记。
判断类是否废弃,需要该类所有实例已被回收、加载该类的 ClassLoader 已被回收,且该类对应的 java.lang.Class 对象在任何地方都未被引用。
- 解释 JVM 中的垃圾回收策略及算法
-
- 标记-清除
分为标记和清除两个阶段。先标记出需要回收的对象,然后直接清除。该算法简单但会产生大量内存碎片。
-
- 复制
将内存分为两块,使用其中一块进行对象存储,当需要回收时,将存活对象复制到另一块,然后清理原区域。适用于新生代,效率较高。
-
- 标记-整理
先标记出存活对象,然后将其整理到一端,再清理另一端。适用于老年代,可减少内存碎片。
-
- 分代收集
根据对象存活周期不同,将堆分为新生代和老年代,采用不同的回收算法。新生代通常使用复制算法,老年代一般使用标记 - 清除或标记 - 整理算法。
二、多线程相关
1. Java 创建线程之后,start()方法和 run()方法的区别
当调用start()方法时,会启动一个新线程,并在新线程中执行run()方法中的逻辑。start()方法会执行一系列的线程初始化操作,然后自动调用run()方法。而直接调用run()方法,只是在当前线程中执行run()方法中的代码,不会创建新的线程,无法实现多线程并发执行的效果。
2. 常用线程池模式及使用场景
常见的线程池模式有:
- FixedThreadPool:线程数量固定。适用于执行大量长期稳定的计算任务,如后台数据处理。
- CachedThreadPool:线程数量可变,能自动回收空闲线程。适合处理大量短期、突发的任务,如网络请求处理。
- SingleThreadExecutor:只有一个线程执行任务。用于保证任务顺序执行,如文件读写操作。
- ScheduledThreadPool:支持定时和周期性任务执行。适用于定时任务,如定时备份数据。
3. 线程间通信的同步方式及问题
线程间通信的同步方式主要有:
- synchronized关键字:通过加锁来实现同步,保证同一时刻只有一个线程访问共享资源。
- ReentrantLock:一种可重入的锁,提供了更灵活的锁控制。
- volatile关键字:保证变量的可见性,但不保证原子性。
- ThreadLocal:为每个线程提供独立的变量副本,实现线程间数据隔离。
在使用这些同步方式时,可能会出现死锁、性能问题、代码可读性降低等问题。
4. 可重入锁与 synchronized 的区别
- 用法不同:synchronized可修饰方法、代码块,ReentrantLock只能用于代码块。
- 锁获取和释放机制不同:synchronized自动加锁和释放,ReentrantLock需手动控制。
- 锁类型:synchronized默认为非公平锁,ReentrantLock可指定公平或非公平。
- 响应中断:ReentrantLock可响应中断,synchronized不行。
5. 线程安全的实现方式及案例
实现线程安全的方式有:
- 使用同步关键字或锁,如synchronized、ReentrantLock。
- 使用线程安全的集合类,如ConcurrentHashMap。
案例:比如多个线程同时操作一个共享的计数器,通过加锁保证计数的准确性。
class Counter {
private int count;
private Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public int getCount() {
return count;
}
}
三、数据库相关
1. MySQL 存储引擎的区别与特点
InnoDB
InnoDB 是 MySQL 数据库的默认存储引擎之一。
- 支持事务,提供 ACID 兼容的事务功能,保证数据的一致性和完整性。
- 支持行级锁,在高并发环境下能提供更好的并发性能。
- 支持外键约束,有助于维护数据的关系完整性。
- 具有崩溃恢复和故障转移能力,能保障数据的安全性。
- 不支持全文索引。
MyISAM
MyISAM 是 MySQL 早期的默认存储引擎。
- 不支持事务,不提供数据的事务保护。
- 支持表级锁,在写入操作时会锁定整个表,并发性能相对较差。
- 不支持外键约束。
- 不支持崩溃后的安全恢复。
- 支持全文索引,适用于文本检索较多的场景。
2. 数据库索引的原理与类型
主键索引
主键索引是一种特殊的唯一索引,它基于表的主键创建。主键索引确保了主键列的值的唯一性和非空性,数据库会自动为主键创建主键索引。其数据结构通常是 B+树,能快速定位和检索主键值对应的行数据。
唯一索引
唯一索引确保索引列中的值在整个表中是唯一的。它与主键索引的区别在于,唯一索引允许列值为 NULL,但最多只有一个 NULL 值。
联合索引
联合索引基于多个列创建,适用于经常通过多个列的组合进行查询的场景。在使用联合索引时,遵循最左前缀原则,即查询条件中使用联合索引的最左侧列,才能有效地利用索引进行优化。
3. 数据库事务的特性及隔离级别
数据库事务具有原子性、一致性、隔离性和持久性这四个特性。
- 原子性:事务是一个不可分割的操作单元,要么全部执行成功,要么全部失败回滚。
- 一致性:事务执行前后,数据库的完整性约束没有被破坏。
- 隔离性:多个事务并发执行时,互相之间没有影响。
- 持久性:事务一旦提交,其对数据库的修改就是永久的。
隔离级别从低到高分为读未提交、读已提交、可重复读和串行化。
- 读未提交:一个事务可以读取到另一个未提交事务的数据,可能导致脏读、不可重复读和幻读问题。
- 读已提交:一个事务只能读取到另一个已提交事务的数据,可以避免脏读,但仍可能存在不可重复读和幻读。
- 可重复读:同一事务在执行期间多次读取同一数据时,结果是一致的,能避免脏读和不可重复读,但可能出现幻读。
- 串行化:事务串行执行,可避免脏读、不可重复读和幻读,但性能最差。
4. 数据库优化的方法与案例
数据库优化的方法包括:
- 合理设计数据库表结构,选择合适的数据类型,避免过度冗余。
- 建立合适的索引,提高查询效率,但要注意避免过度索引影响写入性能。
- 优化 SQL 语句,避免全表扫描,使用合适的查询条件和连接方式。
- 合理配置数据库参数,如缓存大小、连接数等。
案例:例如对于一个频繁查询的大表,通过分析查询语句和数据访问模式,在经常用于查询条件的列上创建索引,从而显著提高查询速度。或者对于数据量较大的表,进行分表或分区操作,将数据分散到多个物理存储上,减少单个表的数据量,提高查询和写入性能。
四、数据结构与算法相关
- 数组中常见问题的解决方法
-
- 查找缺失数字
可以通过先对数组排序,然后遍历数组来查找缺失的数字。或者使用数学方法,计算给定范围内数字的总和,然后减去数组元素的总和,差值即为缺失的数字。
-
- 查找重复数字
可以使用哈希表来存储已经出现过的数字,遍历数组时,如果某个数字已经在哈希表中,就说明是重复数字。也可以先对数组排序,然后相邻元素进行比较来查找重复数字。
-
- 找出最大和最小数字
通过遍历数组,比较每个元素与当前的最大和最小数字,更新最大和最小数字的值。
- 链表中常见问题的解决方法
-
- 反转链表
可以使用三个指针,分别指向当前节点、前一个节点和后一个节点,通过逐个节点的指针调整来实现链表的反转。
-
- 删除重复节点
可以使用一个指针遍历链表,使用一个临时变量记录当前节点的值,当遇到下一个节点值与之相同时,删除下一个节点。
-
- 计算链表长度
从链表头开始,使用一个指针遍历链表,每经过一个节点,长度加 1,直到指针指向链表尾。
- 二叉树和字符串的常见操作
-
- 二叉树的遍历
包括前序遍历(先访问根节点,然后遍历左子树,最后遍历右子树)、中序遍历(先遍历左子树,然后访问根节点,最后遍历右子树)和后序遍历(先遍历左子树,然后遍历右子树,最后访问根节点)。还可以进行层序遍历,使用队列来实现。
-
- 字符串的操作与算法
常见的操作有字符串的拼接、截取、查找、替换等。算法方面,例如字符串的匹配算法,如 KMP 算法、BM 算法等。
五、设计模式相关
- 单例模式的实现方式
-
- 饿汉式
饿汉式单例模式在类加载时就创建实例。其优点是实现简单,线程安全;缺点是可能造成资源浪费,因为无论是否使用,实例都会被创建。
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 懒汉式
懒汉式单例模式在第一次调用获取实例的方法时才创建实例。优点是实现了延迟加载,节省资源;缺点是在多线程环境下可能出现线程安全问题。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 双重检查
双重检查锁机制解决了懒汉式在多线程环境下的线程安全和性能问题。通过两次判断实例是否为空,避免了不必要的同步操作,提高了效率。
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- 常见设计模式的介绍与应用
-
- 工厂模式
工厂模式定义一个用于创建对象的接口,让子类决定实例化哪一个类。它将对象的创建和使用分离,提高了代码的灵活性和可维护性。常用于创建复杂对象的场景。
例如,在一个汽车生产系统中,有不同类型的汽车(如轿车、SUV),可以使用工厂模式根据需求创建不同类型的汽车对象。
-
- 装饰者模式
装饰者模式动态地给一个对象添加额外的职责。它通过包装原始对象,在不改变原始对象接口的情况下,为其添加新的功能。
比如,对于一个咖啡店的订单系统,咖啡可以有不同的装饰(如加糖、加奶),使用装饰者模式可以方便地为基础咖啡添加各种装饰。
-
- 观察者模式
观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
常见于事件处理、消息推送等场景。例如,一个股票行情系统中,多个投资者作为观察者,股票数据作为被观察者,当股票价格变动时,通知所有投资者更新信息。
六、框架相关
- Spring 框架的核心概念
-
- IOC 容器
Spring 的 IOC(控制反转)容器是其核心特性之一。它负责创建、管理和组装对象,将对象的创建和依赖关系的管理从应用代码转移到了容器中。通过配置文件或注解,容器可以自动为对象注入所需的依赖,实现了对象之间的解耦,提高了代码的灵活性和可维护性。
-
- AOP 原理
Spring AOP(面向切面编程)基于动态代理的设计模式。如果被增强的类实现了接口,会使用 JDK 动态代理;对于未实现接口的类,则使用 CGLIB 动态代理生成子类作为代理对象。AOP 将横切关注点(如日志、事务管理等)模块化,通过定义切面、切点、通知等,在不修改业务逻辑代码的情况下,将这些功能织入到目标对象中,提高了代码的模块化和可维护性。
- MyBatis 框架的特点与使用
MyBatis 是一个灵活的持久层框架,专注于 SQL 本身,适用场景广泛。它的特点包括:是一个半自动映射框架,配置关系相对简单;支持动态 SQL,可根据不同条件生成不同的 SQL 语句;需要手动编写 SQL,便于进行 SQL 优化和移植;学习使用门槛低,适合需求变化频繁的大型项目。在使用时,需配置 MyBatis 的全局配置文件和映射文件,通过创建 SqlSessionFactory 和 SqlSession 来执行数据库操作。
- Spring Boot 的优点
Spring Boot 具有诸多优点,使其在开发中备受青睐。它可以创建独立的 Spring 应用程序,支持生成可执行的 JAR 和 WAR 文件;内嵌 Servlet 容器,简化了应用的部署;提供自动配置的 POMS,简化 Maven 配置;尽可能自动配置 Spring 容器,减少开发工作量;提供丰富的特性,如指标、健康检查和外部化配置;无需冗余代码生成和 XML 配置,遵循约定优于配置原则;与 Spring 生态系统容易集成,提高开发效率;提供嵌入式 HTTP 服务器,方便开发测试 Web 应用程序;提供命令行接口工具,用于开发测试应用程序;支持多种插件,便于使用内置工具进行开发测试。
七、网络通信相关
1. TCP 三次握手与四次挥手的过程
TCP 三次握手的过程如下:
- 第一次握手:客户端向服务器发送一个带有 SYN 标志的数据包,其中包含一个随机生成的序列号 x,并进入 SYN_SENT 状态。
- 第二次握手:服务器收到客户端的 SYN 数据包后,向客户端返回一个带有 SYN 和 ACK 标志的数据包,确认号为 x + 1,序列号为一个随机生成的 y,并进入 SYN_RCVD 状态。
- 第三次握手:客户端收到服务器的 SYN + ACK 数据包后,向服务器发送一个带有 ACK 标志的数据包,确认号为 y + 1,序列号为 x + 1,此时客户端和服务器进入 ESTABLISHED 状态,连接建立成功。
TCP 四次挥手的过程如下:
- 第一次挥手:客户端向服务器发送一个带有 FIN 标志的数据包,序列号为 u,进入 FIN_WAIT_1 状态,表示客户端请求关闭连接。
- 第二次挥手:服务器收到客户端的 FIN 数据包后,向客户端返回一个带有 ACK 标志的数据包,确认号为 u + 1,序列号为 v,进入 CLOSE_WAIT 状态。
- 第三次挥手:服务器处理完剩余数据后,向客户端发送一个带有 FIN 标志的数据包,序列号为 w,确认号为 u + 1,进入 LAST_ACK 状态。
- 第四次挥手:客户端收到服务器的 FIN 数据包后,向服务器发送一个带有 ACK 标志的数据包,确认号为 w + 1,序列号为 u + 1,进入 TIME_WAIT 状态。经过 2MSL 时间后,客户端关闭连接,服务器收到客户端的 ACK 数据包后也关闭连接。
2. HTTP 协议的请求与响应
HTTP 协议的请求由以下部分组成:
- 请求行:包含请求方法(如 GET、POST 等)、请求的 URL 以及 HTTP 版本。
- 请求头:包含各种与请求相关的信息,如 Host、User-Agent、Accept 等。
- 空行:用于分隔请求头和请求体。
- 请求体:在 POST 等方法中携带的数据。
HTTP 协议的响应由以下部分组成:
- 状态行:包含 HTTP 版本、状态码和状态描述。
- 响应头:包含各种与响应相关的信息,如 Content-Type、Content-Length 等。
- 空行:用于分隔响应头和响应体。
- 响应体:服务器返回的数据。
常见的状态码有:
- 200 OK:表示请求成功。
- 301 Moved Permanently:表示资源已被永久移动。
- 302 Found:表示资源临时移动。
- 400 Bad Request:表示客户端请求有语法错误。
- 401 Unauthorized:表示请求未经授权。
- 403 Forbidden:表示服务器拒绝提供服务。
- 404 Not Found:表示请求的资源不存在。
- 500 Internal Server Error:表示服务器内部错误。
3. HTTP 1.0 与 2.0 的新特性
HTTP 2.0 相较于 HTTP 1.0 具有以下新特性:
- 新的二进制格式:HTTP 1.x 基于文本,而 HTTP 2.0 采用二进制格式,解析更高效、健壮。
- 多路复用:一个连接上可以并发处理多个请求和响应,消除了队头阻塞问题。
- 头部压缩:使用 encoder 减少传输的头部大小,提高传输效率。
- 服务端推送:服务器可以主动推送资源给客户端,减少客户端的请求次数。
八、其他相关
1. Java 中基本数据类型的转换
在 Java 中,基本数据类型的转换分为自动类型转换(隐式转换)和强制类型转换(显式转换)。
自动类型转换是在满足一定条件下,由编译器自动完成的。例如,将较小范围的整数类型(如 byte、short)转换为较大范围的整数类型(如 int、long),或者将整数类型转换为浮点数类型(如 float、double)。但需要注意的是,这种转换可能会导致精度丢失。
强制类型转换则需要开发者显式地指定,使用括号将目标数据类型括起来,并将需要转换的值放在括号内。例如,将一个 double 类型的值强制转换为 int 类型:int num = (int) 3.14; 。强制类型转换可能会导致数据截断或溢出。
2. 不可变对象的概念与创建
在 Java 中,不可变对象是指一旦创建其状态(属性值)就不能被修改的对象。
不可变对象的概念:
不可变对象具有以下重要特性:
- 其属性在对象创建后不能被修改。
- 不可变对象是线程安全的,多个线程可以同时访问而无需担心数据不一致的问题。
- 由于其状态不可变,便于在不同的代码部分传递和共享,而不用担心被意外修改。
不可变对象的创建:
以下是创建不可变对象的一些常见方式和注意事项:
- 使用
final
关键字修饰类:防止类被继承,从而避免子类修改父类的属性。
final class ImmutableClass {
private final int value;
public ImmutableClass(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
- 使属性为
private
和final
:确保属性只能在对象构造时被初始化,之后无法修改。
class ImmutableObject {
private final String name;
public ImmutableObject(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
- 不提供修改属性的方法:如果没有方法可以改变对象的属性,那么对象就是不可变的。
例如,如果要表示一个人的年龄,我们可以创建一个不可变的 PersonAge
类:
class PersonAge {
private final int age;
public PersonAge(int age) {
this.age = age;
}
public int getAge() {
return age;
}
}
总之,创建不可变对象有助于提高代码的可维护性、可读性和线程安全性。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签: