深入理解Java虚拟机高级特性与最佳实践 - 全国统一考试试卷(上)

686 阅读22分钟

本文整理自 深入理解Java虚拟机:JVM高级特性与最佳实践 -- 周志明

第一部分:自动内存管理机制

一、Java 内存区域 以及 对象内存分配

Num 1:运行时数据区域

题目1

Java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、本地方法栈、堆、程序计数器,其中      1             2       是由线程共享的数据区,其他几个是线程隔离的数据区。程序计数器,虚拟机栈,本地方法栈,随线程而生,线程亡而亡

  • A:程序计数器 、 堆
  • B:本地方法栈 、 虚拟机栈
  • C:方法区 、 堆

题目2

程序计数器所在内存区域是唯一一个在Java虚拟机规范中没有规定任何       1       情况的区域。

  • A:RuntimeException
  • B:StackOverflowError
  • C:OutOfMemoryError

题目3

虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于储存局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

      1       存放了编辑期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(refrence)类型和returnAddress类型(指向了一条字节码指令的地址)

  • A:动态链接
  • B:操作数栈
  • C:局部变量表

题目4

如果线程请求的栈深度大于虚拟机所允许的深度,将抛出       1       异常。如果虚拟机扩展时无法申请到足够的内存,就会抛出       2       异常

  • A:StackOverflowExceptionOutOfMemoryException
  • B:OutOfMemoryErrorStackOverflowError
  • C:StackOverflowErrorOutOfMemoryError

题目5

      1       是垃圾收集器管理的主要区域。按照分代收集算法,其可细分为新生代和老年代,划分的目的都是为了更好的回收内存,或者更快地分配内存

  • A:方法区
  • B:虚拟机栈
  • C:Java堆

Num 2:对象的创建过程

  • 1、虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用。
  • 2、检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 3、虚拟机为新生对象分配内存。所需内存大小在类加载完成后便可完全确定。

以上三步之后,站在虚拟机的视角,一个新的对象已经产生了。但从Java程序的视角来看,对象创建才刚刚开始,紧接着执行<init>,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

题目6

假设Java堆中内存是绝对完整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那么,分配内存就仅仅是把那个指针向空闲空间那边挪动一段和对象大小相等的距离,这种对象内存分配方式称为:      1       ,Serial、ParNew等带有Compact过程的收集器时,采用此分配算法;

反之,如果已使用的内存和空闲的内存相互交错,虚拟机必须维护一个列表,记录那些内存是可用的,在分配的时候,从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。这种对象内存分配方式称为:      1       ,使用CMS这种基于Mark-Sweep算法的收集器时,采用此分配算法;

选择哪种对象分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

  • A:指针移动列表记录
  • B:空闲列表指针碰撞
  • C:指针碰撞空闲列表

题目7

在虚拟机中,对象创建是非常频繁的行为,在并发情况下,对象的内存分配是:      1      

  • A:线程安全
  • B:不知道
  • C:线程不安全

题目8

以“修改对象指针位置”为例,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同事使用了原来的指针分配内存的情况。解决这种问题,有两类方案,一类是对分配内存空间的动作进行同步处理,即:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;另一种方式是把内存分配的动作按照线程划分在不同的控件之中进行,即:每个线程在Java堆中预先分配一小块内存,称为:      1      

  • A:ThreadLocal
  • B:线程工作内存
  • C:本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

Num 3:对象的内存布局

题目9

对象在内存中存储的布局可用分为3块区域:      1       、实例数据(Instance Data)和对齐填充(Padding)。

  • A:锁状态标志
  • B:哈希码
  • C:对象头(Header)

HotSpot 虚拟机的对象头包括两部分信息。第一部分用于存储对象自身的运行时数据,如:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;另外一部分数据是类型指针,即:对象指向它的雷元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。如果一个对象是Java数组,那在对象头中,还必须有一块用于记录数组长度的数据。

Num 4:对象的访问定位

题目10

建立对象是为了使用对象,Java程序需要通过栈上的reference数据来操作堆上的具体对象。reference与具体的对象的访问方式,有:句柄和      1       两种。

  • A:强引用
  • B:句柄
  • C:直接指针

二、垃圾收集器 与 内存分配策略

Java 与 C++ 之间有一堵内存动态分配与垃圾收集技术围成的“高墙”,墙外的人想进去,墙里面的人却想出来。

