大数据面试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 类。

整体流程分为以下几个阶段:

  1. 加载(Loading)
  • 通过类的全限定名找到对应 .class 文件并读取字节流。
  • 将字节流转换为 JVM 能识别的 Class 对象。
  • 可能从本地磁盘、网络、Jar 包等加载。
  1. 验证(Verification)
  • 验证字节码是否合法,防止恶意代码。
  • 包括文件格式验证、元数据验证、字节码验证、符号引用验证。
  1. 准备(Preparation)
  • 为类的静态变量分配内存,并设置默认值(不包括 static 块)。
  • 不执行任何代码。
  1. 解析(Resolution)
  • 将常量池中的符号引用(符号名、方法名等)替换为直接引用(内存地址)。
  • 包括类或接口、字段、类方法、接口方法的解析。
  1. 初始化(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 等)底层实现的重要支撑。

一、反射的作用

  1. 运行时获取类的信息可在不知道类定义的前提下动态加载类、读取类名、方法、属性、注解等元数据。
  2. 动态调用方法和访问字段可用于实现通用框架,如 JSON 序列化、ORM 映射、IoC 容器、依赖注入等。
  3. 绕过封装访问私有成员通过 setAccessible(true) 访问私有属性、方法,增强灵活性。
  4. 动态创建对象无需使用 new,通过构造方法反射调用实例化对象,适用于插件机制、工厂模式。
  5. 应用场景示例序列化与反序列化(如 FastJSON、Jackson);JDK 动态代理;自动装配(如 Spring 注入 Bean);测试框架自动识别 @Test 方法;配置驱动型开发(如读取配置类中字段注解来做注入)。

二、反射的实现原理

Java 反射的核心在于 java.lang.reflect 包下的类:ClassFieldMethodConstructor 等。其本质是操作 JVM 在加载类之后生成的 Class 对象,并操纵其元数据。

  1. Class 对象与类加载每个类加载后,JVM 都会为其生成唯一的 Class 类对象。通过 Class.forName("类名") 或 对象.getClass() 获取该对象。
  2. 反射访问流程:
  3. 访问私有字段/方法:
  4. 反射是如何实现的?JVM 类加载器在加载类时,解析字节码生成 Class 元数据对象。反射通过读取该结构体提供的元信息进行操作。虽然运行期通过 JNI 接口或 Unsafe 操作底层内存,但大多数应用级使用只调用公开 API。

三、反射的性能与安全性问题

  • 性能开销:反射是解释执行,通常比直接调用慢 10 倍左右;频繁使用建议结合缓存(如 Method 缓存)优化。
  • 可维护性差:编译器不会校验方法名、参数,易出错且不易发现,影响重构。
  • 安全性问题:可绕过访问控制机制,可能带来数据泄露、安全漏洞等风险。

四、Java 反射相关 API 简表

Class

表示类或接口的运行时表示

Field

表示类的成员变量

Method

表示类的方法

Constructor

表示类的构造方法

Array

提供动态创建数组的静态方法

Modifier

提供方法或字段修饰符的解码工具类

Runnable 和 Callable 接口的区别是什么?(水滴)

Java 中的 RunnableCallable 都是用于表示并发任务的接口,通常与线程或线程池结合使用,但它们在功能、使用方式和适用场景上存在明显区别。

核心区别对比:

所在包

java.lang

java.util.concurrent

是否有返回值

是(使用泛型指定返回类型)

是否抛出异常

不能抛出受检异常

可以抛出受检异常

方法名称

void run()

V call() throws Exception

与线程关系

可作为 Thread 构造函数参数

必须结合 FutureTask 或线程池使用

线程池使用

Executor.execute(Runnable)

ExecutorService.submit(Callable)

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 = 5b = 5a == b的结果为true,因为它们的值是相同的。对于引用数据类型,==比较的是两个对象的引用是否相同,也就是看这两个变量是否指向内存中的同一个对象。例如,有两个String对象str1 = new String("Hello");str2 = new String("Hello");str1 == str2的结果为false,因为尽管它们的内容相同,但它们是两个不同的对象,在内存中有不同的存储位置。

