Java后端开发 - 基础

Throwable,Error,Exception,RuntimeException

  • ErrorException都是Throwable接口的子类
  • Java分为“受检查异常”(或Checked Exception,这类异常编译器会检查,如果对这类异常既没有try...catch也没throws时编译将不通过)和“未检查异常”(或Unchecked Exception,这类异常编译器不检查),RuntimeException及其子类都是“未检查异常”,如NullPointException,其他为“受检查异常”,如IOException
  • Error一般是指与虚拟机相关的错误,程序一般无法自身恢复(堆内存溢出属于特殊情况),比如各种内存溢出(如OutOfMemoryErrorStackOverflowError,注意:断言失败是抛出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调用一次。

  • 实例初始化

    实例创建的方式有:newClass.newInstance()Constructor.newInstance()Object.clone()ObjectInputStream.readObject()等,从Java虚拟机层面来说是两种:newinvokevirtual

    实例初始化是指<init>方法的调用,该方法由构造函数、实例代码块和实力变量组成,<init>方法的第一行总是会调用父类的<init>方法。

Java8新特性

类型 示例
引用静态方法 ContainingClasss::staticMethodName
引用实例方法 containingObject::instanceMethodName
引用任意类型的实例方法 ContainingType::methodName
引用构造器方法 ClassName::new
  • 重复注解

    可使用@Repeatable添加多个相同的注解

  • 更好的类型推断

    编译器增强了类型推断功能,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public 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_USERElementType.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,可以方便灵活处理容器的filtermapreducesort等串行和并行的聚合操作,Stream由流管道组成,一个流管道由源数据(如数组和集合)、中间操作(产生新的stream对象)、终止操作(产生一个结果)详细参考java.util.stream.Stream

  • Date/Time API

    时间支持以纳秒级表示。

  • Nashorn Javascript引擎

    添加了jjs来运行javascript代码文件,也可以运行javascript字符串格式,如:

    1
    2
    3
    4
    5
    ScriptEngineManager 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保持不变,也会引发异常。比如当我们重写writeObjectreadObject时,写入或读取的字段是按照顺序操作,一旦顺序更改,反序列化失败。因此可以总结为只要新版本不减少字段,不更改字段类型,仅增加字段,JDK序列化支持前后兼容。

由于JDK序列化时需要将类名,字段名写入到序列化结果中,因此序列化效率不高。

其他序列化工具

Thrift、Protobuf、Avro在序列化效率上远高于JDK序列化,它们只需要写入字段序号、用特定的位来表示类型,前后兼容略灵活 [2]

反射

反射机制是程序可以在运行时获取类型或者实例或类的字段描述信息和方法描述信息,然后进行字段的get/set操作以及方法的调用。通过反射可以获取对象字段的值,也可以使用sun.misc.Unsafe来快速读取对象的字段值。

以反射调用Object.hashCode方法为例,叙述反射的原理,样例代码如下:

1
2
3
4
Object o = new Object();
Class clazz = o.getClass();
Method hashCode = clazz.getMethod("hashCode", null);
System.out.println(hashCode.invoke(o, null));

要通过反射获取对象的方法或者字段,首先需要获取Class对象。在静态编码情况下,可以确定Class对象,比如样例代码第二行,可以直接写成Class clazz = Object.class,当在运行时情况下,可以通过调用对象的getClass方法获取对象的Class对象。getClass方法在Object类中声明,方法签名如下:

1
public final native Class<?> getClass();

getClass方法是本地方法,由C++来实现,C++方法定义在Object.c文件中,方法定义如下:

1
2
3
4
5
6
7
8
9
10
JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
if (this == NULL) {
JNU_ThrowNullPointerException(env, NULL);
return 0;
} else {
return (*env)->GetObjectClass(env, this);
}
}

GetObjectClass方法的核心代码如下:

1
2
3
klassOop k = JNIHandles::resolve_non_null(obj)->klass();
jclass ret =
(jclass) JNIHandles::make_local(env, Klass::cast(k)->java_mirror());

指向对象的指针称为Ordinary Object Pointer(OOP),Java实例对象使用C++中的oopDesc来表示,oop就是指向oopDesc类型的指针。在JVM中,Java对象的头部由下列两个字段组成:

1
2
3
4
5
volatile markOop  _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;

