Java平台理解及Java是解释执行吗?
首先Java是一种面向对象的语言,本身具有两个特性:一个是跨平台能力(一次编写,到处运行),屏蔽了操作系统和硬件的细节; 第二个就是垃圾自动回收机制(GC)。
我们日常接触到的jre(Java runtime environment)和jdk(Java development kit), jre是Java运行时环境,包含了JVM和java类库; jdk则是Java开发工具,里面提供了许多开发工具如;编译器(javac)、诊断和监控(jconsole)工具等。
java生态:spring,spark,elasticsearch,maven
不完全正确,Java是通过对源文件进行编译成字节码文件(.class),然后jvm(Java虚拟机)对字节码文件逐条进行解释运行。 但是往往有一部分热点代码(hot spot)会占有大部分时间解释,所以Java中会有jit(Just-In-Time)。即时编译器将该部分直接编译成机器代码直接运行,大大提高效率。 这部分热点代码就是编译执行了而不是解释执行。
在运行时,JVM 会通过类加载器(Class-Loader)加载字节码,解释或者编译执行。 主流 Java 版本中,如 JDK 8 实际是解释和编译混合的一种模式,即所谓的混合模式(-Xmixed)。 通常运行在 server 模式的 JVM,会进行上万次调用以收集足够的信息进行高效的编译,client 模式这个门限是 1500 次。 Oracle Hotspot JVM 内置了两个不同的 JIT compiler, C1 对应前面说的 client 模式,适用于对于启动速度敏感的应用,比如普通 Java 桌面应用; C2 对应 server 模式,它的优化是为长时间运行的服务器端应用设计的,默认是采用所谓的分层编译(TieredCompilation)。
除了我们日常最常见的 Java 使用模式,其实还有一种新的编译方式, 即所谓的 AOT(Aheadof-Time Compilation),直接将字节码编译成机器代码, 这样就避免了 JIT 预热等各方面的开销, 比如 Oracle JDK 9 就引入了实验性的 AOT 特性,并且增加了新的 jaotc 工具。
java创建对象的方式
- 使用new关键字创建对象
- 使用Class类的newInstance方法(反射机制)
- 使用Constructor类的newInstance方法(反射机制)
- 使用Clone方法创建对象
- 使用(反)序列化机制创建对象
Exception和Error有什么区别
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM自身)处于非正常的、不可恢复状态。 既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
Exception 又分为可检查(checked)异常和不检查(unchecked)异常。 可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。 不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException 之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。
扩展
- NoClassDefFoundError 和 ClassNotFoundException 有什么区别,这也是个经典的入门题目。
- 理解 Java 语言中操作 Throwable 的元素和实践。掌握最基本的语法是必须的,如 trycatch-finally 块,throw、throws 关键字等。
- 与此同时,也要懂得如何处理典型场景。
异常处理原则
- 尽量不要捕获类似 Exception 这样的通用异常,而是应该捕获特定异常。进一步讲,除非深思熟虑了,否则不要捕获 Throwable 或者 Error,这样很难保证我们能够正确程序处理 OutOfMemoryError。
- 不要生吞(swallow)异常
- try-catch 代码段会产生额外的性能开销,或者换个角度说,它往往会影响 JVM 对代码进行优化,所以建议仅捕获有必要的代码段,尽量不要一个大的 try 包住整段的代码。
- 请勿在try代码块中调用return、break或continue语句。万一无法避免,一定要确保finally的存在不会改变函数的返回值(不要在finally代码块中处理返回值)。
NoClassDefFoundError 和 ClassNotFoundException 有什么区别
ClassNotFoundException | NoClassDefFoundError |
---|---|
从java.lang.Exception继承,是一个Exception类型 | 从java.lang.Error继承,是一个Error类型 |
当动态加载Class的时候找不到类会抛出该异常 | 当编译成功以后执行过程中Class找不到导致抛出该错误 |
一般在执行Class.forName()、ClassLoader.loadClass()或ClassLoader.findSystemClass()的时候抛出 | 由JVM的运行时系统抛出 |
final、finally、 finalize有什么不同
final 可以用来修饰类(不可继承扩展)、方法(不能重写)、变量(字段值不能修改。
finally 异常处理机制的关键字,表示最后执⾏行。来进行类似关闭 JDBC 连接、保证 unlock 锁、释放资源等动作。
finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。 finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为deprecated。 Java 平台目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。
不可变 Immutable
- 将 class 自身声明为 final,这样别人就不能扩展来绕过限制了。
- 将所有成员变量定义为 private 和 final,并且不要实现 setter 方法。
- 通常构造对象时,成员变量使用深度拷贝来初始化,而不是直接赋值,这是一种防御措施,因为你无法确定输入对象不被其他人修改。
- 如果确实需要实现 getter 方法,或者其他可能会返回内部状态的方法,使用 copy-on-write原则,创建私有的 copy。
String、StringBuffer、StringBuilder 有什么区别?
String是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性类似拼接、裁剪字符串等动作,都会产生新的 String 对象。
原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。
StringBuffer 本质是一个线程安全的可修改字符序列,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是StringBuilder。
为了实现修改字符序列的目的,StringBuffer 和 StringBuilder 底层都是利用可修改的(char,JDK 9 以后是 byte)数组,二者都继承了 AbstractStringBuilder,里面包含了基本操作,区别仅在于最终的方法是否加了 synchronized。
另外,这个内部数组应该创建成多大的呢?如果太小,拼接的时候可能要重新创建足够大的数组;如果太大,又会浪费空间。
目前的实现是,构建时初始字符串长度加 16(这意味着,如果没有构建对象时输入最初的字符串,那么初始值就是 16)。
我们如果确定拼接会发生非常多次,而且大概是可预计的,那么就可以指定合适的大小,避免很多次扩容的开销。
扩容会产生多重开销,因为要抛弃原有数组,创建新的(可以简单认为是倍数)数组,还要进行 arraycopy
字符串缓存
String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。 在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。 一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。
看起来很不错是吧?但实际情况估计会让你大跌眼镜。一般使用 Java 6 这种历史版本,并不推荐大量使用 intern,为什么呢? 魔鬼存在于细节中,被缓存的字符串是存在所谓 PermGen 里的,也就是臭名昭著的“永久代”,这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。 所以,如果使用不当,OOM 就会光顾。
在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在JDK 8 中被 MetaSpace(元数据区)替代了。 而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。
- 使用参数直接打印具体数字
-XX:+PrintStringTableStatistics
- 手动调整大小
-XX:StringTableSize=N
Intern 是一种显式地排重机制,但是它也有一定的副作用,因为需要开发者写代码时明确调用,一是不方便,每一个都显式调用是非常麻烦的;
另外就是我们很难保证效率,应用开发阶段很难清楚地预计字符串的重复情况,有人认为这是一种污染代码的实践。
幸好在 Oracle JDK 8u20 之后,推出了一个新的特性,也就是 G1 GC 下的字符串排重。
它是通过将相同数据的字符串指向同一份数据来做到的,是 JVM 底层的改变,并不需要 Java 类库做什么修改。
这个功能目前是默认关闭的,你需要使用下面参数开启,并且记得指定使用 G1 GC:
-XX:+UseStringDeduplication
动态代理是基于什么原理?
通过反射我们可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。
Java 发射机制的常见应用:动态代理(AOP、RPC)、提供第三方开发者扩展能力(Servlet容器,JDBC连接)、第三方组件创建对象(DI)……
动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的, 比如用来包装 RPC 调用、面向切面的编程(AOP)。
实现动态代理的方式很多, 比如 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。 还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)、Javassist 等。
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。新版本也开始结合ASM机制。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy 的优势
- 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
- 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
- 代码实现简单。
基于类似 cglib 框架的优势
- 有的时候调用目标可能不便实现额外接口,从某种角度看,限定调用者实现接口是有些侵入性的实践,类似 cglib 动态代理就没有这种限制。
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能。
有哪些方法可以在运行时动态生成一个 Java 类
直接用 ProcessBuilder 之类启动 javac 进程,并指定上面生成的文件作为输入,进行编译。最后,再利用类加载器,在运行时加载即可。本质上还是在当前程序进程之外编译的。
使用 Java Compiler API,这是 JDK 提供的标准 API,里面提供了与 javac 对等的编译器功能。
直接生成相应的字节码,然后交给类加载器去加载。通常我们可以利用 Java 字节码操纵工具和类库 ASM、Javassist、cglib 等 来实现。
字节码和类加载到底是怎么无缝进行转换的?发生在整个类加载过程的哪一步?
类从字节码到 Class 对象的转换,在类加载过程中,这一步是通过下面的方法提供的功能,或者 defineClass 的其他本地对等实现。
//只选取了最基础的两个典型的 defineClass 实现
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
只要能够生成出规范的字节码,不管是作为 byte 数组的形式,还是放到 ByteBuffer里,都可以平滑地完成字节码到 Java 对象的转换过程。
JDK 提供的 defineClass 方法,最终都是本地代码实现的。
private native Class<?> defineClass0(String name, byte[] b, int off, int len,
ProtectionDomain pd);
private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);
相关的字节码操纵逻辑
JDK dynamic proxy (1.8) 的实现代码,对应逻辑是实现在 Proxy类中的 ProxyClassFactory 这个静态内部类, 通过 ProxyGenerator 生成字节码,并以 byte 数组的形式保存,然后调用 本地 的 defineClass0 入口。
/*
* Generate the specified proxy class.
*/
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
/*
* A ClassFormatError here means that (barring bugs in the
* proxy class generation code) there was some other
* invalid aspect of the arguments supplied to the proxy
* class creation (such as virtual machine limitations
* exceeded).
*/
throw new IllegalArgumentException(e.toString());
}
JDK 内部动态代理的逻辑,可以参考java.lang.reflect.ProxyGenerator的内部实现。 可以认为这是种另类的字节码操纵技术,这种实现方式的好处是没有太多依赖关系,但是前提是需要懂各种JVM 指令,知道怎么处理那些偏移地址等,实际门槛非常高,所以并不适合大多数的普通开发场景。
Java 社区专家提供了各种从底层到更高抽象水平的字节码操作类库。JDK 内部也集成了 ASM 类库。
如何利用字节码操纵技术,实现基本的动态代理逻辑?
对于一个普通的 Java 动态代理,其实现过程可以简化成为:
- 提供一个基础的接口,作为被调用类型(com.mycorp.HelloImpl)和代理类之间的统一入口,如 com.mycorp.Hello。
- 实现InvocationHandler,对代理对象方法的调用,会被分派到其 invoke 方法来真正实现动作。
- 通过 Proxy 类,调用其 newProxyInstance 方法,生成一个实现了相应基础接口的代理类实例,可以看下面的方法签名。
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
用 ASM 实现的简要过程:
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(V1_8, // 指定 Java 版本
ACC_PUBLIC, // 说明是 public 类型
"com/mycorp/HelloProxy", // 指定包和类的名称
null, // 签名,null 表示不是泛型
"java/lang/Object", // 指定父类
new String[]{ "com/mycorp/Hello" }); // 指定需要实现的接口
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC, // 声明公共方法
"sayHello", // 方法名称
"()Ljava/lang/Object;", // 描述符
null, // 签名,null 表示不是泛型
null); // 可能抛出的异常,如果有,则指定字符串数组
mv.visitCode();
// 省略代码逻辑实现细节
cw.visitEnd(); // 结束类字节码生成
不同的 visitX 方法提供了创建类型,创建各种方法等逻辑。ASM API,广泛的使用了Visitor模式。
按照前面的分析,字节码操作最后大都应该是生成 byte 数组,ClassWriter 提供了一个简便的方法。cw.toByteArray();
除了动态代理,字节码操纵技术还有那些应用场景?
- 各种 Mock 框架
- ORM 框架
- IOC 容器
- 部分 Profiler 工具,或者运行时诊断工具等
- 生成形式化代码的工具
假如我们有这样一个需求,需要添加某个功能,例如对某类型资源如网络通信的消耗进行统计,重点要求是,不开启时必须是零开销,而不是低开销
将资源消耗的这个实例,用动态代理的方式创建这个实例动态代理对象,在动态代理的invoke中添加新的需求。 开始使用代理对象,不开启则使用原来的方法,因为动态代理是在运行时创建。所以是零消耗。
设计模式应用例子
装饰模式:BufferedInputStream 经过包装,为输入流过程增加缓存,类似这种装饰器还可以多次嵌套,不断地增加不同层次的功能。
public BufferedInputStream(InputStream in)
构建器模式:JDK 最新版本中 HTTP/2 Client API,下面这个创建 HttpRequest的过程就是典型的构建器模式(Builder), 通常会被实现成fluent 风格的 API,也有人叫它方法链。
HttpRequest request = HttpRequest.newBuilder(new URI(uri))
.header(headerAlice, valueAlice)
.headers(headerBob, value1Bob,
headerCarl, valueCarl,
headerBob, value2Bob)
.GET()
.build();