经过半个多世纪的发展,Java 内存的动态分配与回收技术已经相当成熟,一切看起来都进入了“自动化”时代,那为什么我们还要去了解GC和内存分配呢?

答案很简单,当需要排查各种内存泄漏、内存溢出问题时、当垃圾收集达到高并发的瓶颈时,就需要对这些“自动化”技术试验实施必要的监控和调节。

Num 1:对象活着吗?

引用计数器法:给对象添加一个引用计数器,每当由一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

题目11

按照引用计数法,此时 classGcA、classGcB 可以被回收吗?

public class ClassGc {

    public Object instance = null;

    public static void main(String[] args) {
        ClassGc classGcA = new ClassGc();
        ClassGc classGcB = new ClassGc();

        classGcA.instance = classGcB;
        classGcB.instance = classGcA;

        classGcA = null;
        classGcB = null;

        // 按照引用计数法,此时 classGcA、classGcB 可以被回收吗?
        System.gc();
    }
}
  • A:不一定,可能回收,也可能不回收
  • B:可回收
  • C:循环引用,不可回收

可达性分析算法:通过一系列的成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象时不可用的。GC Roots就是一组活跃的引用对象链。Java语言中GC Roots的对象包括下面几种:

  • 1.虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 2.方法区中类静态属性引用的对象
  • 3.方法区中常量引用的对象
  • 4.本地方法栈JNI(Native方法)引用的对象

题目12

在Jdk 1.2 之后,Java 对引用的概念进行了扩充,将引用分为:强引用(Strong Reference)、软引用(Soft Reference)、      1       、虚引用(Phantom Refence)。

  • A:微引用
  • B:直接引用
  • C:弱引用(Weak Reference)

题目13

弱引用是用来描述非必须对象的,但是它的强度比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够都会回收掉只被弱引用关联的对象。

byte[] myByteArray = new byte[1];
WeakReference weakReference = new WeakReference(myByteArray);
System.out.println(weakReference.get()==null);
System.gc();
System.out.println(weakReference.get()==null);
// ------------------
WeakReference weakReference = new WeakReference(new byte[1]);
System.out.println(weakReference.get()==null);
System.gc();
System.out.println(weakReference.get()==null);

如上代码段的结果是:

  • A:false fase false false
  • B:false true false true
  • C:false fase false true

Num 2:生存还是死亡

对象回收过程:不可达对象 --> 两次标记(自救) --> 自救失败死亡

题目14

程序计数器、      1       、本地方法栈3个区域随线程而生,随线程而灭,在这几个区域内就不需要过多考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。

  • A:方法区
  • B:Java堆
  • C:虚拟机栈

栈中的栈帧随着方法的进入和退出就有条不紊的执行者出栈和入栈的操作,每一个栈分配多少个内存基本都是在类结构确定下来的时候就已经确定了,这几个区域内存分配和回收都具有确定性

一个接口的实现是多种多样的,多个实现类需要的内存可能不一样,一个方法中多个分支需要的内存也不一样,我们只能在程序运行的期间才会知道需要创建那些对象,分配多少内存,这部分的内存分配和回收都是动态的

题目三

      1       的垃圾收集主要回收两部分内容:废弃常量和无用的类

  • A:Java 堆
  • B:虚拟机栈
  • C:方法去 - 永久代

Num 3:垃圾收集算法

分代收集算法是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

问题15

      1       中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。

  • A:永久代
  • B:老年代
  • C:新生代

问题16

      1       中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记清理或者标记整理算法来进行回收。

  • A:永久代
  • B:新生代
  • C:老年代

Num 4:内存分配以及回收策略

问题17

大多数情况下,对象优先在      1       分配

  • A:永久代
  • B:老年代
  • C:Eden

问题18

大对象直接进入       1       ;长期存活的对象将进入       2      

  • A:永久代Eden
  • B:Eden老年代
  • C:老年代老年代

第二部分:虚拟机执行子系统

一、类文件结构

Class 文件是 Java 虚拟机执行引擎的数据入口,也是Java技术体系的基础构成之一。

类似 与硬件通信的 RIL 指令,Class 文件也是按照 Java虚拟机规范 协议来编排的。

二、类加载的过程

Num 1:类加载时机

问题19

