第1讲 | 谈对Java平台谈你的理解?
对于Java平台的理解,可以从很多方面简明扼要地谈一下,例如:Java语言特性,包括泛型、Lambda等语言特性;基础类库,包括集合、IO/NIO、网络、并发、安全等基础类库。
或者谈谈JVM的一些基础概念和机制,比如Java的类加载机制,常用JDK(如JDK 8)内嵌的Class-Loader,例如Bootstrap、Application、和Extension Class-Loader;类加载大致过程:加载、验证、链接、初始化;自定义Class-Loader等。还有垃圾收集的基本原理,最常见的垃圾收集器,如 Serial GC、Parallel GC、CMS、G1等。

解释执行和编译执行的问题
通常把Java分为编译期和运行时。Javac编译Java源码生成字节码文件“.class”,而不是可以直接执行的机器码。Java通过字节码和Java虚拟机(JVM)这种跨平台的抽象,屏蔽了操作系统和硬件的细节。
在运行时,JVM会通过类加载器(Class-Loader)加载字节码,解释或者编译执行。在主流Java版本中,如JDK 8 实际是解释和编译混合的一种模式,即所谓的混合模式(-Xmixed)。通常运行在server模式的JVM,会进行上万次调用以收集足够的信息进行高效的编译,client模式这个门限是1500次。Oracle Hotspot JVM 内置了两个不同的JIT compiler,C1 对应前面说的 client 模式,适用于对于启动速度敏感的应用,比如普通 Java 桌面应用;C2 对应 server 模式,它的优化是为长时间运行的服务器端应用设计的。默认是采用所谓的分层编译(TieredCompilation)。
Java虚拟机启动时,可以指定不同的参数对运行模式进行选择。比如,指定“-Xint”,就是告诉JVM只进行解释执行,不对代码进行编译,这种模式抛弃了JIT可能带来的性能优势。毕竟解释器(interpreter)是逐条读入,逐条解释运行的。与其相对应的,还有一个“-Xcomp”参数,这是告诉JVM关闭解释器,不要进行解释执行,或者叫做最大优化级别。这种模式未必高效,“-Xcomp”会导致JVM启动变慢非常多,同时有些JIT编译器优化方式,比如分支预测,如果不进行profiling,往往并不能进行有效优化。
除了常见的Java使用模式,还有一种新的编译方式AOT(Ahead-of-Time Compilation),直接将字节码编译成机器代码,这样就避免了JIT预热等各方面的开销,比如Oracle JDK 9 就引入了实验性的AOT特性,并且增加了新的jaotc工具。利用下面的命令把某个类或者某个模块编译成为AOT库。
jaotc --output libHelloWorld.so HelloWorld.class
jaotc --output libjava.base.so --module java.base
然后,在启动时直接指定就可以了。
java -XX:AOTLibrary=./libHelloWorld.so,./libjava.base.so HelloWorld
而且,Oracle JDK 支持分层编译和 AOT 协作使用,这两者并不是二选一的关系。
第2讲 Exception和Error有什么区别?
Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,比如OutOfMemoryError
。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似NullPointerException
、ArrayIndexOutOfBoundsException
之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

