![](https://img.php1.cn/3cd4a/1eebe/cd5/dc7ef30f57b727c7.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pzb25faXQ=,size_16,color_FFFFFF,t_70)
问题1:谈谈你对Java平台的理解,“Java是解释执行”这句话正确吗?
对于Java平台的理解,可以从很多方面谈一下。上图是一个相对宽泛的蓝图,可以作为回答这个问题的蓝图。
这个说法是不准确的。Java的源代码,首先经过Javac编译器编译成字节码文件,然后,在运行时,通过JVM内嵌的解释器将字节码转换为最终的机器码。但是,常见的JVM,比如我们大多数情况使用的Oracle JDK提供的Hotspot JVM都提供了JIT(just in time)编译器,也就是通常所说的动态编译器,JIT能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于编译执行,而不是解释执行。
继续说一下,解释执行和编译执行的问题。字节码 + JVM是Java实现跨平台的基础。JVM会通过类加载器(双亲委托机制)加载字节码,解释或者编译执行。主流的Java版本中,如JDK8实际是解释和编译混合的一种模式。通常,运行在server模式的JVM,会进行上万次调用以收集足够的信息进行高效的编译,client模式的这个上线是1500次。Hotspot JVM内置了两个JIT compiler,c1(client模式 - 适用于对启动速度敏感的应用,比如Java桌面应用) 和 c2(server模式 - 适用于长时间运行的服务器端应用设计)。默认采用所谓的分层编译。
其实还有一种新的编译方式,即AOT(Ahead of time),直接将字节码编译成机器码,这样就避免了JIT预热等各个方面的开销。Android 的ART使用的就是类似AOT的方式,就是比较占用空间。
另外,JVM作为一个强大的平台,不仅仅只有Java语言可以运行在JVM上,本质上合规的字节码都可以运行,Java语言自身也为此提供了便利。我们可以看到类似Groovy、JRuby、Jython、Scala、Clojure等大量JVM语言,活跃在不同的场景。
问题2:Exception和Error有什么区别,另外,运行时异常与一般异常有什么区别?
![](https://img.php1.cn/3cd4a/1eebe/cd5/ff61bfdd3c0af92e.webp?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pzb25faXQ=,size_16,color_FFFFFF,t_70)
1、NoClassDefFoundError和ClassNotFoundException有什么区别?
最明显的区别就是:一个是Exception,一个是Error。
JDK API里面的解释:
a)NoClassDefFoundError
当 Java 虚拟机或 ClassLoader 实例试图在类的定义中加载(作为通常方法调用的一部分或者作为使用 new 表达式创建的新实例的一部分),但无法找到该类的定义时,抛出此异常。
当前执行的类被编译时,所搜索的类定义存在,但运行时无法再找到该定义。
b)ClassNotFoundException
当应用程序试图使用以下方法通过字符串名加载类时,抛出该异常:
* Class 类中的 forName 方法。
* ClassLoader 类中的 findSystemClass 方法。
* ClassLoader 类中的 loadClass 方法。
2、随着Java语言得发展,引入了一些更加便利的特性,比如try-with-resources和multiple catch。示例如下:
try (BufferedReader br = new BufferedReader(…);BufferedWriter writer = new BufferedWriter(…)) {// Try-with-resources
// do something
catch ( IOException | XEception e) {// Multiple catch// Handle it
}
3、异常处理的基本原则
- 尽量不要捕获类似Exception这样的通用异常,而是应该捕获特定异常;进一步讲,除非深思熟虑了,否则不要捕获Throwable或者Error,这样很难保证我们能够正确处理OOM;
- 不要生吞异常;因为如果不把异常跑出来,或者也没有输出日志之类的,可能会导致非常难以诊断的诡异情况;
看下如下代码:
try {// 业务代码// …
} catch (IOException e) {e.printStackTrace();
}
记住:在产品代码中,通常是不允许这样处理的。因为在稍微复杂一点的生产系统中,标准出错不是合适的输出选项,因为你很难判断到底输出到哪里去了。尤其是分布式系统,如果发生异常,但是无法找到堆栈轨迹,这纯属为诊断设置障碍了。最好使用产品日志,详细地输出到日志系统里。
4、从性能的角度审视Java的异常处理机制
- try-catch代码段会产生额外的性能开销,或者换个角度说,往往会影响JVM对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的try包住整段的代码;同时,避免利用异常机制控制代码流程,这远比我们通常意义上的条件语句要低效。
- Java每实例化一个Exception,都会对当时的栈进行快照,这是一个相对比较重的操作。如果发生的非常频繁,开销就不能被忽略了。当我们的服务出现反应变慢、吞吐量下降的时候,检查发生最频发的Exception也是一种思路。
5、匿名内部类,访问局部变量时,局部变量为啥要用final来修饰吗?
Java inner class实际会copy一份,不是去直接使用局部变量,final可以防止出现数据一致性问题。
问题3:强引用、软引用、弱引用、虚引用的区别
虚引用仅仅是提供了一种确保对象被finalize以后(处于虚可达状态),做某些事情的机制,比如,通常用来做所谓的Post-Mortem清理机制。Java平台自身的Cleaner机制等。
判断对象可达性,是JVM垃圾收集器决定如何处理对象的一部分考虑。
所有引用类型,都是抽象类java.lang.ref.Reference的子类,提供了get()方法。除了虚引用(因为get永远返回null),如果对象还没有被销毁,都可以通过get方法获取原有对象。这意味着,利用软引用和弱引用,我们可以将访问到的对象,重新指向强引用,也就是人为的改变对象的可达性状态。所以,对于软引用、弱引用之类,垃圾回收器可能会存在二次确认的问题,以保证处于该状态的对象,没有改变为强引用。
谈到各种引用的编程,就必然要提到引用队列。我们会创建各种引用并关联到响应对象,可以选择是否需要关联引用队列,JVM会在特定时机将引用enqueue到队列里,我们可以从队列里获取引用,然后进行相关后续逻辑。尤其是虚引用,get方法只返回null,如果再不指定引用队列,基本就没有意义了。
Object counter = new Object();
ReferenceQueue refQueue &#61; new ReferenceQueue<>();
PhantomReference p &#61; new PhantomReference<>(counter, refQueue);
counter &#61; null;
System.gc();
try {// Remove 是一个阻塞方法&#xff0c;可以指定 timeout&#xff0c;或者选择一直阻塞Reference ref &#61; refQueue.remove(1000L);if (ref !&#61; null) {// do something}
} catch (InterruptedException e) {// Handle it
}
Java软引用什么时候被回收
问题4&#xff1a;String、StringBuilder、StringBuffer
1、StringBuilder与StringBuffer&#xff08;线程安全&#xff09;
StringBuilder与StringBuffer底层都是利用可修改的(char&#xff0c;JDK9以后是byte)数组&#xff0c;二者都继承自AbstractStringBuilder&#xff0c;里面包含了基本的操作&#xff0c;区别在于最终的方法是否加了Synchronized。注意&#xff1a;数组的大小默认是16&#xff0c;超过则进行扩容。
2、字符串拼接
String strByBuilder &#61; new
StringBuilder().append("aa").append("bb").append("cc").append("dd").toString();String strByConcat &#61; "aa" &#43; "bb" &#43; "cc" &#43; "dd";
非静态的拼接逻辑在JDK8中会自动被javac转换为StringBuilder操作&#xff1b;而在JDK9里面&#xff0c;则体现了思路的变化。Java9利用InvokeDynamic&#xff0c;将字符串拼接的优化与javac生成的字节码解耦&#xff0c;假设未来JVM增强相关运行时实现&#xff0c;将不再需要依赖javac的任何修改。
3、字符串缓存String.intern()
String在Java6以后提供了intern()方法&#xff0c;目的是提示JVM把相应字符串缓存起来&#xff0c;以备重复使用。在我们创建字符串对象并调用intern()方法的时候&#xff0c;如果已经有缓存的字符串&#xff0c;就会返回缓存里的实例&#xff0c;否则将其缓存起来。
上述看起来不错&#xff0c;实际却是大跌眼镜。一般使用Java6这种历史版本并不推荐大量使用intern()&#xff0c;为什么&#xff1f;魔鬼存在于细节之中&#xff0c;被缓存的字符串是保存在所谓PermGen里的&#xff0c;也就是臭名昭著的“永久代”&#xff0c;这个空间是很有限的&#xff0c;也基本不会被FullGC之外的垃圾收集照顾。所以&#xff0c;如果使用不当&#xff0c;OOM就会光顾。
在后续的版本中&#xff0c;这个缓存被放置在了堆中&#xff0c;这样就极大的避免了永久代占满的问题&#xff0c;甚至永久代在JDK8中被MetaSpace&#xff08;元数据区&#xff09;替代了。而且&#xff0c;默认缓存大小也在不断地扩大中&#xff0c;从最初的1009&#xff0c;到7u40以后被修改为60013。
Intern是一种显式地重拍机制&#xff0c;但是有一定的副作用&#xff0c;因为需要开发者写代码时明确调用&#xff0c;一是不方便&#xff0c;每一个都显式调用非常麻烦&#xff1b;另外就是我们很难保证效率&#xff0c;应用开发阶段很难清楚地预计字符串的重复情况&#xff0c;有人认为这是一种污染代码的实践。
幸好在Oracle JDK 8u20之后&#xff0c;推出了一个新的特性&#xff0c;也就是G1 GC下的字符串排重。它是通过将相同数据的字符串指向同一份数据做出来的&#xff0c;是JVM的底层的改变&#xff0c;并不需要Java类库做什么修改。注意&#xff1a;这个功能默认是关闭的&#xff0c;需开启。
4、字符串创建的机理
由于String在Java世界中使用过于频繁&#xff0c;Java为了避免在一个系统中产生大量的String对象&#xff0c;引入了字符串常量池。其运行机制是&#xff1a;创建一个字符串时&#xff0c;首先检查池中是否有值相同的字符串对象&#xff0c;如果有则不需要创建直接从池中刚查找到的对象引用&#xff1b;如果没有则新建字符串对象&#xff0c;返回对象引用&#xff0c;并且将新创建的对象放入池中。但是&#xff0c;通过new方法创建的String对象是不检查字符串池的&#xff0c;而是直接在堆区或栈区创建一个新的对象&#xff0c;也不会把对象放入池中。上述原则只适用于通过直接量给String对象引用赋值的情况。
举例&#xff1a;String str1 &#61; "123"; //通过直接量赋值方式&#xff0c;放入字符串常量池
String str2 &#61; new String(“123”);//通过new方式赋值方式&#xff0c;不放入字符串常量池
注意&#xff1a;String提供了inter()方法。调用该方法时&#xff0c;如果常量池中包括了一个等于此String对象的字符串&#xff08;由equals方法确定&#xff09;&#xff0c;则返回池中的字符串。否则&#xff0c;将此String对象添加到池中&#xff0c;并且返回此池中对象的引用。