equals()Object类中的一个方法,所有的类都继承自Object类,所以所有的对象都有equals()方法。在Object类中,equals()方法的默认实现实际上和==运算符对于引用比较的行为是一样的。但是,很多类会重写equals()方法来提供更符合业务逻辑的比较方式。例如,String类重写了equals()方法,它会比较两个String对象的内容是否相同。对于前面提到的str1str2str1.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

加锁方式

隐式

显式,需要手动加锁和释放

是否可中断

是,

lockInterruptibly()

支持中断

是否公平锁

否,默认非公平

可选,支持公平或非公平锁

是否支持尝试加锁

是,

tryLock()

方法

是否支持条件变量

是,支持多个

Condition

性能表现

JVM 自动优化,如偏向锁、轻量级锁

控制更细粒度,适用于复杂同步场景

示例代码:

// synchronized
synchronized (this) {
    // 临界区代码
}

// ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock();
}

选择建议:

  • 简单同步使用 synchronized 即可,语义清晰,避免死锁风险;
  • 对并发控制要求高,或需中断锁/尝试锁/多个条件的复杂业务逻辑场景,建议使用 ReentrantLock;

JDK 1.6 之后对 synchronized 的性能做了大量优化(偏向锁、轻量级锁等),在多数情况下性能与 ReentrantLock 区别不大。

多态的概念及实现方式是什么?(小鹏汽车、银联)

多态是面向对象编程的三大特性之一,指的是同一操作作用于不同对象时,可以产生不同的行为。在 Java 中,多态分为编译时多态(方法重载)和运行时多态(方法重写)。

实现多态的三个条件:

  1. 有继承关系
  2. 子类重写父类方法
  3. 父类引用指向子类对象
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 引用)

对象回收流程如下:

  1. 从 GC Roots 开始向下搜索
  2. 若对象不可达,则标记为“可回收”
  3. 若对象重写了 finalize() 方法,且从未被调用过,JVM 会将其放入 F-Queue 并执行一次机会
  4. 若该对象在 finalize() 中复活(如将自己赋值给某全局引用),则取消回收;否则回收

引用类型判断规则:

  • 强引用(Strong Reference):正常引用关系,不回收
  • 软引用(SoftReference):内存不足时回收
  • 弱引用(WeakReference):下一次 GC 即回收
  • 虚引用(PhantomReference):不能通过它访问对象,仅用于跟踪回收

常见的垃圾回收算法有哪些?(shein、昆仑万维)

JVM 中常见的垃圾回收算法如下:

  1. 标记-清除(Mark-Sweep)首先标记所有存活对象,然后清除未被标记的对象缺点:空间碎片较多,影响后续内存分配
  2. 复制算法(Copying)将内存分为两块,每次只用一块,将存活对象复制到另一块,再清空当前内存适用于对象生命周期短的场景,如新生代回收
  3. 标记-压缩(Mark-Compact)标记存活对象后将其向内存一端移动,清除边界外的空间用于老年代,解决空间碎片问题
  4. 分代收集算法(Generational GC)将内存划分为新生代(Eden + Survivor)、老年代、元空间;根据对象生命周期不同采用不同回收策略新生代使用复制算法,老年代使用标记-压缩或标记-清除

这些算法在实际 GC 实现中通常是组合使用的,如 G1 GC、ZGC、Shenandoah GC 等。

内存溢出(OOM)的常见场景及排查方法是什么?(携程、昆仑万维)

OOM(OutOfMemoryError)是 JVM 在申请内存时无法满足请求而抛出的严重错误,通常发生在堆、方法区、直接内存或本地线程等区域。

常见 OOM 场景:

  1. 堆内存溢出(Java heap space):创建大量对象或缓存,垃圾回收无法及时清理。
  2. 方法区(Metaspace)溢出:动态生成过多类,如频繁使用反射、动态代理或 CGLIB 动态生成类。
  3. 虚拟机栈溢出:栈帧太多导致栈空间耗尽,但通常报 StackOverflowError 而非 OOM。
  4. 本地内存溢出(Direct buffer memory):使用 NIO 分配堆外内存未及时释放。
  5. 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 类加载机制的一种规范,用于保证核心类的优先加载、防止重复加载。

