《互联网大厂面试:Java 核心、JUC、JVM 等技术深度考察》

38 阅读14分钟

互联网大厂面试:Java 核心、JUC、JVM 等技术深度考察

在互联网大厂的一间明亮的面试室内,严肃的面试官正襟危坐,对面坐着紧张又期待的求职者王铁牛。一场对 Java 核心知识等多方面技术的面试拉开了帷幕。

第一轮提问 面试官:首先,我想问一下,Java 中基本数据类型有哪些? 王铁牛:Java 中的基本数据类型有 byte、short、int、long、float、double、char、boolean。 面试官:不错,回答得很准确。那在 Java 里,什么是面向对象的三大特性? 王铁牛:面向对象的三大特性是封装、继承和多态。封装是把数据和操作数据的方法绑定起来,隐藏内部实现细节;继承是子类可以继承父类的属性和方法;多态是同一个行为具有多个不同表现形式或形态的能力。 面试官:非常好,理解得很透彻。那说说 Java 中异常处理机制是怎样的? 王铁牛:Java 的异常处理机制主要通过 try、catch、finally 关键字来实现。try 块里放可能会抛出异常的代码,catch 块用来捕获并处理异常,finally 块里的代码无论是否发生异常都会执行。

第二轮提问 面试官:进入 JUC 相关的问题。JUC 里的 CountDownLatch 是做什么用的? 王铁牛:CountDownLatch 可以让一个或多个线程等待其他线程完成操作。它有一个计数器,当计数器减到 0 时,等待的线程就会被唤醒继续执行。 面试官:回答得很好。那 CyclicBarrier 和 CountDownLatch 有什么区别? 王铁牛:嗯……好像有点区别,但是我不太能说清楚。大概就是都能让线程等待,但是具体怎么不一样我不太知道。 面试官:没关系,那说说 JUC 里的线程池 ExecutorService 有哪些创建方式? 王铁牛:可以通过 Executors 工具类创建,比如 newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 这些。 面试官:不错。那使用线程池有什么好处? 王铁牛:可以减少创建和销毁线程的开销,提高性能,还能更好地管理线程。

第三轮提问 面试官:接下来聊聊 JVM。JVM 的内存结构是怎样的? 王铁牛:JVM 内存主要分为堆、栈、方法区等。堆是存放对象实例的地方,栈是存储局部变量和方法调用信息的,方法区存放类的信息、常量等。 面试官:回答得还行。那 JVM 的垃圾回收机制是如何工作的? 王铁牛:就是把不用的对象回收掉嘛,具体怎么判断不用我不太清楚,好像有什么标记清除啥的。 面试官:不太准确。那说说常见的垃圾回收器有哪些? 王铁牛:这个我知道一点,有 Serial 回收器、Parallel 回收器,还有 CMS 回收器。

面试结束 面试官推了推眼镜,说道:“今天的面试就到这里,你对一些基础的 Java 知识掌握得还可以,像 Java 基本数据类型、面向对象特性、异常处理机制,以及 JUC 里 CountDownLatch 和线程池的部分内容都回答得不错。但是对于一些稍微复杂的问题,比如 CyclicBarrier 和 CountDownLatch 的区别、JVM 垃圾回收机制的详细工作原理,回答得不够清晰和准确。我们后续会综合评估所有面试者的情况,你回家等通知吧。”

