面试官:请简要介绍一下Java核心知识中面向对象的三大特性。
王铁牛:这个我知道,封装、继承、多态嘛。
面试官:不错,回答正确。那说说接口和抽象类的区别。
王铁牛:接口里的方法都是抽象的,不能有具体实现,而且接口可以多实现;抽象类里可以有抽象方法也可以有具体方法,一个类只能继承一个抽象类。
面试官:很好。再问你,什么是Java内存模型?
王铁牛:这个……我想想,就是描述了Java程序中各种变量(线程共享变量)在内存中的存储模型以及这些变量之间的读写访问规则。
第一轮结束。
面试官:讲讲JUC里的AQS。
王铁牛:AQS就是抽象队列同步器,它是构建锁和同步器的基础框架。
面试官:那说说ReentrantLock是如何基于AQS实现的?
王铁牛:呃……这个我不太清楚。
面试官:CountDownLatch和CyclicBarrier有什么区别?
王铁牛:这个……好像都能实现线程间的同步,具体区别我说不太明白。
第二轮结束。
面试官:JVM的内存结构分哪几个区域?
王铁牛:有堆、栈、方法区、程序计数器、本地方法栈。
面试官:详细说说堆内存的分代情况。
王铁牛:分新生代、老年代、永久代(现在叫元空间)。新生代又分伊甸园区、两个 Survivor 区。
面试官:什么情况下对象会进入老年代?
王铁牛:这个……我不太确定,好像是年龄达到一定次数或者占用空间超过一定比例。
第三轮结束。
面试结束,面试官表示会让王铁牛回家等通知。虽然王铁牛在回答过程中对一些复杂问题回答得不太清晰,但对基础知识还是有一定掌握的。后续需要看整体面试情况以及其他候选人的表现来决定是否录用王铁牛。
答案:
- Java核心知识中面向对象的三大特性:
- 封装:将对象的属性和行为包装在一起,对外提供统一的访问接口,隐藏内部实现细节,提高代码的安全性和可维护性。例如一个类中的成员变量使用private修饰,通过公共的getter和setter方法来访问和修改。
- 继承:允许一个对象直接使用另一对象的属性和方法。通过继承可以实现代码复用,提高开发效率。比如子类继承父类,子类可以继承父类的非私有属性和方法。
- 多态:指同一个行为具有多个不同表现形式或形态的能力。在Java中有方法重载(同一个类中)和方法重写(子类对父类方法的重新实现)两种体现形式。多态使得程序具有更好的扩展性和可维护性。
- 接口和抽象类的区别:
- 方法定义:接口里的方法都是抽象方法,不能有具体实现;抽象类里可以有抽象方法也可以有具体方法。
- 实现方式:一个类可以实现多个接口;一个类只能继承一个抽象类。
- 用途:接口常用于定义一种规范或能力,让不同类去实现;抽象类更侧重于定义一些公共的属性和行为,作为子类的公共基础。
- Java内存模型:描述了Java程序中各种变量(线程共享变量)在内存中的存储模型以及这些变量之间的读写访问规则。它定义了如何处理多线程之间的内存可见性、原子性等问题,确保Java程序在不同的硬件和操作系统环境下能正确运行。
- JUC里的AQS:抽象队列同步器,是构建锁和同步器的基础框架。它使用一个FIFO队列来管理等待获取锁的线程,通过一个int类型的状态变量来表示锁的状态。AQS提供了模板方法模式,子类可以通过继承它并重写一些方法来实现具体的同步逻辑,比如ReentrantLock、CountDownLatch、Semaphore等都是基于AQS实现的。
- ReentrantLock基于AQS的实现:ReentrantLock内部有一个Sync类,它继承自AQS。Sync有两个子类NonfairSync(非公平锁)和FairSync(公平锁)。当线程调用lock方法时,首先会尝试通过CAS操作修改AQS的状态值来获取锁,如果获取成功则直接返回;如果失败则将线程封装成Node节点加入到AQS的队列中等待唤醒。在释放锁时,会减少AQS的状态值,并唤醒队列中的后继节点。
- CountDownLatch和CyclicBarrier的区别:
- CountDownLatch:用于一个或多个线程等待其他线程完成一组操作之后再执行。它通过一个计数器来实现,调用countDown方法计数器减一,当计数器为0时,等待的线程被唤醒。例如,主线程等待多个子线程完成任务后再继续执行。
- CyclicBarrier:用于多个线程相互等待,直到所有线程都到达某个屏障点,然后再一起继续执行。它有一个固定的数量的参与者,当每个参与者都调用await方法时,计数器会减一,当计数器为0时,所有参与者被唤醒并继续执行。而且CyclicBarrier可以重用,而CountDownLatch不能。
- JVM的内存结构:
- 堆:是Java虚拟机所管理的内存中最大的一块,被所有线程共享,用于存放对象实例。堆内存又分为新生代、老年代、永久代(现在叫元空间)。新生代又分为伊甸园区、两个Survivor区。
- 栈:每个线程都有自己独立的栈空间,用于存储局部变量、方法调用等。栈内存中的数据存储是后进先出的。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 程序计数器:是一块较小的内存空间,它记录了当前线程所执行的字节码的行号指示器。
- 本地方法栈:与虚拟机栈类似,用于执行本地方法。
- 堆内存的分代情况:
- 新生代:主要存放新创建的对象,又分为伊甸园区(Eden)和两个Survivor区(一般叫from和to区)。新创建的对象首先存放在伊甸园区,当伊甸园区满了之后,会触发一次Minor GC(新生代垃圾回收),将存活的对象复制到其中一个Survivor区,经过多次Minor GC后,仍然存活的对象会被晋升到老年代。
- 老年代:存放经过多次Minor GC后仍然存活的对象,通常老年代的垃圾回收频率较低,但每次回收的时间较长。
- 永久代(元空间):用于存储类信息、常量、静态变量等。在Java 8中,永久代被元空间取代,元空间使用本地内存,而不是像永久代那样在JVM的堆内存中分配。
- 对象进入老年代的情况:
- 年龄达到一定次数:默认情况下,对象在经过15次Minor GC后仍然存活,就会被晋升到老年代。
- 占用空间超过一定比例:当新生代中相同年龄的对象所占用的空间大小超过了Survivor区的一半时,这些对象就会直接晋升到老年代。