vlambda博客
学习文章列表

面试官:谈谈你对 Java 平台的理解

本文约3100字,完整阅读大概会花费你「6分钟」左右的时间

文章导读

前言

面试的时候,经常会有面试官问:请你谈谈对 Java 平台的理解,「Java 是解释执行」,这句话正确吗?


其实这个问题,问得有点笼统。题目本身是非常开放的,往往考察的是多个方面,比如,基础知识理解是否很清楚;是否掌握 Java 平台主要模块和运行原理等。


个人认为,回答这类开放性问题的思路,可以从宏观的角度出发,从浅入深,由点到面。


总的来说可以从如下几个方面来回答:

  • 面向对象:封装、继承、多态;

  • 平台无关性:这个涉及到字节码,Java虚拟机等;

  • JVM:JVM 的一些基础概念、类加载机制、内存布局等;

  • 垃圾回收:垃圾回收的基本原理,常见的垃圾收集器以及适用的工作负载;

  • 语言特性:反射,泛型,Lambda 等;

  • 类库:JDK 提供的各种类库,重点包括集合、并发、IO/NIO、网络等;

  • 异常处理:Exception 和 Error;

  • 生态:Spring、SpringBoot等。

参考回答


Java语言是一种面向对象的高级语言,它最显著的有两个特性:

  • 一是通过平台中立的 class 文件格式和屏蔽底层硬件差异的 JVM 实现「一次编写,到处运行」;

  • 二是Java 通过垃圾收集器(Garbage Collector)回收分配内存,大部分情况下,程序员不需要自己操心内存的分配和回收。


Java 是一种简单、严谨并且适合编写的语言,它不像 C/C++ 那样有很多晦涩难懂的内容,如头文件、指针、结构等等。


我们日常会接触到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)。

  • JRE,也就是 Java 运行环境,包含了 JVM 和 Java 类库,以及一些模块等,比如:集合,泛型,反射,并发,网络,IO/NIO等。

  • 而 JDK 可以看作是 JRE 的一个超集,提供了更多工具,比如编译器、各种诊断工具等让java 语言更加安全、健壮。


Java 到底是解释执行还是编译执行?


这个问题并没有统一的答案,JVM 规范并没有强制要求 JVM 实现应该使用哪种方式来执行程序,只能说不同的JVM实现的方式不一样。有纯解释执行的、纯编译执行的(JRockit)、还有解释 + 编译两者混用的(HotSpot)。

知识点

面向对象


面向对象 vs 面向过程


当前主流的编程语言有 50 多种,主要分成两大阵营:面向对象编程和面向过程编程。


面向过程强调的是过程化的叙事思维,也就是让计算机有步骤地顺序地做一件事。


优点是流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析。


缺点是在大型项目开发过程中,代码重用性低,扩展能力差,后期维护难度比较大。


面向对象强调高内聚、低耦合,先抽象模型,定义共性行为,在解决实际问题。


优点:

  • 结构清晰,程序是模块化和结构化,更加符合人类的思维方式;

  • 易扩展,代码重用率高,可继承,可覆盖,可以设计出低耦合的系统;

  • 易维护,系统低耦合的特点有利于减少程序的后期维护工作量。


缺点:

  • 开销大,当要修改对象内部时,对象的属性不允许外部直接存取,所以要增加许多没有其他意义、只负责读或写的行为。这会为编程工作增加负担,增加运行开销,并且使程序显得臃肿。

  • 性能低,由于面向更高的逻辑抽象层,使得面向对象在实现的时候,不得不做出性能上面的牺牲,计算时间和空间存储大小都开销很大。


下面我们简单介绍一下面向对象。


首先什么是对象?


这里的「对象」与我们中文概念上的「对象」是有差异的,我们中文普遍意义上的对象是指「标的物」,翻译成英文是 Target


但 Java 语言里的对象不是这个意思,而是指「任何物体」的一种统称,更接近于中文「东西」这个词,所以在英文里它被翻译成 Object


Java中的对象是指任何物体的抽象,面向对象的设计过程即是事件的抽象过程。


面向对象的三大核心特性简介


1、封装

封装是指属性值的访问与修改需要使用相应的 getter/setter 方法,而不是直接对 public 的属性进行读取和修改。


