[核心技术36问]Part1

1.谈谈你对Java平台的理解?“Java是解释执行”,这句话正确吗?
典型回答
Java本身是一种面向对象的语言,最显著的特性有两个方面,一是所谓的“书写一次,到处运行”(write once,run anywhere),能够非常容易地获得跨平台能力;另外就是垃圾收集(GC,garbage collection),Java通过垃圾收集器(garbage collector)回收分配内存,大部分情况下,程序员不需要自己操心内存地分配和回收。
我们日常会接触到JRE( Java runtime environment)或者JDK(Java development kit)。JRE,也就是Java运行环境,包含了JVM和Java类库,以及一些模块等。而JDK可以看作是JRE的一个超集,提供了更多工具,比如编译器、各种诊断工具等。
对于“Java是解释执行”这句话,这个说法不太准确。我们开发的Java的源代码,首先通过javac编译成为字节码,然后在运行时通过Java虚拟机内嵌的解释器将字节码转换成最终的机器码。但是常见的JVM,比如我们大多数情况使用的Oracle JDK提供的hotspot JVM,都提供了JIT(just-in-time)编译器,也就是通常所说的动态编译器,JIT能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行了。
知识扩展:
回归正题,对于Java平台的理解,可以从很多方面简明扼要地谈一下,例如:Java语言特性,包括泛型、lambda等语言特性;基础类库,包括集合、IO/NIO、网络、并发、安全等基础类库。对于我们日常工作应用较多地类库,面试前系统化总结一下。
或者谈谈JVM地一些基础概念和机制,比如Java的类加载机制,常用版本JDK(JDK8)内嵌的classloader,例如boostrap、application和extension classloader;类加载大致过程:加载、验证、链接、初始化;自定义classloader等。还有垃圾收集的基本原理,最常见的垃圾收集器,如SerialGC、Parallel GC、CMS、G1等,对于适用于什么样的工作负载最好也心里有数。
还有JDK包含哪些工具或者Java领域内其他工具等,如编译器、运行时环境、安全工具、诊断和监控工具等。这些基本工具是日常工作效率的保证。

解释执行和编译执行的问题:众所周知,我们通常把Java分为编译期和运行时。这里说的Java的编译和C/C++是有着不同的意义的,javac的编译,编译Java源码生成“.class”文件里面其实是字节码,而不是可以直接执行的机器码。Java通过字节码和Java虚拟机这种跨平台的抽象,屏蔽了操作系统和硬件的细节,这也是实现“一次编译、到处运行”的基础。
在运行时,JVM会通过类加载器加载字节码,解释或者编译执行。就像我前面提到的,主流Java版本中,如JDK8其实是解释和编译混合的一种模式,即所谓的混合模式(-Xmixed)。通常运行在server模式的JVM,会进行上万次调用以收集足够的信息进行高效的编译,clent模式这个门限是1500次。Oracle hotspots JVM内置了两个不同的JIT compiler,C1对应前面说的client模式,适用于对于启动速度敏感的应用,比如普通Java桌面应用;C2对应server模式,它的优化是为长时间运行的服务器端应用设计的。默认是采用所谓的分层编译(tieredcompilation)。
Java虚拟机启动时,可以指定不同的参数对运行模式进行选择。比如,指定“-Xint”,就是告诉JVM只进行解释执行,不对代码进行编译,这种模式抛弃了JIT可能带来的性能优势。毕竟解释器是逐条读入,逐条解释运行的。与其相对应的,还有一个“-Xcomp”参数,这是告诉JVM关闭解释器,不要进行解释执行,或者叫做最大优化级别。但是这种模式也未必是最高效的。“-Xcomp”会导致JVM启动变慢非常多,同时有些JIT编译器优化方式,比如分支预测,如果不进行profiling,往往并不能进行有效优化。
除了我们日常最常见的Java使用模式,其实还有一种新的编译方式,就是所谓的AOT(ahead-of-time compilation),直接将字节码编译成机器代码,这样就避免了JIT预热等各方面的开销,比如Oracle JDK9就引入了实验性的AOT特性,并且增加了新的jaotc工具。把某个类或者某个模块编译成为AOT库。而且,Oracle JDK支持分层编译和AOT协作使用,两者并不是二选一的关系。

