Throwable,Error,Exception,RuntimeException
Error
和Exception
都是Throwable
接口的子类- Java分为“受检查异常”(或
Checked Exception
,这类异常编译器会检查,如果对这类异常既没有try...catch
也没throws
时编译将不通过)和“未检查异常”(或Unchecked Exception
,这类异常编译器不检查),RuntimeException
及其子类都是“未检查异常”,如NullPointException
,其他为“受检查异常”,如IOException
。 Error
一般是指与虚拟机相关的错误,程序一般无法自身恢复(堆内存溢出属于特殊情况),比如各种内存溢出(如OutOfMemoryError
,StackOverflowError
,注意:断言失败是抛出AssertionError
);Exception
则表示程序级别的异常,程序可以处理并恢复。
对于OutOfMemoryError的堆内存溢出,程序不一定无法自身回复,有几点需要注意:
- 如果堆内存溢出,表示无法再分配新的对象,而OutOfMemoryError本来也是一个对象,需要分配内存,是否意味着OutOfMemoryError也存在无法分配在内存导致该Error不能抛出。其实JVM的设计者已经考虑到这个问题,所以OutOfMemoryError是JVM启动时就已经分配,并且该错误信息的调用栈是没有意义的 [1] 。
- 当线程抛出OutOfMemoryError异常时,线程栈会层层弹出,此时有些对象不会被GC Roots引用,会被GC回收,堆空间会释放部分内存。
对象克隆
Object.clone()
方法是native
方法,如果类没有实现Cloneable
接口时将抛出CloneNotSupportedException
,即便是子类重写了clone()
,clone()
方法签名如下:
1 | protected native Object clone() throws CloneNotSupportedException; |
Object.clone()
方法由C++来实现,实现原理是先调用CollectedHeap::array_allocate()
或者CollectedHeap::obj_allocate()
分配与原对象相同大小的新内存,然后调用Copy::conjoint_jlongs_atomic()
将原对象的内存数据复制到新内存当中(所以,该复制方法是浅复制
)。
调用clone()
来克隆对象时需要调用到Object.clone()
,所以子类重写clone()
时,需要调用super.clone()
。
对象克隆分为浅克隆
(或浅复制
)和深克隆
(或深复制
),他们的区别就是会不会为引用类型成员变量调用clone()
来生成新对象。
由于Object.clone()
方法是复制内存数据来克隆对象的,因此,同样可以使用Java序列化机制来复制内存数据达到克隆的效果,而且是深克隆
。
强引用与弱引用
在java.lang.ref
包中存在三种类型的引用类,分别是SoftReference
(软引用),WeakReference
(弱引用),PhantomReference
(虚引用),Java的引用类型除了这三个,还有一个就是强引用(即Java程序中使用等号赋值的引用),它们的级别由高到低分别为强引用、软引用、弱引用、虚引用,引用类型可以控制JVM回收对象的时机。
如果一个对象存在强引用,就不会被回收;如果一个对象只存在软引用,当JVM内存不足时,就会被回收;如果一个对象只存在弱引用,当执行垃圾收集时就会被回收;如果一个对象只存在虚引用,任何时候都可能被回收。
引用类型 | 回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时终止 |
弱引用 | 垃圾收集时 | 对象缓存 | 垃圾收集时终止 |
虚引用 | Unknown | Unknown | Unknown |
静态初始化和实例初始化
静态初始化
静态初始化是指
<cinit>
方法的调用,该方法是编译器自动生成,代码由静态语句块和静态变量(或者类变量)组成。JVM会保证在调用某类的<cinit>
方法时先调用其父类的<cinit>
方法。<cinit>
方法只会被JVM调用一次。实例初始化
实例创建的方式有:
new
、Class.newInstance()
、Constructor.newInstance()
、Object.clone()
、ObjectInputStream.readObject()
等,从Java虚拟机层面来说是两种:new
、invokevirtual
。实例初始化是指
<init>
方法的调用,该方法由构造函数、实例代码块和实力变量组成,<init>
方法的第一行总是会调用父类的<init>
方法。
Java8新特性
Lambda表达式和函数式接口
Lambda表达式(也称为闭包)可以将函数作为参数传递,语法参考Lambda Expression;函数式接口指只有一个抽象方法的接口,可以使用
@FunctionalInterface
标记函数式接口,让编译器检查接口是否为有效的函数式接口。常用的函数接口有:java.lang.Runnable,java.util.concurrent.Callable,java.util.function.Function,java.util.function.Predicate,java.util.function.Supplier,java.util.function.Consumer
接口默认方法和静态方法
接口的默认方法使用
default
修饰并且包括方法体,它不需要接口实现类实现,但可以重写;静态方法的语法同一般类的静态方法。方法引用
方法引用必须与Lambda表达式结合使用,不能直接将方法引用赋值给一个变量。方法引用的类型有:
类型 | 示例 |
---|---|
引用静态方法 | ContainingClasss::staticMethodName |
引用实例方法 | containingObject::instanceMethodName |
引用任意类型的实例方法 | ContainingType::methodName |
引用构造器方法 | ClassName::new |
重复注解
可使用
@Repeatable
添加多个相同的注解更好的类型推断
编译器增强了类型推断功能,比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Value< T > {
public static< T > T defaultValue() {
return null;
}
public T getOrDefault( T value, T defaultValue ) {
return ( value != null ) ? value : defaultValue;
}
}
public class TypeInference {
public static void main(String[] args) {
final Value< String > value = new Value<>();
value.getOrDefault( "22", Value.defaultValue() ); // 这里不需要强制转换类型,编译器可以推断出来
}
}拓宽注解应用场景
Java8添加了
ElementType.TYPE_USER
和ElementType.TYPE_PARAMETER
用来描述注解的使用场景。参数名称
当编译时加上
-parameters
参数时,编译器不会擦除方法的参数名称,此时可以使用Parameter.getName()
获取参数名称。Optional
为了解决大量的
NullPointerException
添加了一个容器类型Optional
,其常用方法如下:
方法名 | 说明 |
---|---|
isPresent() | 如果Optional 实例持有一个非空值,该方法返回true ,否则返回false |
orElseGet() | 如果Optional 实例持有一个空值,则返回lambda表达式的值,如:name.orElseGet(() -> "anonymous") |
map() | map()可以将现有的Optional 实例转换为新值,如:name.map(n -> 'Hi ' + n + "!").orElse("Hi Stranger!") |
orElse() | orElse() 与orElseGet() 类似,区别是orElse() 参数为一个类型T,orElseGet() 为一个lambda表达式。 |
Streams
Java8新增了Stream API,可以方便灵活处理容器的
filter
、map
、reduce
、sort
等串行和并行的聚合操作,Stream由流管道组成,一个流管道由源数据(如数组和集合)、中间操作(产生新的stream对象)、终止操作(产生一个结果)详细参考java.util.stream.Stream。Date/Time API
时间支持以纳秒级表示。
Nashorn Javascript引擎
添加了
jjs
来运行javascript代码文件,也可以运行javascript字符串格式,如:1
2
3
4
5ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName( "JavaScript" );
System.out.println( engine.getClass().getName() );
System.out.println( "Result:" + engine.eval( "function f() { return 1; }; f() + 1;" ) );Base64
添加了
java.util.Base64
,支持Base64
功能。并行数组
可支持并行数组处理,如
Arrays.parallelSetAll()
、Arrays.parallelSort()
。并发性
改进了
java.util.concurrent.ConcurrentHashMap
,添加了java.util.concurrent.locks.StampedLock
替代java.util.concurrent.locks.ReadWriteLock
。JVM新特性
使用Metaspace替代PermGen Space,G1垃圾收集器作为默认收集器。
序列化与反序列化
序列化与反序列化的作用是运行允许对象的数据通过一个JVM进程传到另一个JVM进程。Java世界里可以分为JDK序列化(或称内部序列化)和外部序列化。选择序列化工具时,需要考虑前后兼容问题和序列化效率问题。向前兼容的意思是指旧代码可以反序列化新代码序列化的记录,向后兼容是指新代码可以反序列化旧代码序列化的记录。序列化效率问题指序列化后的字节大小,越小效率越高。
JDK序列化
JDK序列化规则:
- 类实现了
java.io.Serializable
接口时,其对象就可以序列化; java.io.Externalizable
优先级高于java.io.Serializable
,- 对象序列化不会关注类中的静态变量,被
transient
修饰的变量不会被序列化存储,当反序列化时需要负责初始化transient
变量,比如java.util.ArrayList对成员变量elementData
的初始化工作; java.io.ObjectInputStream
处理反序列化工作;java.io.ObjectOutputStream
处理序列化工作;- 自定义反序列化的方式是通过定义
readObject(ObjectInputStream in)
; - 自定义序列化的方式是通过定义
writeObject(ObjectOutputSteam out)
; serialVersionUID
控制对象序列化和反序列化的版本是否兼容,缺省时编译器会自动生成,只要类改变,即便是方法体少了一行代码,serialVersionUID
都会变化;- 对象序列化为字节数组时,类名和字段名都会保留,当反序列化时,不允许少字段,可以多字段;
serialVersionUID
是唯一控制着能否反序列化成功的标志,只要这个值不一样,就无法反序列化成功。但只要这个值相同,无论如何都将反序列化,在这个过程中,对于向前兼容性,新数据流中的多余的内容将会被忽略;对于向后兼容性而言,旧的数据流中所包含的所有内容都将会被恢复,新版本的类中没有涉及到的部分将保持默认值。一旦将新版本中的老的内容拿掉,即使UID保持不变,也会引发异常。比如当我们重写writeObject
和readObject
时,写入或读取的字段是按照顺序操作,一旦顺序更改,反序列化失败。因此可以总结为只要新版本不减少字段,不更改字段类型,仅增加字段,JDK序列化支持前后兼容。
由于JDK序列化时需要将类名,字段名写入到序列化结果中,因此序列化效率不高。
其他序列化工具
Thrift、Protobuf、Avro在序列化效率上远高于JDK序列化,它们只需要写入字段序号、用特定的位来表示类型,前后兼容略灵活 [2] 。
反射
反射机制是程序可以在运行时获取类型或者实例或类的字段描述信息和方法描述信息,然后进行字段的get/set
操作以及方法的调用。通过反射可以获取对象字段的值,也可以使用sun.misc.Unsafe
来快速读取对象的字段值。
以反射调用Object.hashCode
方法为例,叙述反射的原理,样例代码如下:
1 | Object o = new Object(); |
要通过反射获取对象的方法或者字段,首先需要获取Class对象。在静态编码情况下,可以确定Class对象,比如样例代码第二行,可以直接写成Class clazz = Object.class
,当在运行时情况下,可以通过调用对象的getClass
方法获取对象的Class对象。getClass
方法在Object
类中声明,方法签名如下:
1 | public final native Class<?> getClass(); |
getClass
方法是本地方法,由C++来实现,C++方法定义在Object.c文件中,方法定义如下:
1 | JNIEXPORT jclass JNICALL |
GetObjectClass
方法的核心代码如下:
1 | klassOop k = JNIHandles::resolve_non_null(obj)->klass(); |
指向对象的指针称为Ordinary Object Pointer
(OOP),Java实例对象使用C++中的oopDesc来表示,oop
就是指向oopDesc
类型的指针。在JVM中,Java对象的头部由下列两个字段组成:
1 | volatile markOop _mark; |
_metadata
就是指向实例对象的java.lang.Class
的对象,对应C++中的klassOop
。JNIHandles::resolve_non_null(obj)->klass()
就是要获取_metadata
的klassOop
对象,它指向方法区中的类实例对象,类实例对象就是Class
对象。所以o.getClass()
方法的原理就是o
对象存在指向类实例对象的引用,通过该引用可以获取o
对象的Class
对象。
获得了Class
对象,就可以通过getMethod
获得Method
方法引用。getMethod
的调用链为Class.getMethod
->Class.getMethod0
->Class.privateGetMethodRecursive
->Class.privateGetDeclaredMethods
->Class.searchMethods
。在privateGetDeclaredMethods
方法中使用了一个重要的字段,private volatile transient SoftReference<ReflectionData<T>> reflectionData
,ReflectionData
是Class
的内部类,定义如下:
1 | private static class ReflectionData<T> { |
首先,reflectionData
字段是软引用,而reflectionData
指向的ReflectionData
实例对象在内存使用苛刻时就会被GC回收。因此,在获取类对象的方法或者字段时,都需要判断reflectionData
执行的对象是否为null
,如果为null
,表示被回收,需要重新创建,然后通过Atomic.casReflectionData(this, oldReflectionData, new SoftReference<>(rd))
(CAS操作)设置reflectionData
字段。其次,privateGetDeclaredMethods
会判断ReflectionData
对象的declaredPublicMethods
或者declaredMethods
字段是否为null
,如果为null
,表示还未获取类对象的方法,需要调用getDeclaredMethods0
方法获取类对象的方法,并设置到ReflectionData
对象的declaredPublicMethods
或者declaredMethods
字段。
通过privateGetDeclaredMethods
方法可以获取到类实例的方法表,接下来通过searchMethods
搜索需要获取的方法,该方法定义如下:
1 | private static Method searchMethods(Method[] methods, |
searchMethods
方法首先迭代Method[]
方法表,如果期望的Method
方法被找到,需要将Method
方法复制一份,然后返回给样例代码中的hashcode
变量。复制的实现方法copy
定义在Method.java
中。由此可见,每次通过调用getMethod
方法返回的Method对象其实都是一个新的对象,如果调用频繁最好缓存起来。
获取到Method
对象后,接下来就是调用invoke
方法,与invoke
方法相关的字段和方法如下:
1 | private Method root; |
root
字段指向ReflectionData
对象中方法表的某个Method
实例(因为获得的Method
对象是从ReflectionData
复制而来,所以root
保留了被复制的对象或者说原对象)。Method.invoke
方法调用methodAccessor
的invoke
方法。如果root
的methodAccessor
存在,则赋值给methodAccessor
这个属性,否则就创建一个。MethodAccessor
本身就是一个接口,其主要有三种实现
- DelegatingMethodAccessorImpl
- NativeMethodAccessorImpl
- GeneratedMethodAccessorXXX
Method
的methodAccessor
的对象类型就是DelegatingMethodAccessorImpl
,也就是某个Method
的所有的invoke
方法都会调用到这个DelegatingMethodAccessorImpl.invoke
,正如其名一样的,是做代理的,也就是真正的实现可以是NativeMethodAccessorImpl
和GeneratedMethodAccessorXXX
两种。如果是NativeMethodAccessorImpl
,顾名思义,由本地代码C++实现,而GeneratedMethodAccessorXXX
是为每个需要反射调用的Method
动态生成的类,后缀XXX是一个不断递增的数值。并且所有的方法反射都是先走NativeMethodAccessorImpl
,默认调了15次之后,才生成一个GeneratedMethodAccessorXXX
类,生成好之后就会走这个生成的类的invoke
方法。NativeMethodAccessorImpl
的invoke
方法定义如下:
1 | public Object invoke(Object obj, Object[] args) |
generateMethod
会生成一个GeneratedMethodAccessorXXX
实例,它的invoke
方法就是调用样例代码中的o.hashCode()
。通过字节码直接调用对象的方法,称为Java字节码拼接技术,用来在运行时生成Java类。javassist和cglib都是基于字节码拼接技术实现,在Spring
中就是使用cglib
来动态的生成代理类,实现AOP
功能。GeneratedMethodAccessorXXX
的类加载器是一个DelegatingClassLoader
类加载器,使用新的类加载器是为了性能考虑,在某些情况下可以卸载这些生成的类。invoke0
方法是本地方法,由C实现,方法定义在NativeAccessors.c如下:
1 | JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0 |
实际方法调用交给JVM执行即可。
当执行命令java JavaApplication
时,JVM内部也是通过反射获取JavaApplication
的main
方法的Method
对象,然后调用Method.invoke
方法执行main
方法的代码。
对反射使用不当,会造成反射类加载器导致Perm溢出 [3] 。
Statement和PreparedStatement的区别,如何防止SQL注入
JDBC执行SQL语句可以使用三个类,分别是Statement
、PreparedStatement
、CallableStatement
,描述如下表:
类名 | 作用 |
---|---|
Statement | 通用查询 |
PreparedStatemnt | 预编译语句查询,可以解决SQL注入问题,由于是预编译,所以可以减少数据库对SQL代码的解析操作,提高效率 |
CallableStatement | 存储过程查询 |
Java命令
java
启动JVM运行Java字节码的工具;
javac
编译Java代码的工具,或者称为java编译器(javac就是java compiler的缩写);
jar
操作jar文件的工具;
jdb
java调试器工具,作用和用法与gnu的gdb类似;
javah
开发JNI时使用的工具,它可以根据Java代码声明的
native
方法自动生成C的头文件;javap
java字节码反编译工具;
jps
Java Virtual Machine Process Status Tool,用来查看java虚拟机进程状态;
jjs
运行javascript代码的工具;
jdeps
分析java依赖的工具;
jinfo
查看JVM配置信息的工具;
jstack
查看JVM线程栈工具;
jmap
导出JVM内存的工具;
jhat
分析jmap导出的文件的工具;
jconsole
查看虚拟机内存、线程等信息的工具;
jvisualvm
jconsole的增强工具;
javadoc
生成java api文档的工具;
keytool
操作RSA或者证书相关的工具;
rmiregistry
RMI相关工具。
NoClassDefFoundError和ClassNotFoundException
NoClassDefFoundError
和ClassNotFoundException
都是由于在CLASSPATH
下找不到对应的类而引起的,通常是缺少对应的jar包或者jar包冲突导致,具体如下:
ClassNotFoundException
是在代码中显示调用加载类的方法导致,如Class.forName
、ClassLoader.findSystemClass()
和ClassLoader.loadClass()
等;NoClassDefFoundError
是JVM链接时找不到类时抛出的错误,如new
一个实例或者调用静态方法等。
可以简言之:如果找不到类要抛异常时,ClassNotFoundException是类名作为字符串,而NoClassDefFoundError是类名作为符号。
方法动态绑定原理
在Java中, final
,static
,private
以及构造方法与类的绑定关系是在编译期确定,称之为“前期绑定”或者“静态绑定”,对于实例“静态绑定”的方法,采用invokespecial
指令调用。对于其他实例方法,则需要在运行时根据对象类型再行决议,我们称之为“后期绑定”或“动态绑定”,采用invokevirtual
指令调用方法。
以下列代码为例,叙述方法动态绑定原理:
1 | public class VtableExample { |
上述代码的输出结果为
1 | i=0 |
Java方法的动态绑定原理源自C++的虚方法调用,使用一张虚方法表vtable
来控制方法调用,所不同的是C++在编译时就创建了类的虚方法表,而Java在运行时加载类才创建虚方法表。
当第一次加载类时,JVM会调用classFileParser.cpp::parseClassFile()函数对类的字节码解析,调用parseMethods()
函数解析类的所有方法,之后再调用klassVtable::compute_vtable_size_and_num_mirandas()
函数计算当前类的vtable
大小。计算vtable
大小的过程是首先获取父类的vtable
大小,再循环当前类的方法,调用needs_new_vtable_entry
方法判断方法是否需要加入到vtable
(如果方法被声明为public
或者protected
且不是static
或者final
时,称此方法为虚方法,此时该方法返回true
),如果返回true,则vtable
大小加1。当类解析完成后,就需要调用InstanceKlass::allocate_instance_klass()方法分配内存,存储类信息,这些信息就包括vtable
大小。当类信息创建完成后就可以准备方法调用了。在执行真正的方法调用前,需要调用instanceKlass::link_class进行方法链接,此时将会初始化虚方法表。初始化虚方法表的方法在instanceKlass::link_class_impl
中执行,代码如下:
1 | // Initialize the vtable and interface table after |
在initialize_vtable方法中,先复制父类的虚方法表到当前类的虚方法表。然后在update_inherited_vtable
方法中将子类重写的方法入口地址通过klassVtable::put_method_at(Method* m, int index)
方法写回到虚方法表中,以替换父类方法地址。如果不是重写父类的虚方法,需要在虚方法表中插入一个新元素。
当执行invokevirtual
调用虚方法时,由LinkResolver::resolve_invoke完成解析任务,该方法定义如下:
1 | void LinkResolver::resolve_invoke(CallInfo& result, Handle recv, constantPoolHandle pool, int index, Bytecodes::Code byte, TRAPS) { |
在样例代码中,当执行new Son()
时,先创建Father
的虚方法表,假设print
方法在虚方法表位置为n
,父类初始化完成后,开始初始化子类Son
,然后创建Son
的虚方法表。创建Son
的虚方法表时,先将父类的虚方法表复制到子类的虚方法表中,此时子类虚方法表位置为n
的方法是Father.print
。当执行update_inherited_vtable
方法时会将子类的print
方法入口写入到虚方法表位置为n
的地方,此时虚方法表位置为n
的方法是Son.print
。所有类信息构造完成后,开始执行Son
的构造函数,它首先调用Father
的构造函数,在此函数中,会调用print
方法,实际上是invokevirtual print
指令。通过instanceKlass::uncached_lookup_method
方法在Father
类中查询print
方法,可以找到该方法,该方法使用methodOopDesc*表示,即methodOop
指针,指向Father.print
,它记录了通过klassVtable::put_method_at(Method* m, int index)
放入虚方法表的位置n
。然后在LinkResolver::runtime_resolve_virtual_method
方法中通过位置n
在Son
的虚方法表中找到真正要执行的方法,即Son.print
。最后调用Son.print
方法。
异常处理原理
当使用javac
编译java源码时,会为方法内的try/catch/finally
语句块生成一个异常表(exception_table
),异常表指定了当出现异常时代码需要跳转到何处执行 [4] 。
以如下代码为例:
1 | public static void main(String[] args) throws Exception { |
当使用javac
编译时,会生成如下字节码(javap
):
1 | 0: new #17 // class java/lang/Exception |
异常表内容为:
1 | from to target type |
异常表的内容由四部分组成:
- from 抛出异常的开始行
- to 抛出异常的结束行
- target 需要跳转的行
- type 异常类型
异常表内容的第一行表示如果在第0行(此处的行指程序地址,比如第3行是指程序地址为3的指令,即dup
)和第8行抛出java.lang.Exception
异常时,跳转到第8行执行代码。如果在第0行和第17号抛出代码时,跳转到第28行执行代码。如果当前方法未找到合适的异常处理器时,当前方法弹栈,交给栈顶方法处理。如果线程栈方法全部弹出也未找到异常处理器,则线程结束。
泛型原理
泛型可以对类和接口的类型参数化,使用比较多的是容器类,比如List<String>
就是将List
的元素参数化为String
。使用泛型的作用有:
- 编译器强类型检查
- 去掉强制类型转换的代码
- 支持泛型算法的实现
类型参数的命名习惯如下:
- E - Element (used extensively by the Java Collections Framework)
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
泛型示例代码如下:
1 | public class Generic<T> { |
编译上述代码,然后反编译后得到如下信息:
1 | public class Generic<T extends java.lang.Object> extends java.lang.Object |
从反编译代码中可以看到,字段t
的类型是Object
,而代码Generic<String> g = new Generic<String>();
的泛型信息被擦除,变为Generic g = new Generic();
,此时已经看不到泛型信息。对于类型元信息,即类、字段和方法(包括参数和返回值)不会擦除泛型信息,因此可以通过反射来获取泛型信息。上述代码Generic<T>
变编译后,泛型信息是Generic<T extends java.lang.Object>
,而变量g
由于泛型类型被擦除,无法通过反射获取其泛型类型。可以通过new Generic<String>(){}
构造一个子类,此时可以通过反射获取泛型信息。这种方式使用的比较多的是在json解析中,当要解析一串json文本为带有泛型的类型时使用如fastjson的用法JSONObject.parseObject(json, new TypeReference<List<Person>>(){})
。