模型原理:

  1. 类加载器分为 启动类加载器(Bootstrap)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader);
  2. 每个类加载器在加载类时,先将请求委派给父类加载器;父加载器若无法加载,才由当前加载器尝试加载。

示意流程: 应用类加载器 → 扩展类加载器 → 启动类加载器(从上往下委派)

优点:

  • 避免重复加载
  • 保证 Java 核心类的安全性,如不会被用户代码篡改 java.lang.String

破坏双亲委派的方式:

  1. 自定义类加载器重写 loadClass() 方法:不调用 super.loadClass(),直接使用 findClass()
  2. 使用第三方框架(如 SPI、OSGi):部分模块化系统允许子类优先加载(打破委派)
  3. 通过线程上下文类加载器(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篇以上,包括一些企业内部秘不外宣的干货,欢迎订阅!

全部评论

相关推荐

07-11 19:33
门头沟学院 Java
你怎么知道我今天git本地代码覆盖了公司的仓库代码?以前一直以为技术岗技术为王,现在深刻认识到一个人的综合实力强才是真的强。实习日记,今天也是很深刻体会到为什么说技术岗的软实力也是很重要的一个因素。Monitor本来就讲话不是很清楚,我们同组实习生都在吐槽他,我后我又是第一次接触这个新的业务,我也是那种很喜欢刨根问底的人,所以很多时候跟monitor不在一个频道上面,导致一个需求需要沟通很久。我的思维习惯是接收概念,然后自己理解之后再立马去确认是否正确。有点类似于tcc。Monitor的思维习惯是他讲一个东西,喜欢延伸到很多很远的东西,很适合那种和人聊天,喝茶,高谈阔论的那种。有点类似于sagas。因为我完全没有接触过这方面的业务,所以他讲一个东西,对于我来说就是一个新的东西,新的概念。然后呢,他直接跟我讲一个事物的逻辑,然而我却想要马上确认一下这一个事物的逻辑,每一个概念分别是什么意思,有什么作用,在这个逻辑上面会起到什么作用。然后又因为我是新接触的,所以很多时候会有理解偏差,然后monitor就会觉得我怎么这都听不懂,然后又要重头再讲一遍,相当于我每确认一次概念,他都要从头把业务流程再讲一遍,我只能够等他讲完业务流程之后我再确认我的概念有没有正确形成一个这样子的一个循环。现在看来,我已经算是理解这个业务了。其实这件事情本来就挺简单的。技术实现也不难。但是就是涉及到一个人与人沟通的问题,你不能只是坚持你自己的一个思维习惯,你要去能包容人家的思维习惯,并且尝试从人家的思维习惯里面去推演出来一个模板模型。沟通也许算是一种软实力吧,能够很清楚的把事情给讲明白,讲的通俗易懂,一针见血。Monitor是那种比较系统性的就给我解释一些东西,我是我属于那种应用性的,我只在乎什么东西对我的这个业务实现是有用的,我就只管它,我以很多时候我觉得他讲不到重点,他觉得我理解有问题。虽然这是语音转文字,但是我也看出来其实我自己表达也有一些问题。唉,要好好研究一下表达的艺术了。然后我又是那种基本每天提早一小时到岗然后下班自愿无偿加班一两个小时的人,也许因此mt还算对我有点耐心吧。一想起几乎比我早进来的实习生基本都犯过git本地直接全部覆盖远程仓库代码的错误,我就感觉少一丝愧疚了。
来offer来oc:看到monitor还以为是八股文了
牛客在线求职答疑中心
点赞 评论 收藏
分享
面试了几家,全程问项目,八股一点都不问,可惜准备了这么久
独角仙梦境:现在感觉问八股像是中场休息一样的,问几个八股放松一下再上强度
我的求职思考
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务