大数据面试2小时前冲刺必备:大厂高频大数据面试题上(基础篇-多张原理图)
Java 中四种引用(强引用、软引用、弱引用、虚引用)的区别是什么?(小鹏汽车、昆仑万维)
在 Java 中,引用的强弱程度直接影响对象的垃圾回收行为。JDK 提供了四种不同级别的引用类型,它们从强到弱依次为:强引用、软引用、弱引用、虚引用。
强引用(Strong Reference) 是 Java 最常见的引用类型,赋值方式如:Object obj = new Object();
。只要一个对象存在强引用,就不会被垃圾回收器回收。
特点:
- 内存不足时,GC 也不会回收它。
- 是默认的引用类型。
- 只有将引用设为 null 后,对象才有可能被 GC 回收。
软引用(SoftReference) 软引用可以用来实现内存敏感的缓存。只要内存不紧张,就不会被回收;当内存不足时会被回收。
用法:
SoftReference<Object> softRef = new SoftReference<>(new Object());
特点:
- GC 在内存不足时回收软引用的对象。
- 可用于缓存系统,如图片、页面对象等。
弱引用(WeakReference) 弱引用比软引用更容易被回收,只要进行 GC,不管内存是否充足,都会被回收。
用法:
WeakReference<Object> weakRef = new WeakReference<>(new Object());
特点:
- 常用于 ThreadLocal 的底层实现。
- 使用后要及时清除,避免内存泄漏。
虚引用(PhantomReference) 虚引用最弱,几乎没有实际用途,仅用于监控对象是否被 GC 回收。
用法:
ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
特点:
- 必须与 ReferenceQueue 配合使用。
- 一旦对象被 GC,虚引用会加入队列。
- 常用于管理堆外内存(如 DirectByteBuffer)。
强引用 |
是 |
永不回收 |
普通对象引用 |
软引用 |
否 |
内存不足时 |
缓存(如图片、对象) |
弱引用 |
否 |
一旦发现 |
ThreadLocal、Map key |
虚引用 |
否 |
对象将被回收前 |
管理堆外资源 |
Java 类加载机制的过程是什么?(货拉拉、斗鱼)
Java 类加载机制指的是 JVM 如何将 .class
字节码文件加载到内存中,并进行连接与初始化,最终形成可以运行的 Java 类。
整体流程分为以下几个阶段:
- 加载(Loading)
- 通过类的全限定名找到对应 .class 文件并读取字节流。
- 将字节流转换为 JVM 能识别的 Class 对象。
- 可能从本地磁盘、网络、Jar 包等加载。
- 验证(Verification)
- 验证字节码是否合法,防止恶意代码。
- 包括文件格式验证、元数据验证、字节码验证、符号引用验证。
- 准备(Preparation)
- 为类的静态变量分配内存,并设置默认值(不包括 static 块)。
- 不执行任何代码。
- 解析(Resolution)
- 将常量池中的符号引用(符号名、方法名等)替换为直接引用(内存地址)。
- 包括类或接口、字段、类方法、接口方法的解析。
- 初始化(Initialization)
- 执行类构造方法 <clinit>()。
- 初始化静态变量和 static 块,按代码顺序执行。
- 父类先于子类初始化。
类加载器的双亲委派机制(重要) 类加载通常遵循“父加载器优先”,流程为:
- 如果父类加载器可以加载,则返回父类加载器结果。
- 否则当前加载器才尝试加载。
这可防止重复加载标准类库,保障系统安全。
自定义类加载器代码示例:
public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); return defineClass(name, classData, 0, classData.length); } private byte[] loadClassData(String name) { // 自定义逻辑加载字节码 } }
类加载过程是 JVM 性能调优和热部署的关键部分,也是 OSGi、Tomcat 等容器的核心能力基础。
抽象类和接口的区别与联系是什么?(soul、货拉拉、小鹏汽车)
抽象类(abstract class)和接口(interface)是 Java 中实现抽象与多态的两种主要手段。两者都不能被实例化,都可被继承或实现,但各自使用场景和机制不同。
主要区别:
比较项 |
抽象类 |
接口 |
关键字 |
abstract |
interface |
构造方法 |
可以有构造方法 |
不能有构造方法 |
多继承 |
单继承(只能继承一个抽象类) |
多继承(可以实现多个接口) |
成员变量 |
可以有实例变量和静态变量 |
只能有静态常量(默认 public static final) |
成员方法 |
可以有普通方法和抽象方法 |
只能有抽象方法(Java 8 后可有默认方法、静态方法) |
访问修饰符 |
可用 public、protected、default等 |
默认 public |
应用场景 |
表示“是什么”(is-a)关系 |
表示“能做什么”(can-do) |
联系与补充说明:
- 抽象类可包含业务实现,适合用于代码复用;
- 接口强调行为规范,适合架构设计时定义功能边界;
- Java 8 引入 default 和 static 方法,使接口也具备部分实现能力;
- Java 9 引入 private 方法增强了接口的封装性;
- 一个类可以继承一个抽象类并实现多个接口,常用于“模板方法 + 策略”组合。
代码示例:
abstract class Animal { String name; public Animal(String name) { this.name = name; } public abstract void speak(); } interface Swimmable { void swim(); } class Dog extends Animal implements Swimmable { public Dog(String name) { super(name); } @Override public void speak() { System.out.println(name + " says: Woof"); } @Override public void swim() { System.out.println(name + " can swim"); } }
适用场景:
- 抽象类用于同类对象的统一行为抽象,如动物都可以叫;
- 接口用于横向能力扩展,如动物中只有部分能游泳。
Java 反射的作用及实现原理是什么?(富途证券、昆仑万维)
Java 反射(Reflection)是 Java 提供的一种强大机制,它允许在运行时动态获取类的信息(包括类名、属性、方法、构造器等),并可动态调用方法、访问字段、构造对象,甚至修改类的行为。这种机制是 Java 动态语言特性的基础,也是框架(如 Spring、MyBatis、JUnit 等)底层实现的重要支撑。
一、反射的作用
- 运行时获取类的信息可在不知道类定义的前提下动态加载类、读取类名、方法、属性、注解等元数据。
- 动态调用方法和访问字段可用于实现通用框架,如 JSON 序列化、ORM 映射、IoC 容器、依赖注入等。
- 绕过封装访问私有成员通过 setAccessible(true) 访问私有属性、方法,增强灵活性。
- 动态创建对象无需使用 new,通过构造方法反射调用实例化对象,适用于插件机制、工厂模式。
- 应用场景示例序列化与反序列化(如 FastJSON、Jackson);JDK 动态代理;自动装配(如 Spring 注入 Bean);测试框架自动识别 @Test 方法;配置驱动型开发(如读取配置类中字段注解来做注入)。
二、反射的实现原理
Java 反射的核心在于 java.lang.reflect
包下的类:Class
、Field
、Method
、Constructor
等。其本质是操作 JVM 在加载类之后生成的 Class 对象,并操纵其元数据。
- Class 对象与类加载每个类加载后,JVM 都会为其生成唯一的 Class 类对象。通过 Class.forName("类名") 或 对象.getClass() 获取该对象。
- 反射访问流程:
- 访问私有字段/方法:
- 反射是如何实现的?JVM 类加载器在加载类时,解析字节码生成 Class 元数据对象。反射通过读取该结构体提供的元信息进行操作。虽然运行期通过 JNI 接口或 Unsafe 操作底层内存,但大多数应用级使用只调用公开 API。
三、反射的性能与安全性问题
- 性能开销:反射是解释执行,通常比直接调用慢 10 倍左右;频繁使用建议结合缓存(如 Method 缓存)优化。
- 可维护性差:编译器不会校验方法名、参数,易出错且不易发现,影响重构。
- 安全性问题:可绕过访问控制机制,可能带来数据泄露、安全漏洞等风险。
四、Java 反射相关 API 简表
|
表示类或接口的运行时表示 |
|
表示类的成员变量 |
|
表示类的方法 |
|
表示类的构造方法 |
|
提供动态创建数组的静态方法 |
|
提供方法或字段修饰符的解码工具类 |
Runnable 和 Callable 接口的区别是什么?(水滴)
Java 中的 Runnable
和 Callable
都是用于表示并发任务的接口,通常与线程或线程池结合使用,但它们在功能、使用方式和适用场景上存在明显区别。
核心区别对比:
所在包 |
java.lang |
java.util.concurrent |
是否有返回值 |
否 |
是(使用泛型指定返回类型) |
是否抛出异常 |
不能抛出受检异常 |
可以抛出受检异常 |
方法名称 |
|
|
与线程关系 |
可作为 Thread 构造函数参数 |
必须结合 FutureTask 或线程池使用 |
线程池使用 |
|
|
Runnable 示例:
Runnable task = () -> System.out.println("任务执行中"); new Thread(task).start();
Callable 示例:
Callable<Integer> task = () -> { Thread.sleep(100); return 123; }; FutureTask<Integer> future = new FutureTask<>(task); new Thread(future).start(); Integer result = future.get(); // 获取返回值
功能拓展性对比:
- Runnable 适合不需要结果的简单异步任务,如打印日志、更新状态;
- Callable 更适合需要结果的任务,如数据库查询、文件读取等;
- Callable 可结合 Future 获取异步结果,也能进行取消、中断、异常追踪。
线程池使用场景:
ExecutorService pool = Executors.newCachedThreadPool(); Future<String> future = pool.submit(() -> "结果返回"); System.out.println(future.get());
常见误区:
- 将 Runnable 误用在需要结果的场景,实际无法回传;
- 直接用 Callable 不配合 FutureTask/Future,会抛异常;
- 忽略 Future.get() 是阻塞操作,需结合并发框架优化。
适用建议:
- 若任务无需返回结果,优先使用 Runnable;
- 若需要返回结果、捕获异常、取消任务等功能,使用 Callable + Future。
String、StringBuilder 和 StringBuffer 有什么区别?
在 Java 中,String、StringBuilder 和 StringBuffer 都与字符串处理有关,但它们在一些重要方面存在区别:
首先,String 是不可变类。这意味着一旦一个 String 对象被创建,它的值就不能被修改。每次对 String 进行修改操作,如拼接、替换等,实际上都会创建一个新的 String 对象。例如:
String str = "Hello"; str = str + " World";
在上述代码中,当执行 str = str + "World" 时,会创建一个新的 String 对象,原始的 "Hello" 对象仍然存在,只是 str 引用指向了新创建的 "Hello World" 对象。这可能会导致性能问题,尤其是在频繁修改字符串的情况下,会产生大量的中间对象,增加内存开销。
而 StringBuilder 和 StringBuffer 是可变的字符串序列。它们允许对字符串进行修改而不创建新的对象。两者的主要区别在于线程安全性。
StringBuilder 是非线程安全的,它的性能相对较高,适用于单线程环境。例如:
StringBuilder sb = new StringBuilder("Hello"); sb.append(" World");
在这里,调用 append () 方法会在原 StringBuilder 对象上进行修改,不会创建新的对象,从而提高了性能。
StringBuffer 是线程安全的,它的方法使用了 synchronized 关键字进行同步,适用于多线程环境。例如:
StringBuffer sbf = new StringBuffer("Hello"); sbf.append(" World");
在多线程并发修改字符串的情况下,使用 StringBuffer 可以保证操作的一致性,避免出现数据不一致的问题。但由于同步机制的存在,其性能会略低于 StringBuilder。
在性能方面,对于单线程应用,StringBuilder 通常是更好的选择,因为它避免了同步的开销。而在多线程环境中,如果需要保证字符串操作的线程安全性,应该使用 StringBuffer。
另外,从方法的角度来看,它们都提供了一些相似的方法,如 append () 用于追加内容,insert () 用于插入内容,delete () 用于删除内容等,但由于 String 是不可变的,其相关操作实际上是返回一个新的 String 对象,而 StringBuilder 和 StringBuffer 会在原对象上进行修改。
== 和 equals () 有什么区别?(字根互联、南网)
在 Java 中,==
和equals()
有很大的区别。
==
是一个运算符,它主要用于比较两个变量的值是否相等。对于基本数据类型,它比较的是实际的值。例如,对于两个int
变量a = 5
和b = 5
,a == b
的结果为true
,因为它们的值是相同的。对于引用数据类型,==
比较的是两个对象的引用是否相同,也就是看这两个变量是否指向内存中的同一个对象。例如,有两个String
对象str1 = new String("Hello");
和str2 = new String("Hello");
,str1 == str2
的结果为false
,因为尽管它们的内容相同,但它们是两个不同的对象,在内存中有不同的存储位置。
equals()
是Object
类中的一个方法,所有的类都继承自Object
类,所以所有的对象都有equals()
方法。在Object
类中,equals()
方法的默认实现实际上和==
运算符对于引用比较的行为是一样的。但是,很多类会重写equals()
方法来提供更符合业务逻辑的比较方式。例如,String
类重写了equals()
方法,它会比较两个String
对象的内容是否相同。对于前面提到的str1
和str2
,str1.equals(str2)
的结果为true
,因为String
类的equals()
方法比较的是字符串的字符序列是否相同,而不是对象引用。
在实际编程中,当需要比较基本数据类型的值时,使用==
;当需要比较对象的内容是否相等(特别是对于自定义类)时,需要根据类是否正确重写了equals()
方法来决定是否使用equals()
进行比较。如果没有重写equals()
方法,可能会得到不符合预期的比较结果。
介绍ConcurrentHashMap和HashMap的区别。(货拉拉、斗鱼)
(1)线程安全性
HashMap:不是线程安全的。在多线程环境下,如果多个线程同时对HashMap进行写操作(比如添加、删除元素),可能会导致数据不一致、死循环等问题。例如,两个线程同时对同一个哈希桶进行插入操作,可能会破坏链表的结构。
ConcurrentHashMap:是线程安全的。它专门为多线程并发环境设计,多个线程可以同时对ConcurrentHashMap进行读和写操作而不会出现数据不一致等问题。
(2)性能方面
HashMap:在单线程环境下性能较好,因为没有线程安全相关的开销。但在多线程环境下,由于缺乏线程安全机制,不能直接用于多线程并发操作。
ConcurrentHashMap:虽然保证了线程安全,但相比HashMap在单线程下的性能会稍差一些。不过,它采用了高效的并发控制机制,如分段锁(Java 7及之前版本)或者CAS(Compare - And - Swap)操作 + 同步机制(Java 8及之后版本),使得在多线程环境下能够高效地进行读写操作。
(3)结构方面
HashMap:前面已经介绍了其结构为数组 + 链表(或红黑树)。
ConcurrentHashMap:在Java 8之前,采用分段锁的机制,将整个哈希表分成多个段(Segment),每个段相当于一个独立的小哈希表,不同的段可以被不同的线程并发操作。在Java 8及之后,采用了数组 + 链表(或红黑树)的结构,并且在元素操作时使用CAS操作和synchronized关键字进行更细粒度的同步控制。
重载(Overload)和重写(Override)的区别是什么?(4399、昆仑万维)
重载(Overload) 指的是在同一个类中,方法名相同但参数列表不同(参数类型或参数个数不同)。
重写(Override) 是子类对父类方法的重新实现,要求方法签名(包括方法名、参数列表)相同。
定义位置 |
同一个类中 |
子类中覆盖父类方法 |
方法名 |
相同 |
相同 |
参数列表 |
不同 |
必须相同 |
返回类型 |
可不同(但不能只因返回类型不同而重载) |
可以是父类返回类型的子类(协变返回类型) |
访问修饰符 |
不受限制 |
子类方法的访问修饰符不能比父类更严格 |
抛出异常 |
无要求 |
子类方法抛出的异常不能比父类更多、更广泛 |
多态 |
无法体现多态 |
实现运行时多态 |
示例:
class Animal { void eat() { System.out.println("Animal eats"); } } class Dog extends Animal { @Override void eat() { System.out.println("Dog eats bone"); } void eat(String food) { // 重载 System.out.println("Dog eats " + food); } }
ArrayList 和 LinkedList 的区别(底层实现、扩容机制)是什么?(小鹏汽车、携程)
ArrayList 和 LinkedList 都是 List 接口的实现类,但它们在底层结构、插入删除性能、访问速度等方面存在显著区别。
底层结构 |
动态数组(Object[]) |
双向链表(Node) |
随机访问 |
支持,时间复杂度 O(1) |
不支持,需遍历,时间复杂度 O(n) |
插入删除 |
插入/删除效率低(需要元素移动) |
插入/删除效率高(只需修改引用) |
插入位置 |
在末尾插入效率高,中间插入需移动元素 |
任意位置插入效率高(仅限于节点移动) |
线程安全 |
否 |
否 |
扩容机制(仅限 ArrayList): ArrayList 初始容量为 10,添加元素超出容量时,默认扩容为原容量的 1.5 倍:
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容 1.5 倍
扩容会创建一个更大的数组,并将原数组的元素复制过去,因此扩容操作代价较高,推荐预估容量并使用构造函数提前设置初始容量。
LinkedList 不涉及扩容,其每个元素由一个 Node 节点存储,包含 prev、next 指针指向前后节点,插入或删除时只需修改指针指向。
选择建议:
- 频繁查询、随机访问时使用 ArrayList;
- 插入、删除操作频繁(尤其在头部或中间)时使用 LinkedList。
进程与线程的区别是什么?(货拉拉、小鹏汽车、斗鱼)
进程是操作系统资源分配的基本单位,而线程是程序执行的最小单位,是进程内的一个执行路径。一个进程可以包含多个线程,这些线程共享进程的资源。
区别点 |
进程 |
线程 |
地址空间 |
每个进程拥有独立的地址空间 |
同一进程内线程共享地址空间 |
创建开销 |
创建和销毁开销较大 |
相对较小,资源开销少 |
通信方式 |
进程间通信较复杂,如管道、Socket 等 |
线程间共享内存,可直接通信 |
调度和切换 |
上下文切换成本高 |
切换开销小 |
崩溃影响 |
一个进程崩溃不会影响其他进程 |
一个线程异常可能导致整个进程崩溃 |
在 Java 中,线程是由 java.lang.Thread
或实现 Runnable
接口来创建的,JVM 自身是多线程运行的,比如垃圾回收线程、JIT 编译线程等。
Java 线程创建的方式有哪些?(水滴、货拉拉)
Java 中创建线程的方式主要有以下几种:
- 继承 Thread 类:
class MyThread extends Thread { @Override public void run() { System.out.println("线程执行"); } } new MyThread().start();
- 实现 Runnable 接口:
class MyRunnable implements Runnable { @Override public void run() { System.out.println("线程执行"); } } new Thread(new MyRunnable()).start();
- 实现 Callable 接口 + FutureTask:支持有返回值,且可以抛出异常
Callable<Integer> task = () -> 123; FutureTask<Integer> future = new FutureTask<>(task); new Thread(future).start(); Integer result = future.get();
- 线程池方式(推荐):通过 Executors 工厂方法或者自定义 ThreadPoolExecutor
ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit(() -> System.out.println("线程池执行")); executor.shutdown();
线程池方式是企业开发中最常用、最可控的方式,可以避免频繁创建销毁线程带来的开销。
synchronized 和 ReentrantLock 的区别是什么?(阅文集团、携程)
synchronized
是 Java 提供的关键字,用于加锁代码块或方法,保证线程间的互斥访问;而 ReentrantLock
是 JDK 1.5 引入的显式锁类,属于 java.util.concurrent.lock 包,功能更强大。
特性 |
synchronized |
ReentrantLock |
加锁方式 |
隐式 |
显式,需要手动加锁和释放 |
是否可中断 |
否 |
是,
支持中断 |
是否公平锁 |
否,默认非公平 |
可选,支持公平或非公平锁 |
是否支持尝试加锁 |
否 |
是,
方法 |
是否支持条件变量 |
否 |
是,支持多个
|
性能表现 |
JVM 自动优化,如偏向锁、轻量级锁 |
控制更细粒度,适用于复杂同步场景 |
示例代码:
// synchronized synchronized (this) { // 临界区代码 } // ReentrantLock Lock lock = new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
选择建议:
- 简单同步使用 synchronized 即可,语义清晰,避免死锁风险;
- 对并发控制要求高,或需中断锁/尝试锁/多个条件的复杂业务逻辑场景,建议使用 ReentrantLock;
JDK 1.6 之后对 synchronized 的性能做了大量优化(偏向锁、轻量级锁等),在多数情况下性能与 ReentrantLock 区别不大。
多态的概念及实现方式是什么?(小鹏汽车、银联)
多态是面向对象编程的三大特性之一,指的是同一操作作用于不同对象时,可以产生不同的行为。在 Java 中,多态分为编译时多态(方法重载)和运行时多态(方法重写)。
实现多态的三个条件:
- 有继承关系
- 子类重写父类方法
- 父类引用指向子类对象
class Animal { void speak() { System.out.println("动物叫"); } } class Dog extends Animal { void speak() { System.out.println("狗叫"); } } Animal a = new Dog(); // 父类引用指向子类对象 a.speak(); // 输出:狗叫(运行时多态)
多态的优点:
- 提高代码的可扩展性和可维护性
- 降低耦合度,方便框架设计
- 可用于实现接口驱动编程
运行时多态的实现原理: 底层通过方法表(vtable)机制实现。当程序运行时,JVM 根据对象实际类型调用对应的方法版本,而不是引用类型的方法。
注意事项:
- 只能调用父类中定义的方法,不能调用子类新增方法;
- 变量不具备多态性:属性访问取决于引用类型,而非对象实际类型;
- 构造方法不能被继承,因此也不存在构造方法的多态。
JVM 内存区域分为哪些?哪些是线程私有,哪些是线程共享?(昆仑万维、知乎、小鹏汽车)
Java 虚拟机在运行时将内存划分为若干个区域,每个区域承担不同职责,可分为线程私有与线程共享两类。
区域名称 |
所属类型 |
描述 |
程序计数器(PC) |
线程私有 |
指示当前线程执行的字节码指令地址 |
Java 虚拟机栈 |
线程私有 |
保存方法调用信息、局部变量表等 |
本地方法栈 |
线程私有 |
调用 Native 方法时的栈帧信息 |
堆(Heap) |
线程共享 |
存放对象实例,是垃圾回收的主要区域 |
方法区(元空间 Metaspace) |
线程共享 |
存放类元信息、常量、静态变量等 |
此外还有:
- 运行时常量池:原属方法区,JDK 1.8 后也位于元空间;
- 直接内存:不受 JVM 管控,通过 NIO 分配的堆外内存。
堆是最大的一块区域,GC 主要工作也围绕堆进行。线程私有区域生命周期随线程而定,线程结束后自动销毁。
堆和栈的区别,分别存放什么内容?(小鹏汽车、知乎)
特性 |
堆(Heap) |
栈(Stack) |
作用 |
存储对象实例、数组等 |
存储局部变量、方法调用信息 |
所属 |
线程共享 |
每个线程独立拥有 |
生命周期 |
与 JVM 生命周期一致 |
与线程生命周期一致 |
内存大小 |
一般较大,受 -Xmx 限制 |
较小,受 -Xss 限制 |
GC 管理 |
是 |
否,由线程自动回收 |
堆内容: 所有 new 出来的对象、数组、常量池(部分)都在堆中;
栈内容: 每次方法调用都会在栈中创建一个栈帧,包含局部变量表、操作数栈、方法出口信息等。
示例:
public void foo() { int a = 10; // a 是栈变量 String s = new String("hi"); // s 是栈变量,指向堆中创建的 String 对象 }
JVM 如何判断对象需要回收?(水滴、昆仑万维)
JVM 主要通过 可达性分析算法(Reachability Analysis) 来判断对象是否“存活”,即是否还被任何“GC Roots”可达。
GC Roots 包括:
- 虚拟机栈中引用的对象(局部变量表)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- JNI 引用的对象(Native 引用)
对象回收流程如下:
- 从 GC Roots 开始向下搜索
- 若对象不可达,则标记为“可回收”
- 若对象重写了 finalize() 方法,且从未被调用过,JVM 会将其放入 F-Queue 并执行一次机会
- 若该对象在 finalize() 中复活(如将自己赋值给某全局引用),则取消回收;否则回收
引用类型判断规则:
- 强引用(Strong Reference):正常引用关系,不回收
- 软引用(SoftReference):内存不足时回收
- 弱引用(WeakReference):下一次 GC 即回收
- 虚引用(PhantomReference):不能通过它访问对象,仅用于跟踪回收
常见的垃圾回收算法有哪些?(shein、昆仑万维)
JVM 中常见的垃圾回收算法如下:
- 标记-清除(Mark-Sweep)首先标记所有存活对象,然后清除未被标记的对象缺点:空间碎片较多,影响后续内存分配
- 复制算法(Copying)将内存分为两块,每次只用一块,将存活对象复制到另一块,再清空当前内存适用于对象生命周期短的场景,如新生代回收
- 标记-压缩(Mark-Compact)标记存活对象后将其向内存一端移动,清除边界外的空间用于老年代,解决空间碎片问题
- 分代收集算法(Generational GC)将内存划分为新生代(Eden + Survivor)、老年代、元空间;根据对象生命周期不同采用不同回收策略新生代使用复制算法,老年代使用标记-压缩或标记-清除
这些算法在实际 GC 实现中通常是组合使用的,如 G1 GC、ZGC、Shenandoah GC 等。
内存溢出(OOM)的常见场景及排查方法是什么?(携程、昆仑万维)
OOM(OutOfMemoryError)是 JVM 在申请内存时无法满足请求而抛出的严重错误,通常发生在堆、方法区、直接内存或本地线程等区域。
常见 OOM 场景:
- 堆内存溢出(Java heap space):创建大量对象或缓存,垃圾回收无法及时清理。
- 方法区(Metaspace)溢出:动态生成过多类,如频繁使用反射、动态代理或 CGLIB 动态生成类。
- 虚拟机栈溢出:栈帧太多导致栈空间耗尽,但通常报 StackOverflowError 而非 OOM。
- 本地内存溢出(Direct buffer memory):使用 NIO 分配堆外内存未及时释放。
- GC 回收失效:老年代堆积大量大对象,频繁 full GC 无法清理。
排查方法:
- 查看错误日志和堆栈:通过 jmap, jstack 分析异常线程与内存快照;
- 使用内存分析工具:如 MAT、VisualVM、JProfiler 分析堆 Dump 文件;
- 检查代码中缓存/集合是否无限增长:如 Map、List 使用不当导致泄露;
- 使用 JVM 参数限制内存:
-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=128m -XX:+HeapDumpOnOutOfMemoryError
示例代码导致 OOM:
List<byte[]> list = new ArrayList<>(); while (true) { list.add(new byte[1024 * 1024]); // 每次添加 1MB }
解决方式:释放无用引用、优化对象生命周期、启用合适的 GC 策略,避免内存泄漏。
双亲委派模型的原理是什么?如何破坏?(米哈游、携程)
双亲委派模型(Parent Delegation Model)是 Java 类加载机制的一种规范,用于保证核心类的优先加载、防止重复加载。
模型原理:
- 类加载器分为 启动类加载器(Bootstrap)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader);
- 每个类加载器在加载类时,先将请求委派给父类加载器;父加载器若无法加载,才由当前加载器尝试加载。
示意流程: 应用类加载器 → 扩展类加载器 → 启动类加载器(从上往下委派)
优点:
- 避免重复加载
- 保证 Java 核心类的安全性,如不会被用户代码篡改 java.lang.String
破坏双亲委派的方式:
- 自定义类加载器重写 loadClass() 方法:不调用 super.loadClass(),直接使用 findClass()
- 使用第三方框架(如 SPI、OSGi):部分模块化系统允许子类优先加载(打破委派)
- 通过线程上下文类加载器(TCCL):如 JDBC 加载驱动时由调用方提供加载器,绕过父类加载器
示例(破坏):
@Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 不委派父类,直接查找 return findClass(name); }
破坏双亲委派会带来类冲突、核心类替换等风险,仅在确有必要时使用,如插件隔离、热部署等场景。
链表和数组的区别是什么?(金山云、银联)
链表与数组是两种基本的数据结构,在内存分配、访问效率、插入删除等方面存在本质区别。
对比维度 |
数组(Array) |
链表(LinkedList) |
内存结构 |
连续内存块 |
非连续,节点间通过指针连接 |
随机访问效率 |
高(O(1)) |
低(O(n)) |
插入删除 |
慢(需移动元素) |
快(修改引用) |
空间利用 |
固定大小或需扩容 |
动态分配 |
内存消耗 |
相对较低 |
多指针开销,消耗大 |
选择建议:
- 数据量小或频繁访问元素:选择数组;
- 数据量大或频繁插入删除:选择链表;
- 注意 Java 中的 ArrayList 和 LinkedList 就是两者的典型实现。
栈和队列的区别是什么?如何用栈实现队列?(斗鱼、昆仑万维)
特性 |
栈(Stack) |
队列(Queue) |
访问顺序 |
后进先出(LIFO) |
先进先出(FIFO) |
插入位置 |
栈顶 |
队尾 |
删除位置 |
栈顶 |
队头 |
使用两个栈实现队列思路:
- 入队
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
17年+码农经历了很多次面试,多次作为面试官面试别人,多次大数据面试和面试别人,深知哪些面试题是会被经常问到。 在多家企业从0到1开发过离线数仓实时数仓等多个大型项目,详细介绍项目架构等企业内部秘不外传的资料,介绍踩过的坑和开发干货,分享多个拿来即用的大数据ETL工具,让小白用户快速入门并精通,指导如何入职后快速上手。 计划更新内容100篇以上,包括一些企业内部秘不外宣的干货,欢迎订阅!