_metadata就是指向实例对象的java.lang.Class的对象,对应C++中的klassOopJNIHandles::resolve_non_null(obj)->klass()就是要获取_metadataklassOop对象,它指向方法区中的类实例对象,类实例对象就是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>> reflectionDataReflectionDataClass的内部类,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static class ReflectionData<T> {
volatile Field[] declaredFields;
volatile Field[] publicFields;
volatile Method[] declaredMethods;
volatile Method[] publicMethods;
volatile Constructor<T>[] declaredConstructors;
volatile Constructor<T>[] publicConstructors;
// Intermediate results for getFields and getMethods
volatile Field[] declaredPublicFields;
volatile Method[] declaredPublicMethods;
volatile Class<?>[] interfaces;

// Value of classRedefinedCount when we created this ReflectionData instance
final int redefinedCount;

ReflectionData(int redefinedCount) {
this.redefinedCount = redefinedCount;
}
}

首先,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)
{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}

return (res == null ? res : getReflectionFactory().copyMethod(res));
}

searchMethods方法首先迭代Method[]方法表,如果期望的Method方法被找到,需要将Method方法复制一份,然后返回给样例代码中的hashcode变量。复制的实现方法copy定义在Method.java中。由此可见,每次通过调用getMethod方法返回的Method对象其实都是一个新的对象,如果调用频繁最好缓存起来。

获取到Method对象后,接下来就是调用invoke方法,与invoke方法相关的字段和方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private Method              root;
private volatile MethodAccessor methodAccessor;
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

root字段指向ReflectionData对象中方法表的某个Method实例(因为获得的Method对象是从ReflectionData复制而来,所以root保留了被复制的对象或者说原对象)。Method.invoke方法调用methodAccessorinvoke方法。如果rootmethodAccessor存在,则赋值给methodAccessor这个属性,否则就创建一个。MethodAccessor本身就是一个接口,其主要有三种实现

  • DelegatingMethodAccessorImpl
  • NativeMethodAccessorImpl
  • GeneratedMethodAccessorXXX

MethodmethodAccessor的对象类型就是DelegatingMethodAccessorImpl,也就是某个Method的所有的invoke方法都会调用到这个DelegatingMethodAccessorImpl.invoke,正如其名一样的,是做代理的,也就是真正的实现可以是NativeMethodAccessorImplGeneratedMethodAccessorXXX两种。如果是NativeMethodAccessorImpl,顾名思义,由本地代码C++实现,而GeneratedMethodAccessorXXX是为每个需要反射调用的Method动态生成的类,后缀XXX是一个不断递增的数值。并且所有的方法反射都是先走NativeMethodAccessorImpl,默认调了15次之后,才生成一个GeneratedMethodAccessorXXX类,生成好之后就会走这个生成的类的invoke方法。NativeMethodAccessorImplinvoke方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}

return invoke0(method, obj, args);
}

generateMethod会生成一个GeneratedMethodAccessorXXX实例,它的invoke方法就是调用样例代码中的o.hashCode()。通过字节码直接调用对象的方法,称为Java字节码拼接技术,用来在运行时生成Java类。javassistcglib都是基于字节码拼接技术实现,在Spring中就是使用cglib来动态的生成代理类,实现AOP功能。GeneratedMethodAccessorXXX的类加载器是一个DelegatingClassLoader类加载器,使用新的类加载器是为了性能考虑,在某些情况下可以卸载这些生成的类。invoke0方法是本地方法,由C实现,方法定义在NativeAccessors.c如下:

1
2
3
4
5
JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0
(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args)
{
return JVM_InvokeMethod(env, m, obj, args);
}

实际方法调用交给JVM执行即可。

当执行命令java JavaApplication时,JVM内部也是通过反射获取JavaApplicationmain方法的Method对象,然后调用Method.invoke方法执行main方法的代码。

对反射使用不当,会造成反射类加载器导致Perm溢出 [3]

Statement和PreparedStatement的区别,如何防止SQL注入

JDBC执行SQL语句可以使用三个类,分别是StatementPreparedStatementCallableStatement,描述如下表:

类名 作用
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相关工具。

详细见JDK Tools and Utilities

NoClassDefFoundError和ClassNotFoundException

NoClassDefFoundErrorClassNotFoundException都是由于在CLASSPATH下找不到对应的类而引起的,通常是缺少对应的jar包或者jar包冲突导致,具体如下:

  • ClassNotFoundException是在代码中显示调用加载类的方法导致,如Class.forNameClassLoader.findSystemClass()ClassLoader.loadClass()等;
  • NoClassDefFoundError是JVM链接时找不到类时抛出的错误,如new一个实例或者调用静态方法等。

可以简言之:如果找不到类要抛异常时,ClassNotFoundException是类名作为字符串,而NoClassDefFoundError是类名作为符号。

方法动态绑定原理