问题答案详解

  1. Java 中基本数据类型有哪些? Java 中有 8 种基本数据类型,可分为 4 类:

    • 整数类型
      • byte:8 位,有符号,范围是 -128 到 127。
      • short:16 位,有符号,范围是 -32768 到 32767。
      • int:32 位,有符号,范围是 -2147483648 到 2147483647。
      • long:64 位,有符号,范围是 -9223372036854775808 到 9223372036854775807,定义时需在数字后面加“L”。
    • 浮点类型
      • float:32 位,单精度浮点数,定义时需在数字后面加“F”。
      • double:64 位,双精度浮点数,是 Java 中默认的浮点类型。
    • 字符类型
      • char:16 位,用于表示单个字符,使用单引号括起来,如 'A'。
    • 布尔类型
      • boolean:只有两个值,true 和 false,用于逻辑判断。
  2. 在 Java 里,什么是面向对象的三大特性?

    • 封装:将数据(属性)和操作数据的方法捆绑在一起,隐藏对象的内部实现细节,只对外提供必要的接口。这样可以提高代码的安全性和可维护性,防止外部代码随意访问和修改对象的内部状态。例如,一个类的私有属性可以通过公有的 getter 和 setter 方法来访问和修改。
    • 继承:子类可以继承父类的属性和方法,从而实现代码的复用和扩展。子类可以重写父类的方法,以实现自己的特定行为。通过继承,形成了类的层次结构,提高了代码的可扩展性。例如,狗类可以继承动物类,狗类会拥有动物类的一些通用属性和方法,同时还可以有自己独特的属性和方法。
    • 多态:同一个行为具有多个不同表现形式或形态的能力。多态通过继承和接口实现,主要有方法重载和方法重写两种方式。方法重载是指在一个类中可以有多个方法名相同但参数列表不同的方法;方法重写是指子类重写父类的方法。多态可以提高代码的灵活性和可维护性,使得代码可以根据对象的实际类型来调用相应的方法。
  3. 说说 Java 中异常处理机制是怎样的? Java 的异常处理机制主要通过 try、catch、finally、throw 和 throws 关键字来实现。

    • try 块:用于包含可能会抛出异常的代码。当代码在 try 块中执行时,如果发生异常,程序会立即跳转到相应的 catch 块进行异常处理。
    • catch 块:用于捕获并处理 try 块中抛出的异常。catch 块后面跟着要捕获的异常类型,当 try 块中抛出的异常类型与 catch 块指定的异常类型匹配时,该 catch 块会被执行。可以有多个 catch 块来捕获不同类型的异常。
    • finally 块:无论 try 块中是否发生异常,finally 块中的代码都会被执行。通常用于释放资源,如关闭文件、数据库连接等。
    • throw 关键字:用于在方法内部手动抛出一个异常对象。
    • throws 关键字:用于在方法声明中声明该方法可能会抛出的异常类型,调用该方法的代码需要处理这些异常。
  4. JUC 里的 CountDownLatch 是做什么用的? CountDownLatch 是 Java 并发包(JUC)中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。它有一个初始计数器,创建 CountDownLatch 对象时需要指定计数器的初始值。当一个线程完成了自己的任务后,可以调用 CountDownLatch 的 countDown() 方法将计数器减 1。其他线程可以调用 await() 方法来等待计数器变为 0,当计数器变为 0 时,等待的线程会被唤醒继续执行。例如,在一个多线程任务中,主线程需要等待所有子线程完成任务后再继续执行,就可以使用 CountDownLatch 来实现。

  5. CyclicBarrier 和 CountDownLatch 有什么区别?

    • 计数机制
      • CountDownLatch 的计数器是递减的,初始设置一个值,线程完成任务后调用 countDown() 方法使计数器减 1,计数器减到 0 时,等待的线程被唤醒。
      • CyclicBarrier 的计数器是递增的,初始设置一个值,表示需要等待的线程数量,当有线程调用 await() 方法时,计数器加 1,当计数器达到初始值时,所有等待的线程会同时被释放,并且计数器可以重置,继续下一轮的等待。
    • 使用场景
      • CountDownLatch 主要用于一个或多个线程等待其他线程完成任务的场景,例如主线程等待多个子线程完成数据加载后再进行后续处理。
      • CyclicBarrier 主要用于多个线程之间相互等待,直到所有线程都到达某个屏障点后再同时继续执行,例如多个运动员在起跑线等待,当所有运动员都准备好后同时起跑。
    • 可重用性
      • CountDownLatch 的计数器一旦减到 0 就不能再使用,不能重置。
      • CyclicBarrier 的计数器可以重置,因此可以重复使用。
  6. JUC 里的线程池 ExecutorService 有哪些创建方式? 可以通过 Executors 工具类来创建不同类型的线程池:

    • newFixedThreadPool(int nThreads):创建一个固定大小的线程池,线程池中的线程数量始终保持为指定的 nThreads。当有新任务提交时,如果线程池中有空闲线程,则立即执行任务;如果没有空闲线程,则任务会被放入任务队列中等待。
    • newCachedThreadPool():创建一个可缓存的线程池,线程池中的线程数量会根据任务的数量自动调整。如果有新任务提交,且线程池中有空闲线程,则使用空闲线程执行任务;如果没有空闲线程,则创建一个新线程来执行任务。当线程空闲一段时间后,会被自动回收。
    • newSingleThreadExecutor():创建一个单线程的线程池,线程池中只有一个线程来执行任务。所有任务会按照提交的顺序依次执行,保证了任务的顺序性。
    • newScheduledThreadPool(int corePoolSize):创建一个可定时执行任务的线程池,线程池中的核心线程数量为指定的 corePoolSize。可以使用该线程池来执行定时任务或周期性任务。
  7. 使用线程池有什么好处?

    • 减少开销:创建和销毁线程需要消耗系统资源,使用线程池可以复用已经创建的线程,减少了线程创建和销毁的开销,提高了系统的性能。
    • 提高响应速度:当有新任务提交时,线程池中如果有空闲线程,可以立即执行任务,无需等待新线程的创建,从而提高了系统的响应速度。
    • 更好的线程管理:线程池可以对线程进行统一的管理,包括线程的数量控制、任务队列的管理等。可以根据系统的实际情况调整线程池的参数,避免创建过多的线程导致系统资源耗尽。
    • 提供定时和周期性任务执行功能:如 ScheduledThreadPoolExecutor 可以方便地实现定时任务和周期性任务的执行。
  8. JVM 的内存结构是怎样的? JVM 内存主要分为以下几个区域:

    • 堆(Heap):是 JVM 中最大的一块内存区域,用于存放对象实例和数组。堆是所有线程共享的,垃圾回收主要就是针对堆进行的。堆可以分为新生代和老年代,新生代又可以分为 Eden 区和两个 Survivor 区。
    • 栈(Stack):每个线程都有自己独立的栈,用于存储局部变量、方法调用信息和操作数栈等。栈中的数据是线程私有的,每个方法在执行时会创建一个栈帧,栈帧中包含了该方法的局部变量表、操作数栈、动态链接和方法出口等信息。方法执行完成后,栈帧会被弹出。
    • 方法区(Method Area):是所有线程共享的内存区域,用于存储类的信息(如类的结构、常量池、字段和方法数据等)、静态变量等。在 JDK 1.8 之前,方法区也被称为永久代;JDK 1.8 及以后,方法区被元空间(Metaspace)取代,元空间使用的是本地内存。
    • 程序计数器(Program Counter Register):是一块较小的内存区域,每个线程都有自己独立的程序计数器。它用于记录当前线程执行的字节码指令的地址,是线程私有的。
    • 本地方法栈(Native Method Stack):与栈类似,不过它是为执行本地方法(使用 native 关键字修饰的方法)服务的,也是线程私有的。
  9. JVM 的垃圾回收机制是如何工作的? JVM 的垃圾回收机制主要包括以下几个步骤:

    • 对象存活判断
      • 引用计数法:给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器加 1;当引用失效时,计数器减 1。当计数器为 0 时,认为该对象可以被回收。但这种方法无法解决循环引用的问题。
      • 可达性分析法:从一系列被称为“GC Roots”的对象开始,通过引用关系向下搜索,能被搜索到的对象被认为是可达的,即存活对象;无法被搜索到的对象则被认为是不可达的,可以被回收。GC Roots 包括虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象等。
    • 垃圾回收算法
      • 标记 - 清除算法:首先标记出所有需要回收的对象,然后统一回收这些对象。该算法的缺点是会产生大量的内存碎片,导致后续分配大对象时可能无法找到连续的内存空间。
      • 标记 - 整理算法:先标记出需要回收的对象,然后将存活的对象向一端移动,最后清理掉边界以外的内存。这种算法解决了内存碎片的问题,但移动对象会带来一定的性能开销。
      • 复制算法:将内存分为大小相等的两块,每次只使用其中一块。当这一块内存用完后,将存活的对象复制到另一块内存中,然后清空当前使用的这块内存。该算法的优点是不会产生内存碎片,缺点是可用内存空间减少了一半。
      • 分代收集算法:根据对象的存活周期将内存分为不同的区域,一般分为新生代和老年代。新生代中对象的存活时间较短,使用复制算法;老年代中对象的存活时间较长,使用标记 - 清除或标记 - 整理算法。
    • 垃圾回收器:不同的垃圾回收器实现了不同的垃圾回收算法,如 Serial 回收器、Parallel 回收器、CMS 回收器、G1 回收器等。垃圾回收器会根据不同的场景和需求选择合适的算法进行垃圾回收。
  10. 常见的垃圾回收器有哪些?

    • Serial 回收器:是最基本、历史最悠久的垃圾回收器,它是单线程的,在进行垃圾回收时,会暂停所有用户线程(Stop The World)。适用于客户端应用程序或对内存占用较小的场景。
    • Parallel 回收器:也是基于复制算法的多线程垃圾回收器,它在进行垃圾回收时也会暂停用户线程,但可以利用多个 CPU 核心并行进行垃圾回收,提高了垃圾回收的效率。适用于对吞吐量要求较高的场景。
    • CMS(Concurrent Mark Sweep)回收器:是一种以获取最短回收停顿时间为目标的垃圾回收器,它的工作过程主要分为初始标记、并发标记、重新标记和并发清除四个阶段。其中初始标记和重新标记阶段需要暂停用户线程,并发标记和并发清除阶段可以与用户线程并发执行,因此可以减少垃圾回收对应用程序的影响。但 CMS 回收器会产生内存碎片,并且在并发清除阶段可能会出现“浮动垃圾”问题。
    • G1(Garbage - First)回收器:是一种面向服务器端应用的垃圾回收器,它将堆内存划分为多个大小相等的 Region,根据每个 Region 中垃圾的数量和回收成本,优先回收垃圾最多的 Region。G1 回收器可以实现低停顿的垃圾回收,并且可以较好地控制内存碎片问题。适用于大内存、多 CPU 的服务器场景。