封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。适当的封装可以让程式码更容易理解与维护,也加强了程式码的安全性。


2、继承
继承是面向对象编程的基石,通过创建具有逻辑等级的类体系,形成继承树,实现基础模块的复用。


继承是 is-a 关系,通过继承,使代码更有层次感,更有扩展性,并为多态打下语法基础。


3、多态
多态以前两个特性为基础,根据运行时的实际对象类型,同一个方法产生不同的运行结果,使同一个行为具有不同的表现形式。


我们先明确两个非常容易混淆的概念:override 和  overload

  • override:覆写,指子类实现接口或者继承父类时,保持方法签名完全相同,实现的方法体不同,是垂直方向上行为的不同实现;

  • overload:重载,是指在同一个类中,方法名相同,参数类型或者参数个数不同的,是水平方向上行为的不同实现。


多态在编译层面无法判断最终调用的方法体,是在运行时由 JVM 进行动态绑定,调用合适的覆写方法体来执行。


重载是编译器确定的方法调用,属于静态绑定,所以笔者认为多态专指覆写。


平台无关性

Java 通过字节码和 Java 虚拟机(JVM)这种跨平台的抽象,屏蔽了操作系统和硬件的细节,这也是实现「一次编写,到处运行」的基础。


在运行时,JVM 会通过类加载器(Class-Loader)加载字节码,解释或者编译执行。就像前面提到的,主流 Java 版本中,如 JDK 8 实际是解释和编译混合的一种模式,即所谓的混合模式(-Xmixed)。

我们开发的 Java 的源代码,首先通过 Javac 编译成为字节码(bytecode), 然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码。但是常见的 JVM,比如我们大多数情况使用的 Oracle JDK 提供的 Hotspot JVM, 都提供了 JIT(Just-In-Time)编译器,也就是通常所说的动态编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。


JVM


类加载机制

虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。


在 Java 语言里面,类的加载、连接和初始化过程都是在程序运行期间完成的。


Java 类的整个生命周期如下图:


面试官:谈谈你对 Java 平台的理解


3 个重要的类加载器:

  • Bootstrap ClassLoader:启动类加载器,由原生代码(如C语言)编写,不继承自java.lang.ClassLoader,java 程序无法直接操作这个类。它用来加载 Java 核心类库。

  • Extension ClassLoader:扩展类加载器,父类加载器为启动类加载器。负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。

  • Application ClassLoader:应用程序类加载器,父类加载器为启动类加载器。它负责加载环境变量 classpath 或者系统属性 java.class.path 指定路径下的类库。它是程序中默认的类加载器,一般情况下,我们 Java 程序中的类,都是由它加载完成的。


垃圾回收

我们知道,程序在运行的时候,为了提高性能,大部分数据都是会加载到内存中进行运算的,有些数据是需要常驻内存中的,但是有些数据,用过之后便不会再需要了,我们称这部分数据为垃圾数据。


为了防止内存被使用完,我们需要将这些垃圾数据进行回收,即需要将这部分内存空间进行释放。


不同于 C++ 需要自行释放内存的机制,Java 虚拟机(JVM)提供了一种自动回收内存的机制,也就是垃圾回收(Garbage Collection,GC)。


垃圾判断算法:

  • 引用计数法

  • 可达性分析法


垃圾回收算法:

  • 标记-清除算法(Tracing Collector)

  • 标记-整理算法(Compacting Collector)

  • 复制算法(Copying Collector)

  • 适应性算法(Adaptive Collector)

  • 分代收集算法(Generational Collector)


垃圾回收器:

  • Serial 回收器

  • CMS 回收器

  • G1


异常处理




Exception 和 Error 都是继承了 Throwable 类,Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。

  • Error 是一种非常特殊的异常类型,它的出现标识着系统出现了不可控的错误,例如 StackOverflowErrorOutOfMemoryError。针对此类错误,程序无法处理,只能人工介入。

  • Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。


Exception 又分为可检查(checked)异常和不检查(unchecked)异常:

  • 可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。比如:IOExceptionClassNotFoundException

  • 不检查异常就是所谓的运行时异常,类似 NullPointerException ArrayIndexOutOfBoundsException之类,它们都继承自 RuntimeException。通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。