在Java中, finalstaticprivate以及构造方法与类的绑定关系是在编译期确定,称之为“前期绑定”或者“静态绑定”,对于实例“静态绑定”的方法,采用invokespecial指令调用。对于其他实例方法,则需要在运行时根据对象类型再行决议,我们称之为“后期绑定”或“动态绑定”,采用invokevirtual指令调用方法。

以下列代码为例,叙述方法动态绑定原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class VtableExample {
static class Father {
protected int i = 1;
public Father() {
print();
}
public void print() {
System.out.println("i=" + i);
}
}

static class Son extends Father {
protected int i = 2;
public Son() {

}
public void print() {
System.out.println("i=" + i);
}
}

public static void main(String[] args) {
Father fa = new Son();
fa.print();
}
}

上述代码的输出结果为

1
2
i=0
i=2

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
2
3
4
5
6
7
8
9
// Initialize the vtable and interface table after
// methods have been rewritten since rewrite may
// fabricate new methodOops.
// also does loader constraint checking
if (!this_oop()->is_shared()) {
ResourceMark rm(THREAD);
this_oop->vtable()->initialize_vtable(true, CHECK_false);
this_oop->itable()->initialize_itable(true, CHECK_false);
}

initialize_vtable方法中,先复制父类的虚方法表到当前类的虚方法表。然后在update_inherited_vtable方法中将子类重写的方法入口地址通过klassVtable::put_method_at(Method* m, int index)方法写回到虚方法表中,以替换父类方法地址。如果不是重写父类的虚方法,需要在虚方法表中插入一个新元素。

当执行invokevirtual调用虚方法时,由LinkResolver::resolve_invoke完成解析任务,该方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
void LinkResolver::resolve_invoke(CallInfo& result, Handle recv, constantPoolHandle pool, int index, Bytecodes::Code byte, TRAPS) {
switch (byte) {
case Bytecodes::_invokestatic : resolve_invokestatic (result, pool, index, CHECK); break;
case Bytecodes::_invokespecial : resolve_invokespecial (result, pool, index, CHECK); break;
case Bytecodes::_invokevirtual : resolve_invokevirtual (result, recv, pool, index, CHECK); break;
case Bytecodes::_invokehandle : resolve_invokehandle (result, pool, index, CHECK); break;
case Bytecodes::_invokedynamic : resolve_invokedynamic (result, pool, index, CHECK); break;
case Bytecodes::_invokeinterface: resolve_invokeinterface(result, recv, pool, index, CHECK); break;
}
return;
}

在样例代码中,当执行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方法中通过位置nSon的虚方法表中找到真正要执行的方法,即Son.print。最后调用Son.print方法。

异常处理原理

当使用javac编译java源码时,会为方法内的try/catch/finally语句块生成一个异常表(exception_table),异常表指定了当出现异常时代码需要跳转到何处执行 [4]

以如下代码为例:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) throws Exception {
try {
throw new Exception();
} catch (Exception e) {
System.out.print("Caught!");
} finally {
System.out.print("Finally!");
}
}

当使用javac编译时,会生成如下字节码(javap):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0: new           #17                 // class java/lang/Exception
3: dup
4: invokespecial #19 // Method java/lang/Exception."<init>":()V
7: athrow
8: astore_1
9: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
12: ldc #26 // String Caught!
14: invokevirtual #28 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
17: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #34 // String Finally!
22: invokevirtual #28 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
25: goto 39
28: astore_2
29: getstatic #20 // Field java/lang/System.out:Ljava/io/PrintStream;
32: ldc #34 // String Finally!
34: invokevirtual #28 // Method java/io/PrintStream.print:(Ljava/lang/String;)V
37: aload_2
38: athrow
39: return

异常表内容为:

1
2
3
from  to  target type
0 8 8 Class java/lang/Exception
0 17 28 any

异常表的内容由四部分组成:

  • 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
2
3
4
5
6
7
public class Generic<T> {
private T t;
public static void main(String[] args) {
Generic<String> g = new Generic<String>();
g.t = "Hello";
}
}

编译上述代码,然后反编译后得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Generic<T extends java.lang.Object> extends java.lang.Object
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Class #21 // Generic
#3 = Methodref #2.#20 // Generic."<init>":()V
#4 = String #22 // Hello
#5 = Fieldref #2.#23 // Generic.t:Ljava/lang/Object;
#6 = Class #24 // java/lang/Object
...
{
public Generic();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class Generic
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: ldc #4 // String Hello
11: putfield #5 // Field t:Ljava/lang/Object;
14: return
}

从反编译代码中可以看到,字段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>>(){})