异常处理
try-with-resources 和 multiple catch,在编译期,会自动生成相应的处理逻辑,比如,自动按照约定俗成close那些扩展了AutoCloseable
或者 Closeable
的对象。
try (BufferedReader br = new BufferedReader(...);
BufferedWriter writer = new BufferedWriter(...)){// Try-with-resources
// do something
catch ( IOException | XEception e) {// Multiple catch
// handle it
}
异常处理的两个基本原则
- 尽量不要捕获类似Exception这样的通用异常,而应该捕获特定异常。一是能够直观地体现出尽量多的信息,二是保证程序不会捕获到我们不希望捕获的异常。
- 不要生吞(swallow)异常。如果我们不把异常抛出来,或者也没有输出到日志之类,程序可能在后续代码以不可控的方式结束。如e.printStackTrace(),输出到standard error stream,但在生产系统中,标准出错(STERR)不是个合适的输出选项。
Throw early,catch late 原则
throw early:在发现问题的时候,第一时间抛出,能够更加清晰地反应问题。
public void readPreferences(String filename){
Objects.requireNonNull(filename); // throw early NullPointerException
// ...
InputStream in = new FileInputStream(filename);
}
catch late:如果实在不知道如何处理,可以选择保留原有异常的 cause 信息,直接再抛出或者构建新的异常抛出去。
自定义异常,有两点需要考虑:
- 是否需要定义成Checked Exception,因为这种类型设计的初衷是为了从异常情况恢复。
- 避免包含敏感信息。
Java异常处理机制的性能问题
- try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句(if/else、switch)要低效。
- Java 每实例化一个 Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
NoClassDefFoundErrory与ClassNotFoundException
NoClassDefFoundError是一个错误(Error),而ClassNotFoundException是一个异常,在Java中对于错误和异常的处理是不同的,我们可以从异常中恢复程序但却不应该尝试从错误中恢复程序。
ClassNotFoundException的产生原因主要是:
Java支持使用反射方式在运行时动态加载类,例如使用Class.forName方法来动态地加载类时,可以将类名作为参数传递给上述方法从而将指定类加载到JVM内存中,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。解决该问题需要确保所需的类连同它依赖的包存在于类路径中,常见问题在于类名书写错误。
另外还有一个导致ClassNotFoundException的原因就是:当一个类已经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。通过控制动态类加载过程,可以避免上述情况发生。
NoClassDefFoundError产生的原因在于:
如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个时候就会导致NoClassDefFoundError。
造成该问题的原因可能是打包过程漏掉了部分类,或者jar包出现损坏或者篡改。解决这个问题的办法是查找那些在开发期间存在于类路径下但在运行期间却不在类路径下的类。
第3讲 谈谈 final、finally、 finalize 有什么不同?
final 可以用来修饰类、方法、变量,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
finally 则是Java保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC连接、保证 unlock 锁等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为 deprecated。
特例:下面finally中的代码不会被执行。
try{
System.exit(1);
} finally {
System.out.println("Print from finally");
}
final 不是 immutable,如果要实现 immutable 类,需要做到:
- 将 class 声明为 final
- 将所有成员变量定义为 private 和 final,并且不要实现 setter 方法。
- 构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值。
- 如果确实需要实现 getter 方法,或者其他可能会返回内部状态的方法,使用 copy-on-write 原则,创建私有的 copy。
finalize 不推荐使用,因为无法保证 finalize 什么时候执行,执行的是否符合预期。使用不当会影响性能,导致程序死锁、挂起等。
finalize 的执行是和垃圾收集关联在一起的,一旦实现了非空的 finalize 方法,就会导致相应对象回收呈现数量级上的变慢。因为,finalize 被设计成在对象被垃圾收集前调用,这就意味着实现了 finalize 方法的对象是个“特殊公民”,JVM要对它进行额外处理。finalize 本质上成为了快速回收的阻碍者,可能导致对象经过多个垃圾收集周期才能被回收。实践中,因为 finalize 拖慢垃圾收集,导致大量对象堆积,也是一种典型的导致OOM的原因。
第4讲 强引用、软引用、弱引用、幻象引用有什么区别?
不同的引用类型,主要体现的是对象不同的可达性(reachable)状态和对垃圾收集的影响。
强引用(StrongReference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显示地将相应(强)引用赋值为null, 就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
软引用(SoftReference),是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用(WeakReference)并不能使对象豁免垃圾收集,仅仅是提供一种访问在弱引用状态下对象的途径。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重现实例化。它同样是很多缓存实现的选择。
对于幻象引用,有时候也翻译成虚引用,你不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,也有人利用幻象引用监控对象的创建和销毁。
对象可达性状态流转分析

上图是是 Java 定义的不同可达性级别(reachability level),具体如下:
- 强可达(Strongly Reachable),就是当一个对象可以有一个或多个线程可以不通过各种引用访问到的情况。比如,我们新创建一个对象,那么创建它的线程对它就是强可达。
- 软可达(Softly Reachable),就是当我们只能通过软引用才能访问到对象的状态。
- 弱可达(Weakly Reachable),类似前面提到的,就是无法通过强引用或者软引用访问,只能通过弱引用访问时的状态。这是十分临近 finalize 状态的时机,当弱引用被清除的时候,就符合 finalize 的条件了。
- 幻象可达(Phantom Reachable),就是没有强、软、弱引用关联,并且 finalize 过了,只有幻象引用指向这个对象的时候。
- 还有一个最后的状态,就是不可达(unreachable),意味着对象可以被清除了。
判断对象可达性,是 JVM 垃圾收集器决定如何处理对象的一部考虑。
所有引用类型,都是抽象类 java.lang.ref.Reference 的子类,其get()方法:
T get(); //Returns this reference object's referent.
除了幻象引用(因为 get 永远返回 null),如果对象还没有被销毁,都可以通过 get 方法获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变了对象的可达性状态!(如图中的双向箭头)
所以,对于软引用、弱引用之类,垃圾收集器可能会存在二次确认的问题,以保证处于弱引用状态的对象,没有改变为强引用。
如果我们错误的保持了强引用(比如,赋值给了 static 变量),那么对象可能就没有机会变回类似弱引用的可达性状态了,就会产生内存泄漏。所以,检查弱引用指向对象是否被垃圾收集,也是诊断是否有特定内存泄漏的一个思路。如果我们的框架使用到弱引用又怀疑有内存泄漏,就可以从这个角度检查。
引用队列(ReferenceQueue)
在创建各种引用并关联到响应对象时,可以选择是否需要关联引用队列,JVM 会在特定时机将引用 enqueue 到队列里,我们可以从队列里获取引用(remove 方法在这里实际是有获取的意思)进行相关后续逻辑。尤其是幻象引用,get 方法只返回 null,如果再不指定引用队列,基本就没有意义了。下面的示例代码,利用引用队列,我们可以在对象处于相应状态时(对于幻象引用,就是前面说的被 finalize 了,处于幻象可达状态),执行后期处理逻辑。
Object counter = new Object();
ReferenceQueue refQueue = new ReferenceQueue<>();
PhantomReference<Object> p = new PhantomReference<>(counter, refQueue);
counter = null;
System.gc();
try{
// Remove 是一个阻塞方法,可以指定 timeout,或者选择一直阻塞
Reference<Object> ref = refQueue.remove(1000L);
if (ref!=null){
// do something
}
} catch (InterruptedException e) {
// Handle it
}
显式地影响软引用垃圾收集
软引用通常会在最后一次引用后,还能保持一段时间,默认值是根据堆剩余空间计算的(以 M bytes 为单位)。从 Java 1.3.1 开始,提供了 -XX:SoftRefLRUPolicyMSPerMB 参数,我们可以以毫秒(milliseconds)为单位设置。
-XX:SoftRefLRUPolicyMSPerMB=3000 // 3000 毫秒
这个剩余空间,其实会受不同 JVM 模式影响,对于 Client 模式,比如通常的 Windows 32 bit JDK,剩余空间是计算当前堆里空闲的大小,所以更加倾向于回收;而对于 server 模式 JVM,则是根据 -Xmx 指定的最大值来计算。
Reachability Fence
除了前面的几种基本引用类型,我们也可以通过底层 API 来达到强引用的效果,这就是所谓的设置reachability fence。
考虑一下这样的场景,按照 Java 语言规范,如果一个对象没有指向强引用,就符合垃圾收集的标准,有些时候,对象本身并没有强引用,但是也许它的部分属性还在被使用,这样就导致诡异的问题,所以我们需要一个方法,在没有强引用情况下,通知 JVM 对象是在被使用的。来看看 Java 9 中提供的案例。
class Resource {
private static ExternalResource[] externalResourceArray = ...
int myIndex;
Resource(...) {
myIndex = ...
externalResourceArray[myIndex] = ...;
}
protected void finalize() {
externalResourceArray[myIndex] = null;
}
public void action() {
try {
// 需要被保护的代码
int i = myIndex;
Resource.update(externalResourceArray[i]);
} finally {
// 调用 reachbilityFence,明确保障对象 strongly reachable
Reference.reachabilityFence(this);
}
}
private static void update(ExternalResource ext) {
ext.status = ...;
}
}
方法 action 的执行,依赖于对象的部分属性,所以被特定保护了起来。否则,如果我们在代码中像下面这样调用,那么就可能会出现困扰,因为没有强引用指向我们创建出来的 Resource 对象,JVM 对它进行 finalize 操作是完全合法的。
new Resource().action()
类似的书写结构,在异步编程中似乎是很普遍的,因为异步编程中往往不会用传统的“执行 -> 返回 -> 使用”的结构。
在 Java 9 之前,实现类似类似功能相对比较繁琐,有的时候需要采取一些比较隐晦的小技巧。幸好,java.lang.ref.Reference 给我们提供了新方法,它是 JEP 193: Variable Handles 的一部分,将 Java 平台底层的一些能力暴露出来:
static void reachabilityFence(Object ref)
在 JDK 源码中,reachabilityFence 大多使用在 Executors 或者类似新的 HTTP/2 客户端代码中,大部分都是异步调用的情况。编程中,可以按照上面这个例子,将需要 reachability 保障的代码段利用 try-finally 包围起来,在 finally 里明确声明对象强可达。
网友评论