类从被加载到虚拟机内存中开始,到卸载出内存为止,生命周期包括:加载验证准备解析      1       使用卸载 7 个阶段。其中:验证准备解析 三个部分统称为 连接。

  • A:new
  • A:实例化
  • C:初始化

Num 2:初始化

加载验证准备初始化卸载这5个阶段的顺序是确定的,即:必须按照这种顺序按部就班地进行。有且只有5中情况必须立即对类进行“初始化”,此时加载验证准备自然需要在此之前进行。这5中情况是:

  • 遇上newgetstaticputstaticinvokestatic这4条字节码指令时。
    • new:实例化对象
    • getstatic/putstatic:读取或设置一个类静态字段
    • invokestatic:调用一个类的静态方法
  • 使用java.lang.reflect包的方法对类进行反射调用的时候。
  • 初始化时,当发现其父类未初始化,则需先对父类初始化
  • 当虚拟机启动时,用户需要制定一个要执行的主类
  • Java 动态语言支持,java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。

问题20

public class ClassInitTest {
    public static void main(String[] args) {
        System.out.println(SubClass.SuperClassIntValue);
    }
}

class SuperClass {
    public static int SuperClassIntValue = 123;

    static {
        System.out.println("SuperClass init!");
    }
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init!");
    }
}

如上代码,输出什么?

  • A:SubClass init!
  • B:
SubClass init!
SuperClass init!
  • C:SuperClass init!

解析:对于静态字段,只有直接定义这个字段的类才会被初始化。

问题21

public class ClassArrayInitTest {
    public static void main(String[] args) {
        SuperClass[] superClasseArray = new SuperClass[10];
    }

    static class SuperClass {
        static {
            System.out.println("SuperClass init!");
        }
    }
}

如上代码,输出什么?

  • A:1次 SuperClass init!
  • B:SuperClass init!
  • C:什么都不输出!

问题22

public class ClassStaticFinalInitTest {
    public static void main(String[] args) {
        System.out.println(SuperClass.SuperClassIntValue);
    }

    static class SuperClass {
        public static final int SuperClassIntValue = 123;

        static {
            System.out.println("SuperClass init!");
        }
    }
}

如上代码,输出什么?

  • A:
SuperClass init!
123
  • B:
123
SuperClass init!
  • C:123

Num 2:加载阶段

类加载阶段,需要完成以下三件事情:

  • 1、通过一个类的全限定名类获取定义此类的二进制字节流
  • 2、将这字节流所代表的静态存储结构转化为方法区运行时数据结构
  • 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

怎么获取二进制字节流?

  • 1、从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础
  • 2、从网络中获取,这种场景最典型的应用就是Applet
  • 3、运行时计算生成,这种常见使用得最多的就是动态代理技术
  • 4、由其他文件生成,典型场景就是JSP应用
  • 5、从数据库中读取,这种场景相对少一些(中间件服务器)

Num 3:验证阶段

验证阶段是链接阶段的第一步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致上会完成下面4个阶段的检验动作:

  • 1、文件格式验证
  • 2、元数据验证
  • 3、字节码验证
  • 4、符号引用验证

Num 4:准备

准备阶段是为类变量(非实例变量)分配内存并设置类变量初始值的阶段,这些变量都在方法区中进行分配。这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里说的初始值通常下是数据类型的零值。

问题23

例:public static int value = 123;中变量value准备阶段过后的初始值为0而不是123

这是因为,这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在      1       阶段才会执行,但是如果使用final修饰,则在这个阶段其初始值设置为123

  • A:解析阶段
  • B:使用阶段
  • C:初始化阶段

Num 5:解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

类或接口解析时:把一个类D中的一个从未解析过的符号引用N解析为一个类或接口C的直接引用,但发现类D不具有对类C的访问权限; 字段解析此时,将会从下往上递归搜索名称和字段描述符都匹配的字段并返回直接引用,但发现不具备对字段的访问权限; 类方法解析时,将会在类C的父类中递归查找名称和字段描述符都匹配的方法并返回这个方法的直接引用,但发现不具备对此方法的访问权限。

问题24

如上三种情形,将抛出:      1       异常?接口方法解析不会存在此访问权限的问题,是因为接口中的所有方法默认都是 public 修饰的。

A:java.lang.IncompatibleClassChangeError异常。 B:java.lang.NoSuchFieldError异常。 C:java.lang.IllegalAccessError异常。

问题25

