互联网大厂面试:Java 核心、JUC、JVM 等知识大考验
互联网大厂的面试室内,气氛紧张而严肃。面试官正襟危坐,面前摆放着求职者的简历,而坐在对面的王铁牛,虽然表面故作镇定,但双手不自觉地捏着衣角,透露出他内心的紧张。
第一轮面试开始 面试官:“先问几个 Java 核心知识的问题。Java 中基本数据类型有哪些?” 王铁牛:“有 byte、short、int、long、float、double、char、boolean。” 面试官:“回答正确。那在 Java 里,什么是自动装箱和拆箱?” 王铁牛:“自动装箱就是把基本数据类型转换为对应的包装类类型,拆箱就是把包装类类型转换为基本数据类型。比如 Integer i = 10 就是自动装箱,int j = i 就是拆箱。” 面试官:“非常好。那说说 Java 中的多态是怎么实现的?” 王铁牛:“多态主要通过继承、接口和方法重写来实现。父类的引用可以指向子类的对象,调用重写的方法时会根据实际的对象类型来执行不同的逻辑。” 面试官:“回答得很清晰,看来你对 Java 核心知识掌握得不错。”
第二轮面试开始 面试官:“接下来聊聊 JUC 相关的。JUC 里的 CountDownLatch 有什么作用?” 王铁牛:“它主要用于让一个或多个线程等待其他线程完成操作。就像比赛时,所有运动员都跑完了,裁判才宣布比赛结束。” 面试官:“理解得挺形象。那 CyclicBarrier 和 CountDownLatch 有什么区别?” 王铁牛:“嗯……好像 CyclicBarrier 可以循环使用,而 CountDownLatch 不能。”(回答得有点犹豫) 面试官:“对,还有其他区别吗?那说说 JUC 里的 Semaphore 是做什么的?” 王铁牛:“这个……好像是控制并发线程数量的,但具体怎么用我有点模糊了。”
第三轮面试开始 面试官:“现在谈谈 JVM。JVM 的内存模型是怎样的?” 王铁牛:“有堆、栈、方法区等。堆主要存放对象实例,栈存放局部变量等。” 面试官:“大致对了。那 JVM 的垃圾回收机制是怎样的?有哪些常见的垃圾回收算法?” 王铁牛:“垃圾回收就是回收不再使用的对象内存。算法嘛,有标记清除、标记整理、复制算法,不过具体原理我不太清楚。” 面试官:“再问一个,什么情况下会触发 Full GC?” 王铁牛:“这个……好像是内存不足的时候,但不太确定。”
面试结束 面试官放下手中的笔,看着王铁牛说:“今天的面试就到这里了。你对一些基础的知识点掌握得还可以,但对于一些深入的问题,回答得不够准确和完整。我们需要综合评估一下,你先回家等通知吧。”
问题答案
- Java 中基本数据类型有哪些?
Java 中的基本数据类型分为四类八种:
- 整数类型:byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)。
- 浮点类型:float(4 字节)、double(8 字节)。
- 字符类型:char(2 字节)。
- 布尔类型:boolean(理论上 1 位,但实际实现中可能是 1 字节)。
- 在 Java 里,什么是自动装箱和拆箱?
自动装箱是 Java 编译器在基本数据类型和对应的包装类之间进行的自动转换。例如,当把一个基本数据类型赋值给对应的包装类对象时,会自动进行装箱操作,如
Integer i = 10,这里编译器会自动将10转换为Integer对象。 拆箱则相反,当把一个包装类对象赋值给基本数据类型时,会自动进行拆箱操作,如int j = i,编译器会将Integer对象i转换为基本数据类型int。 - 说说 Java 中的多态是怎么实现的?
Java 多态的实现主要基于以下几种方式:
- 继承:通过子类继承父类,子类可以重写父类的方法。例如:
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
- **接口**:一个类可以实现多个接口,不同的类实现同一个接口的方法可以有不同的实现逻辑。例如:
interface Shape {
double area();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double length;
private double width;
public Rectangle(double length, double width) {
this.length = length;
this.width = width;
}
@Override
public double area() {
return length * width;
}
}
- **方法重写**:子类重写父类的方法,在运行时,根据实际对象的类型来决定调用哪个方法。例如:
Animal animal = new Dog();
animal.makeSound(); // 输出 "Dog barks"
- JUC 里的 CountDownLatch 有什么作用?
CountDownLatch是一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。CountDownLatch有一个计数器,在初始化时指定一个初始值,每当一个线程完成任务时,计数器的值减 1。当计数器的值变为 0 时,等待的线程会被唤醒继续执行。例如:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " is working");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " has finished");
latch.countDown();
}).start();
}
latch.await();
System.out.println("All threads have finished, main thread can continue");
}
}
- CyclicBarrier 和 CountDownLatch 有什么区别?
- 可重用性:
CyclicBarrier可以循环使用,当所有线程到达屏障点后,屏障可以重置,再次使用;而CountDownLatch只能使用一次,计数器减到 0 后就不能再使用了。 - 作用对象:
CountDownLatch主要用于一个或多个线程等待其他线程完成操作;CyclicBarrier用于多个线程相互等待,当所有线程都到达屏障点后,才会继续执行后续操作。 - 计数器操作:
CountDownLatch的计数器是递减的,由其他线程调用countDown()方法来减少计数器的值;CyclicBarrier的计数器是递增的,当线程调用await()方法时,计数器加 1,直到达到指定的数量。
- 可重用性:
- 说说 JUC 里的 Semaphore 是做什么的?
Semaphore是一个计数信号量,用于控制同时访问某个资源的线程数量。它维护了一个许可证的数量,线程在访问资源前需要先获取许可证,如果许可证数量为 0,则线程需要等待,直到有其他线程释放许可证。例如:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(2); // 最多允许 2 个线程同时访问
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " has acquired the permit");
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " is releasing the permit");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
- JVM 的内存模型是怎样的?
JVM 的内存模型主要分为以下几个部分:
- 堆(Heap):是 JVM 中最大的一块内存区域,用于存放对象实例和数组。堆是所有线程共享的,垃圾回收主要针对堆进行。
- 栈(Stack):包括虚拟机栈和本地方法栈。虚拟机栈为 Java 方法服务,每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。本地方法栈为本地方法服务。
- 方法区(Method Area):用于存储类的信息、常量、静态变量、即时编译器编译后的代码等数据。在 JDK 1.8 之前,方法区也被称为永久代,JDK 1.8 及以后使用元空间(Metaspace)来替代永久代。
- 程序计数器(Program Counter Register):是一块较小的内存区域,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有自己独立的程序计数器。
- JVM 的垃圾回收机制是怎样的?有哪些常见的垃圾回收算法?
JVM 的垃圾回收机制主要是自动回收不再使用的对象所占用的内存,以避免内存泄漏和提高内存利用率。常见的垃圾回收算法有:
- 标记 - 清除算法(Mark - Sweep):首先标记出所有需要回收的对象,然后统一回收这些对象。该算法的缺点是会产生大量的内存碎片。
- 标记 - 整理算法(Mark - Compact):先标记出需要回收的对象,然后将存活的对象向一端移动,最后清理掉边界以外的内存。该算法解决了内存碎片的问题,但效率相对较低。
- 复制算法(Copying):将内存分为大小相等的两块,每次只使用其中一块。当这一块内存用完后,将存活的对象复制到另一块内存中,然后清理掉原来的内存。该算法效率较高,但内存利用率较低。
- 分代收集算法(Generational Collection):根据对象的存活周期将内存分为新生代和老年代。新生代使用复制算法,老年代使用标记 - 清除或标记 - 整理算法。
- 什么情况下会触发 Full GC?
以下几种情况可能会触发 Full GC:
- 老年代空间不足:当老年代没有足够的空间来分配新的对象时,会触发 Full GC 来清理老年代的内存。
- 永久代(JDK 1.8 之前)或元空间(JDK 1.8 及以后)空间不足:当永久代或元空间的内存不足时,会触发 Full GC 来回收这些区域的内存。
- System.gc() 调用:当调用
System.gc()方法时,会建议 JVM 进行一次 Full GC,但 JVM 不一定会立即执行。 - CMS GC 时并发模式失败:在使用 CMS 垃圾回收器时,如果并发标记和并发清除阶段失败,会触发 Full GC。
- 新生代晋升到老年代时老年代空间不足:当新生代的对象晋升到老年代时,如果老年代没有足够的空间,会触发 Full GC。