2.请对比Exception和Error,另外,运行时异常与一般异常有什么区别?
典型回答:
Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception和Error体现了Java平台设计者对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error是指在正常情况下,不大可能出现的情况,绝大部分的Error都会导致程序(比如JVM自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如OutOfMemoryError之类,都是Error的子类。
Exception又分为可检查(checked)异常和不检查(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似NullPointerException、ArrayIndexOutOfBoundsException之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
考点分析:
NoClassDefFoundError和ClassNotFoundException有什么区别?
知识扩展:

      如下代码:

    try{
	Thread.sleep(10001);
    }catch(Exception e){
	//ignore it
    }
这段代码违反了异常处理的两个基本原则,第一,尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常,在这里是Thread.sleep()抛出的InterruptedException。这是因为在日常的开发和合作中,我们读代码的机会往往超过写代码,软件工程是门协作的艺术,所以我们有义务让自己的代码能够直观地体现出尽量多的信息,而泛泛的Exception之类,恰恰隐藏了我们的目的。另外,我们也要保证程序不会捕获到我们不希望捕获的异常。比如,你可能更希望RuntimeException被扩散出来,而不是被捕获。第二,不要生吞异常。这是异常处理中要特别注意的事情,因为很可能会导致非常难以诊断的诡异情况。生吞异常,往往是基于假设这段代码可能不会发生,或者感觉忽略异常是无所谓的,但是千万不要在产品代码做这种假设!
如果我们不把异常抛出来,或者也没有输出到日志之类,程序可能在后续代码以不可控的方式结束。没人能够轻易判断究竟是哪里抛出了异常,以及是什么原因产生了异常。

    请看如下代码:

try{
	//业务代码
}catch(Exception e){
	e.printStackTrace();
}
这段代码作为一段实验代码,它是没有任何问题的,但是在产品代码中,通常都不允许这样处理。
printStackTrace()的文档,开头就是“Prints this throwable and its backtrace to the standard error stream”.问题就在这里,在稍微复杂一点的生产系统中,标准出错(STERR)不是个合适的输出选项,因为你很难判断出到底输出到哪里去了。
尤其是对于分布式系统,如果发生异常,但是无法找到堆栈轨迹,这纯属是为诊断设置障碍。所以,最好使用产品日志,详细地输出到日志系统里。
我们接下来看下面的代码段,体会一下throw early,catch late原则。

代码如下:

public void readPreference(String fileName){
    //perform operations
    InputStream in=new InputStream(fileName);
    //read the preferences file
}
如果filename是null,程序就会抛出NullPointerException,但是由于没有第一时间暴露出问题,堆栈信息可能非常令人费劲,往往需要相对复杂的定位。这个NPE只是作为例子,在实际产品代码中,可能是各种情况,比如获取配置失败之类的。在发现问题的时候,第一时间抛出,能够更加清晰地反映问题。
我们可以修改一下,让问题“throw early”,对应的异常信息就非常直观了。

代码如下:

public void readPreference(String fileName){
    Object.requireNonNull(fileName);
    //perform operations
    InputStream in=new InputStream(fileName);
    //read the preferences file
}
至于“catch late”,其实是我们经常苦恼的问题,捕获异常后,需要怎么处理呢?最差的处理方式,就是生吞异常,本质上其实是掩盖问题。如果实在不知道如何处理,可以选择保留原有异常的cause信息,直接再抛出或者构建新的异常抛出去。在更高层面,因为有了清晰的(业务)逻辑,往往会更清楚合适的处理方式是什么。

从性能角度来审视一下java的异常处理机制,这里有两个可能会相对昂贵的地方:
(1)try-catch代码段会产生额外的性能开销,或者换个角度说,它往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码;与此同时,利用异常控制代码流程,也不是一个好主意,远比我们通常意义上的条件语句要低效。
(2)java会实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,这个开销可就不能被忽略了。
所以,对于部分追求极致性能的底层类库,有种方式是尝试创建不进行栈快照的Exception。这本身也存在争议,因为这样做的假设在于,我创建异常时知道未来是否需要堆栈。问题是,实际上可能吗?小范围或许可能,但是在大规模项目中,这么做可能不是个理智的选择。如果需要堆栈,但又没有收集这些信息,在复杂情况下,尤其是类似微服务这种分布式系统,这会大大增加诊断的难度。
当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频繁的Exception也是一种思路。


3.谈谈final、finally、finalize有什么不同?

4.强引用、软引用、弱引用、幻象引用有什么区别?

    不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

    所谓强引用,就是我们常见的普通的对象引用,只要还有强引用指向一个对象,就能表明该对象还活着,垃圾收集器就不会碰这种对象。对于一个普通对象,如果没有其他的引用关系,只要超出了引用的作用域或者显式地将引用值赋为null,那么垃圾收集器就可以收集这种引用了。当然,在收集的时候还是要具体根据垃圾收集策略来收集。

    软引用,是一种相对于强引用弱化一些的引用,它可以使对象豁免一些内存收集。只有当内存不足时,JVM才会试图去回收软引用指向的对象,JVM会在抛出OutOfMemoryError之前回收软引用指向的对象。软引用通常用来实现内存敏感的缓存,在还有空闲内存时,就会暂时保留缓存,在内存不够时清理掉,这样就确保了使用缓存的同时,不会耗尽内存

    弱引用,不会使对象豁免垃圾收集,它仅仅是提供一种对弱引用状态下对象的访问途径可以用来构建一种没有特定约束的关系,比如维护一种非强制性映射的关系,如果要使用对象的时候,对象还存在,那么就使用它,如果不存在那么创建新的实例。弱引用也经常用来实现缓存。

    对于幻象引用,也叫做虚引用,并不能通过它来访问对象,它仅仅是提供了一种确保对象在被finalize()以后,做某些事情的机制。比如,通常用来做所谓的Post-Mortem清理机制,也有人利用幻象引用监控对象的创建和销毁。


5.String、StringBuffer、StringBuilder有什么区别?

    


猜你喜欢

转载自blog.csdn.net/hellodake/article/details/80921963
今日推荐