字段解析:从未被解析过的字段符号引用,当解析完成,将会从下往上递归搜索该字段名称和字段描述符都匹配的字段并返回字段直接引用,如果查找失败,抛出:      1       异常。

  • A:java.lang.IncompatibleClassChangeError异常。
  • B:java.lang.IllegalAccessError异常。
  • C:java.lang.NoSuchFieldError异常。

Num 6:初始化

初始化阶段是执行类构造器<clinit>方法的过程。除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,开发者才开始参与进来。附:<clinit>:类构造器。Constructor:实例构造器。

<clinit>方法由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{})中的语句合并产生的,父类的<clinit>先于子类执行,即:父类定义的静态语块优于子类的变量赋值操作。

问题26

public class Main {
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

class Parent {
    public static int A = 1;

    static {
        A = 2;
    }
}

class Sub extends Parent {
    public static int B = A;
}

如上代码段的结果是:

  • A:0
  • B:1
  • C:2

问题27

public class BlockRunningTest {

    static {
        if (true) {
            System.out.println(Thread.currentThread() + "初始化 BlockRunningTest");
            while (true) {

            }
        }
    }

    public static void main(String[] args) {
        Runnable runnableTask = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread() + "线程任务 ------> 开始!");
                BlockRunningTest blockRunningTest = new BlockRunningTest();
                System.out.println(Thread.currentThread() + "线程任务 ------> 结束!");
            }
        };

        Thread thread1 = new Thread(runnableTask);
        Thread thread2 = new Thread(runnableTask);

        thread1.start();
        thread2.start();
    }
}

如上代码段的结果是:

  • A:
Thread[thread1 线程描述]线程任务 ------> 开始!
Thread[thread2 线程描述]线程任务 ------> 开始!
Thread[thread1 线程描述]初始化 BlockRunningTest
Thread[thread2 线程描述]初始化 BlockRunningTest
Thread[thread1 线程描述]线程任务 ------> 结束!
Thread[thread2 线程描述]线程任务 ------> 结束!
  • B:
Thread[thread1 线程描述]线程任务 ------> 开始!
Thread[thread2 线程描述]线程任务 ------> 开始!
Thread[thread1 线程描述]初始化 BlockRunningTest
Thread[thread2 线程描述]初始化 BlockRunningTest
  • C:
Thread[thread1 线程描述]线程任务 ------> 开始!
Thread[thread2 线程描述]线程任务 ------> 开始!
Thread[thread1 线程描述]初始化 BlockRunningTest

问题解析:虚拟机会保证一个类的<clinit>方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>方,其他线程都徐璈阻塞等待,直到活动线程执行<clinit>方法完毕。

Num 7:类加载器

  • 每一个类加载器,都拥有一个独立的类名称空间。
  • 对任意一个类都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

所以,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的相等包括代表类的 equals()isAssignableFrom()isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定的情况。

问题28

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myClassLoader = customClassLoader();

        Object obj = myClassLoader.loadClass(ClassLoaderTest.class.getName()).newInstance();

        System.out.println(obj instanceof ClassLoaderTest);
    }

    static ClassLoader customClassLoader() {
        return new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String classFileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream is = getClass().getResourceAsStream(classFileName);
                if (is == null) {
                    return super.loadClass(name);
                }

                try {
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return super.loadClass(name);
            }
        };
    }
}

如上代码段的结果是:

  • A:undefine
  • B:true
  • C:false

Num 8:类加载器 - 双亲委派模型

从Java虚拟机的角度来讲,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):C++语言实现,是虚拟机自身的一部分。负责将<JAVA_HOME>\lib目录中类库加载到内存中。开发者无法直接引用。
  • 其他类加载器:Java语言实现,独立于虚拟机外部,全部继承自:java.lang.ClassLoader
    • 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的所有类库。开发者可以直接使用。
    • 应用程序类加载器(Application ClassLoader):由sun.misc.Launcher$ApplicationClassLoader实现,ClassLoader.getSystemClassLoader()的返回值。一般情况下,它也是开发者开发的Java程序的默认类加载器。

双亲委派模型要求除了顶层的启动加载类外,其余的类加载器都应当有自己的父类加载器。 类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父类加载器的代码。

