互联网大厂面试:Java核心、JUC、JVM等技术大考验
严肃的面试官坐在桌前,面前放着王铁牛的简历,表情认真。王铁牛则有些紧张地坐在对面,搓着手。
第一轮面试
面试官:“我们先从Java核心知识开始。Java中基本数据类型有哪些?” 王铁牛:“嗯,有 byte、short、int、long、float、double、char、boolean。” 面试官:“不错,回答得很准确。那 String 是基本数据类型吗?” 王铁牛:“不是,String 是引用数据类型。” 面试官:“很好。那说说 Java 中多态的实现方式有哪些?” 王铁牛:“主要有继承和接口实现。子类重写父类的方法,或者类实现接口中的抽象方法,通过父类引用指向子类对象或者接口引用指向实现类对象来实现多态。” 面试官:“非常棒,基础很扎实。接下来,讲讲 Java 中的访问修饰符有哪些,分别有什么作用?” 王铁牛:“有 public、protected、default(不写修饰符)和 private。public 可以被任何类访问;protected 可以被同一个包内的类以及不同包的子类访问;default 只能被同一个包内的类访问;private 只能在本类中访问。”
第二轮面试
面试官:“现在我们聊聊 JUC 相关的。说说 CountDownLatch 是什么,有什么应用场景?” 王铁牛:“呃……这个好像是和线程同步有关的,应用场景嘛,我想想……好像是在多个线程执行完任务后再进行后续操作。” 面试官:“你说对了一部分。那 CyclicBarrier 呢,它和 CountDownLatch 有什么区别?” 王铁牛:“这个……我有点不太清楚,好像也是和线程同步有关,但具体区别我说不上来。” 面试官:“看来你这方面掌握得不太好。那 Fork/Join 框架是做什么的?” 王铁牛:“我记得是用来并行执行任务的,但具体怎么用我不太懂。” 面试官:“嗯,后续需要加强这方面的学习。再问你,ReentrantLock 和 synchronized 有什么区别?” 王铁牛:“好像 ReentrantLock 更灵活一些,但具体的我也说不明白。”
第三轮面试
面试官:“接下来是关于 JVM 的问题。说说 JVM 的内存区域划分。” 王铁牛:“有堆、栈、方法区,还有程序计数器和本地方法栈。” 面试官:“回答得不错。那堆内存又可以细分为哪些区域?” 王铁牛:“有新生代、老年代,新生代又可以分为 Eden 区和两个 Survivor 区。” 面试官:“很好。那 JVM 的垃圾回收算法有哪些?” 王铁牛:“有标记 - 清除、标记 - 整理、复制算法。” 面试官:“看来你对 JVM 基础还是有一定了解的。那说说 CMS 垃圾回收器的工作流程。” 王铁牛:“这个……我只知道它是一种并发的垃圾回收器,具体流程我不太清楚。”
面试官:“今天的面试就到这里,你可以回家等通知。整体来看,你对 Java 核心知识和 JVM 基础有一定的掌握,回答得不错,这方面值得肯定。但在 JUC 相关知识上,明显掌握得不够扎实,很多问题回答得不够清晰。后续你可以深入学习 JUC 相关内容,提升自己的技术能力。我们会综合考虑你的表现,尽快给你答复。”
答案详解
- Java 基本数据类型:
- Java 中有 8 种基本数据类型,分别是 byte(1 字节)、short(2 字节)、int(4 字节)、long(8 字节)、float(4 字节)、double(8 字节)、char(2 字节)、boolean(理论上 1 位,但实际实现因不同 JVM 而异)。这些基本数据类型用于存储不同类型的数据,如整数、浮点数、字符和布尔值。
- String 不是基本数据类型:
- String 是引用数据类型,它是 Java 中用于表示字符串的类。引用数据类型在内存中存储的是对象的引用,而不是对象本身。
- Java 多态的实现方式:
- 继承:子类继承父类,并重写父类的方法。通过父类引用指向子类对象,在调用重写的方法时,会根据实际指向的子类对象来执行相应的方法。
- 接口实现:类实现接口中的抽象方法,通过接口引用指向实现类对象,同样可以实现多态。
- Java 访问修饰符:
- public:可以被任何类访问,没有访问限制。
- protected:可以被同一个包内的类以及不同包的子类访问。
- default(不写修饰符):只能被同一个包内的类访问。
- private:只能在本类中访问,其他类无法直接访问。
- CountDownLatch:
- 是 JUC 包中的一个同步工具类,它允许一个或多个线程等待其他线程完成操作。通过一个计数器来实现,初始化时设置计数器的值,每个线程完成任务后调用 countDown() 方法将计数器减 1,当计数器的值为 0 时,等待的线程可以继续执行。应用场景如多个线程同时执行不同的任务,主线程需要等待所有子线程完成后再进行后续操作。
- CyclicBarrier 与 CountDownLatch 的区别:
- CountDownLatch:计数器的值只能使用一次,当计数器减到 0 后,无法重置。主要用于一个或多个线程等待其他线程完成操作。
- CyclicBarrier:计数器可以循环使用,当所有线程都到达屏障点后,计数器会重置。主要用于多个线程相互等待,直到所有线程都到达某个状态后再继续执行。
- Fork/Join 框架:
- 是 Java 7 引入的一个并行执行任务的框架,它将一个大任务拆分成多个小任务,然后并行执行这些小任务,最后将小任务的结果合并得到大任务的结果。适用于可以分解成多个子任务的计算密集型任务。
- ReentrantLock 和 synchronized 的区别:
- 灵活性:ReentrantLock 更加灵活,它可以实现公平锁和非公平锁,还可以通过 lockInterruptibly() 方法响应中断,而 synchronized 是隐式锁,不具备这些特性。
- 锁的获取和释放:synchronized 是自动获取和释放锁,而 ReentrantLock 需要手动调用 lock() 和 unlock() 方法来获取和释放锁。
- 锁的状态判断:ReentrantLock 可以通过 isLocked() 方法判断锁的状态,而 synchronized 无法直接判断。
- JVM 内存区域划分:
- 堆:是 JVM 中最大的一块内存区域,用于存储对象实例。
- 栈:分为虚拟机栈和本地方法栈,虚拟机栈用于存储局部变量表、操作数栈等信息,每个线程都有自己的虚拟机栈;本地方法栈用于执行本地方法。
- 方法区:用于存储类的信息、常量、静态变量等。
- 程序计数器:记录当前线程执行的字节码指令的地址。
- 本地方法栈:与虚拟机栈类似,不过是为本地方法服务的。
- 堆内存细分区域:
- 新生代:对象刚创建时通常会被分配到新生代,新生代又分为 Eden 区和两个 Survivor 区(通常比例为 8:1:1)。
- 老年代:当对象在新生代经过多次垃圾回收仍然存活时,会被晋升到老年代。
- JVM 垃圾回收算法:
- 标记 - 清除:先标记出需要回收的对象,然后清除这些对象。缺点是会产生内存碎片。
- 标记 - 整理:先标记出需要回收的对象,然后将存活的对象移动到一端,最后清除边界以外的内存。避免了内存碎片的问题。
- 复制算法:将内存分为两块,每次只使用其中一块,当这块内存满了之后,将存活的对象复制到另一块内存中,然后清除原来的内存。适用于对象存活率较低的场景。
- CMS 垃圾回收器工作流程:
- 初始标记:标记 GC Roots 能直接关联到的对象,这个阶段需要暂停所有用户线程(Stop The World),速度很快。
- 并发标记:从 GC Roots 的直接关联对象开始遍历整个对象图,这个阶段可以和用户线程并发执行。
- 重新标记:修正并发标记阶段因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段也需要暂停所有用户线程,时间比初始标记阶段长,但比并发标记阶段短。
- 并发清除:清除标记为需要回收的对象,这个阶段可以和用户线程并发执行。