题目29

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给 【选择填空1】 去完成,每一个层次的类加载器都是如此,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有当 【选择填空1】 反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,【选择填空2】 才会尝试自己去加载。

  • A:父类加载器 、 父类加载器
  • B:子类加载器 、 父类加载器
  • C:父类加载器 、 子类加载器

题目30

双亲委派模型的源代码都在java.lang.ClassLoader的loadClass()方法中,逻辑如下:

  • 先检查是否被加载过,若没有,则:调用父加载器的loadClass()方法。

  • 若父加载器为空,则默认使用启动类加载器作为父加载器。

  • 如果父加载器加载失败,抛出:【选择填空】,再调用自己的findClass()方法进行加载。

  • A:NoSuchFieldError异常。

  • B:IllegalAccessError异常。

  • C:ClassNotFoundException异常。

Num 8:MethodHandle

JDK 1.7 新加入了java.lang.invoke包,这个包提供了一种新的动态确定目标方法的机制,称为:MethodHandle,可以类比C/C++中的函数指针MethodHandle之后,Java 相当于拥有了类似函数指针或者委托方法别名的工具了。

MethodHandleReflection 反射的区别:

  • MethodHandleReflection都是在模拟方法调用。Reflection属于Java代码层次的方法调用。MethodHandle是模拟字节码层次的方法调用。
  • Reflection是方法在Java一端的全部映像,包括了方法签名、描述符等等。MethodHandle仅仅包含与执行该方法相关的信息。换句话来说:Reflection是重量级,而:MethodHandle轻量级。

仅站在Java语言的角度来看:Reflection API的设计目标是只为Java语言服务的,而:MethodHandle则设计成可服务于所有Java虚拟机之上的语言。

问题31

public class MethodHandleTest {
    static class ClassA {
        public void println(String s) {
            System.out.println("ClassA println --> " + s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object classAObj = new ClassA();
        getPrintlnMH(classAObj).invokeExact("HelloWorld!");
    }

    private static MethodHandle getPrintlnMH(Object receiver) throws Throwable {
        MethodType mt = MethodType.methodType(void.class, String.class);

        return MethodHandles.lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    }
}

如上代码段的结果是:

  • A:不输出
  • B:HelloWorld
  • C:ClassA println --> HelloWorld!

Num 9:字节码生成技术与动态代理的实现

“字节码生成”并不是什么高深的技术,在看到“字节码生成”这个标题时,先不必去想诸如:JavaAssist、CGLib、ASM之类的字节码类库,因为JDK里面的javac命令就是字节码生成技术的老祖宗,它的代码放在OpenJDK的langtools/share/classes/com/sum/tools/javac目录中。

“字节码生成”应用场景:Web服务器重的JSP编译器,编译时植入AOP框架、动态代理技术等。

动态代理技术中所谓的“动态”,是针对使用Java代码实际编写了代理类的“静态”代理而言的,它的优势不在于省去了编写代理类的那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定了代理类的代理行为,当代理类与原始类脱离直接联系后,就可以很灵活地中重用于不同的应用场景之中。

问题32

public class DynamicProxyTest {
    interface IHello {
        void sayHello();
    }

    static class Hello implements IHello {

        @Override
        public void sayHello() {
            System.out.println("HelloWorld");
        }
    }

    static class DynamicProxy implements InvocationHandler {
        Object orignalObject;

        Object bind(Object orignalObject) {
            this.orignalObject = orignalObject;
            return Proxy.newProxyInstance(orignalObject.getClass().getClassLoader(), orignalObject.getClass().getInterfaces(), this);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("Welcome");
            return method.invoke(orignalObject, args);
        }
    }

    public static void main(String[] args) {
        Object helloProxyObject = new DynamicProxy().bind(new Hello());
        if (helloProxyObject instanceof IHello) {
            ((IHello) helloProxyObject).sayHello();
        }
    }
}

如上代码段的结果是:

  • A:Welcome
  • B:HelloWorld
  • C:
Welcome
HelloWorld

参考资料

  1. 深入理解Java虚拟机:JVM高级特性与最佳实践 -- 周志明

  2. 《深入理解Android:Java虚拟机ART》 -- 邓凡平

  3. 《Java并发编程的艺术》+《Java并发编程实战》

  4. 深入理解java虚拟机 -- 战斗民族就是干

  5. 强引用、软引用、弱引用、虚引用 -- CoderBear

  6. Java虚拟机规范

  7. 文中涉及源代码,github传送门