文档库 最新最全的文档下载
当前位置:文档库 › java-explore

java-explore

免费在线版本

(非印刷免费在线版)

了解本书更多信息请登录本书的官方网站

InfoQ 中文站出品

本书由InfoQ 中文站免费发放,如果您从其他渠道获取本书,请注册InfoQ 中文站以支持

作者和出版商,并免费下载更多InfoQ 企业软件开发系列图书。

本迷你书主页为

https://www.wendangku.net/doc/827107949.html,/cn/minibooks/java-explore

Java 深度历险

1

《Java 深度历险》专栏的作者成富,是IBM 中国软件开发中心的高级工程师,也是

我的前同事。他曾经是CTO 毛新生的得意门生,承担过Lotus Mashups 产品的重要

研发职责,现在负责领导Project Vulcan 项目的重要组件在中国团队的开发。成富对

于Java 和Web 开发有着很深的造诣,同时在其他技术领域有着自己独到的见解。

他是我见过的少有的具有极强技术领悟力和实践能力的一部分人之一。

成富还是一个专业的技术写手,看看他博客上的列表就知道,他在一年内会投递多

少优质的稿件。所以顺理成章地,在我参与InfoQ 中文站社区贡献时,很自然邀请

他来开辟一个深入Java 和JVM 的专栏,他欣然应允,重要的是他以专业的技术作者

素质,不再让我担心催稿,最终有了这十篇关于Java 不同方面但深入浅出的主题内

容。在几乎每篇专栏的结尾,都有多于平均数量的积极的评论,在InfoQ 内部月度

内容排行上尤为突出。同样是出于读者的呼声,才有了这本迷你书面世的可能。

很高兴地知道,成富接下来还会和华章有进一步的合作,撰写有关Java 方面的技术

书籍,让我们一起期待吧。

InfoQ 中文站原创团队主编 张凯峰

现在报名8折优惠!

知名网站案例分析—阿里巴巴、淘宝、大众点评等知名网站背后的架构故事

脚本代码之美—专家解析HTML5、JavaScript、Node.JS设计中的难题

开放平台—来自百度、360、腾讯、盛大的案例分享

首席架构师的架构观—首席架构师眼中的简单原则大数据和NoSQL —Hadoop、HBase、MongoDB和Cassandra等技术在当前的企业中的应用DevOps —最前沿的开发&运维之道

运行中的云计算架构—云计算平台面面观,从架构到实践

Java依旧灿烂—Java使用者与平台架构师谈Java为何依旧灿烂

敏捷已到壮年—敏捷与精益开发的现状与未来

大会主题

QCon全球企业开发大会

(杭州站)10月21日-10月23日

8月31日前报名8折优惠!

Java深度历险

2

目录

序 (1)

目录 (2)

JAVA字节代码的操纵 (4)

动态编译J AVA源文件 (4)

J AVA字节代码增强 (6)

https://www.wendangku.net/doc/827107949.html,NG.INSTRUMENT (8)

总结 (9)

参考资料 (10)

JAVA类的加载、链接和初始化 (11)

J AVA类的加载 (11)

J AVA类的链接 (12)

J AVA类的初始化 (13)

创建自己的类加载器 (14)

参考资料 (15)

JAVA线程:基本概念、可见性与同步 (16)

J AVA线程基本概念 (16)

可见性 (17)

J AVA中的锁 (18)

J AVA线程的同步 (19)

中断线程 (20)

参考资料 (20)

JAVA垃圾回收机制与引用类型 (22)

J AVA垃圾回收机制 (22)

J AVA引用类型 (23)

参考资料 (27)

JAVA泛型 (28)

类型擦除 (28)

实例分析 (29)

通配符与上下界 (30)

类型系统 (31)

开发自己的泛型类 (32)

最佳实践 (32)

参考资料 (33)

目录

3

JAVA 注解

..................................................................................................................... 34使用注解

.......................................................................................................................................... 34开发注解

.......................................................................................................................................... 35处理注解

.......................................................................................................................................... 35实例分析

.......................................................................................................................................... 38参考资料

.......................................................................................................................................... 39JAVA 反射与动态代理

.................................................................................................. 40基本用法

.......................................................................................................................................... 40处理泛型

.......................................................................................................................................... 42动态代理

.......................................................................................................................................... 42使用案例

.......................................................................................................................................... 43参考资料

.......................................................................................................................................... 44JAVA I/O

........................................................................................................................ 45流

...................................................................................................................................................... 45缓冲区

.............................................................................................................................................. 47字符与编码

...................................................................................................................................... 48通道

.................................................................................................................................................. 49参考资料

.......................................................................................................................................... 52JAVA 安全

..................................................................................................................... 53认证

.................................................................................................................................................. 53权限控制

.......................................................................................................................................... 55加密、解密与签名

.......................................................................................................................... 57安全套接字连接

.............................................................................................................................. 58参考资料

.......................................................................................................................................... 59JAVA 对象序列化与RMI

................................................................................................ 60基本的对象序列化

.......................................................................................................................... 60自定义对象序列化

.......................................................................................................................... 61序列化时的对象替换

...................................................................................................................... 62序列化与对象创建

.......................................................................................................................... 63版本更新

.......................................................................................................................................... 63序列化安全性

.................................................................................................................................. 64RMI

.................................................................................................................................................... 64参考资料

(66)

Java深度历险

4

1

Java字节代码的操纵

在一般的Java应用开发过程中,开发人员使用Java的方式比较简单。打开惯用的IDE,编写Java源代码,再利用IDE提供的功能直接运行Java 程序就可以了。这种开发模式背后的过程是:开发人员编写的是Java源代码文件(.java),IDE会负责调用Java的编译器把Java源代码编译成平台无关的字节代码(byte code),以类文件的形式保存在磁盘上(.class)。Java虚拟机(JVM)会负责把Java字节代码加载并执行。Java通过这种方式来实现其“编写一次,到处运行(Write once, run anywhere)” 的目标。Java 类文件中包含的字节代码可以被不同平台上的JVM所使用。Java字节代码不仅可以以文件形式存在于磁盘上,也可以通过网络方式来下载,还可以只存在于内存中。JVM 中的类加载器会负责从包含字节代码的字节数组(byte[])中定义出Java类。在某些情况下,可能会需要动态的生成Java字节代码,或是对已有的Java字节代码进行修改。这个时候就需要用到本文中将要介绍的相关技术。首先介绍一下如何动态编译Java源文件。

动态编译Java源文件

在一般情况下,开发人员都是在程序运行之前就编写完成了全部的Java源代码并且成功编译。对有些应用来说,Java源代码的内容在运行时刻才能确定。这个时候就需要动态编译源代码来生成Java字节代码,再由JVM来加载执行。典型的场景是很多算法竞赛的在线评测系统(如PKU JudgeOnline),允许用户上传Java代码,由系统在后台编译、运行并进行判定。在动态编译Java源文件时,使用的做法是直接在程序中调用Java编译器。

JSR 199引入了Java编译器API。如果使用JDK 6的话,可以通过此API来动态编译Java 代码。比如下面的代码用来动态编译最简单的Hello World类。该Java类的代码是保存在一个字符串中的。

public class CompilerTest {

public static void main(String[] args) throws Exception {

String source = "public class Main { public static void main(String[] args) {System.out.println(\"Hello World!\");} }";

第一章Java 字节代码的操纵

5

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

StandardJavaFileManager fileManager =

compiler.getStandardFileManager(null, null, null);

StringSourceJavaObject sourceObject = new

CompilerTest.StringSourceJavaObject("Main", source);

Iterable< extends JavaFileObject> fileObjects =

Arrays.asList(sourceObject);

CompilationTask task = compiler.getTask(null, fileManager, null,

null, null, fileObjects);

boolean result = task.call();

if (result) {

System.out.println("编译成功。");

}

}

static class StringSourceJavaObject extends SimpleJavaFileObject {

private String content = null;

public StringSourceJavaObject(String name, String content) throws

URISyntaxException {

super(URI.create("string:///" + name.replace('.','/') +

Kind.SOURCE.extension), Kind.SOURCE);

this.content = content;

}

public CharSequence getCharContent(boolean

ignoreEncodingErrors) throws IOException {

return content;

}

}

}

如果不能使用JDK 6提供的Java 编译器API 的话,可以使用JDK 中的工具

类com.sun.tools.javac.Main ,不过该工具类只能编译存放在磁盘上的文件,类似于

直接使用javac 命令。

另外一个可用的工具是Eclipse JDT Core 提供的编译器。这是Eclipse Java 开发环境使用

的增量式Java 编译器,支持运行和调试有错误的代码。该编译器也可以单独使用。Play

框架在内部使用了JDT 的编译器来动态编译Java 源代码。在开发模式下,Play 框架会

定期扫描项目中的Java 源代码文件,一旦发现有修改,会自动编译Java 源代码。因此

在修改代码之后,刷新页面就可以看到变化。使用这些动态编译的方式的时候,需

要确保JDK 中的tools.jar 在应用的 CLASSPATH 中。

下面介绍一个例子,是关于如何在Java 里面做四则运算,比如求出来(3+4)*7-10的值。

Java深度历险

6

一般的做法是分析输入的运算表达式,自己来模拟计算过程。考虑到括号的存在和运算符的优先级等问题,这样的计算过程会比较复杂,而且容易出错。另外一种做法是可以用JSR 223引入的脚本语言支持,直接把输入的表达式当做JavaScript或是JavaFX脚本来执行,得到结果。下面的代码使用的做法是动态生成Java源代码并编译,接着加载Java类来执行并获取结果。这种做法完全使用Java来实现。

private static double calculate(String expr) throws CalculationException {

String className = "CalculatorMain";

String methodName = "calculate";

String source = "public class " + className

+ " { public static double " + methodName + "() { return " + expr + "; } }";

//省略动态编译Java源代码的相关代码,参见上一节

boolean result = task.call();

if (result) {

ClassLoader loader = Calculator.class.getClassLoader();

try {

Class clazz = loader.loadClass(className);

Method method = clazz.getMethod(methodName, new Class[] {}); Object value = method.invoke(null, new Object[] {});

return (Double) value;

} catch (Exception e) {

throw new CalculationException("内部错误。");

}

} else {

throw new CalculationException("错误的表达式。");

}

}

上面的代码给出了使用动态生成的Java字节代码的基本模式,即通过类加载器来加载字节代码,创建Java类的对象的实例,再通过Java反射API来调用对象中的方法。

Java字节代码增强

Java 字节代码增强指的是在Java字节代码生成之后,对其进行修改,增强其功能。这种做法相当于对应用程序的二进制文件进行修改。在很多Java框架中都可以见到这种实现方式。Java字节代码增强通常与Java源文件中的注解(annotation)一块使用。注解在Java源代码中声明了需要增强的行为及相关的元数据,由框架在运行时刻完成对字节代码的增强。Java字节代码增强应用的场景比较多,一般都集中在减少冗余代码和对开发人员屏蔽底层的实现细节上。用过JavaBeans的人可能对其中

第一章Java 字节代码的操纵

7

那些必须添加的getter/setter 方法感到很繁琐,并且难以维护。而通过字节代码增强,

开发人员只需要声明Bean 中的属性即可,getter/setter 方法可以通过修改字节代码来

自动添加。用过JPA 的人,在调试程序的时候,会发现实体类中被添加了一些额外

的 域和方法。这些域和方法是在运行时刻由JPA 的实现动态添加的。字节代码增强

在面向方面编程(AOP )的一些实现中也有使用。

在讨论如何进行字节代码增强之前,首先介绍一下表示一个Java 类或接口的字节代

码的组织形式。

类文件 {

0xCAFEBABE ,小版本号,大版本号,常量池大小,常量池数组, 访问控制标记,当前类信息,父类信息,实现的接口个数,实现的接口信息

数组,域个数,域信息数组,方法个数,方法信息数组,属性个数,属

性信息数组

}

如上所示,一个类或接口的字节代码使用的是一种松散的组织结构,其中所包含的

内容依次排列。对于可能包含多个条目的内容,如所实现的接口、域、方法和属性

等,是以数组来表示的。而在数组之前的是该数组中条目的个数。不同的内容类型,

有其不同的内部结构。对于开发人员来说,直接操纵包含字节代码的字节数组的话,

开发效率比较低,而且容易出错。已经有不少的开源库可以对字节代码进行修改或

是从头开始创建新的Java 类的字节代码内容。这些类库包括ASM 、cglib 、serp 和BCEL

等。使用这些类库可以在一定程度上降低增强字节代码的复杂度。比如考虑下面一

个简单的需求,在一个Java 类的所有方法执行之前输出相应的日志。熟悉AOP 的人都

知道,可以用一个前增强(before advice )来解决这个问题。如果使用ASM 的话,相

关的代码如下: ClassReader cr = new ClassReader(is);

ClassNode cn = new ClassNode();

cr.accept(cn, 0);

for (Object object : cn.methods) {

MethodNode mn = (MethodNode) object;

if ("".equals(https://www.wendangku.net/doc/827107949.html,) || "".equals(https://www.wendangku.net/doc/827107949.html,)) {

continue;

}

InsnList insns = mn.instructions;

InsnList il = new InsnList();

il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out",

Java深度历险

8

"Ljava/io/PrintStream;"));

il.add(new LdcInsnNode("Enter method -> " + https://www.wendangku.net/doc/827107949.html,));

il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));

insns.insert(il); mn.maxStack += 3;

}

ClassWriter cw = new ClassWriter(0);

cn.accept(cw);

byte[] b = cw.toByteArray();

从ClassWriter就可以获取到包含增强之后的字节代码的字节数组,可以把字节代码

写回磁盘或是由类加载器直接使用。上述示例中,增强部分的逻辑比较简单,只是

遍历Java类中的所有方法并添加对System.out.println方法的调用。在字节代码中,Java

方法体是由一系列的指令组成的。而要做的是生成调用System.out.println方法的指令,并把这些指令插入到指令集合的最前面。ASM对这些指令做了抽象,不过熟悉

全部的指令比较困难。ASM提供了一个工具类ASMifierClassVisitor,可以打印出Java

类的字节代码的结构信息。当需要增强某个类的时候,可以先在源代码上做出修改,再通过此工具类来比较修改前后的字节代码的差异,从而确定该如何编写增强的代码。

对类文件进行增强的时机是需要在Java源代码编译之后,在JVM执行之前。比较常

见的做法有:

●由IDE在完成编译操作之后执行。如Google App Engine的Eclipse插件会在编译之

后运行DataNucleus来对实体类进行增强。

●在构建过程中完成,比如通过Ant或Maven来执行相关的操作。

●实现自己的Java类加载器。当获取到Java类的字节代码之后,先进行增强处理,

再从修改过的字节代码中定义出Java类。

●通过JDK 5引入的https://www.wendangku.net/doc/827107949.html,ng.instrument包来完成。

https://www.wendangku.net/doc/827107949.html,ng.instrument

由于存在着大量对Java字节代码进行修改的需求,JDK 5引入了https://www.wendangku.net/doc/827107949.html,ng.instrument

包并在JDK 6中得到了进一步的增强。基本的思路是在JVM启动的时候添加一些代

理(agent)。每个代理是一个jar包,其清单(manifest)文件中会指定一个代理类。这个类会包含一个premain方法。JVM在启动的时候会首先执行代理类的premain方法,再执行Java程序本身的main方法。在premain方法中就可以对程序本身的字节

第一章Java 字节代码的操纵

9

代码进行修改。JDK 6中还允许在JVM 启动之后动态添加代理。https://www.wendangku.net/doc/827107949.html,ng.instrument

包支持两种修改的场景,一种是重定义一个Java 类,即完全替换一个 Java 类的字节

代码;另外一种是转换已有的Java 类,相当于前面提到的类字节代码增强。还是以

前面提到的输出方法执行日志的场景为例,首先需要实

现https://www.wendangku.net/doc/827107949.html,ng.instrument.ClassFileTransformer 接口来完成对已有Java 类的转换。 static class MethodEntryTransformer implements ClassFileTransformer {

public byte[] transform(ClassLoader loader, String className,

Class classBeingRedefined, ?ProtectionDomain protectionDomain,

byte[] classfileBuffer)

throws IllegalClassFormatException {

try {

ClassReader cr = new ClassReader(classfileBuffer);

ClassNode cn = new ClassNode();

//省略使用ASM 进行字节代码转换的代码

ClassWriter cw = new ClassWriter(0);

cn.accept(cw);

return cw.toByteArray();

} catch (Exception e){

return null;

}

}

}

有了这个转换类之后,就可以在代理的premain 方法中使用它。

把该代理类打成一个jar 包,并在jar 包的清单文件中通过Premain-Class 声明代理类

的名称。运行Java 程序的时候,添加JVM 启动参数-javaagent:myagent.jar 。这样的

话,JVM 会在加载Java 类的字节代码之前,完成相关的转换操作。

总结

操纵Java 字节代码是一件很有趣的事情。通过它,可以很容易的对二进制分发的Java

程序进行修改,非常适合于性能分析、调试跟踪和日志记录等任务。另外一个非常

重要的作用是把开发人员从繁琐的Java 语法中解放出来。开发人员应该只需要负责

编写与业务逻辑相关的重要代码。对于那些只是因为语法要求而添加的,或是模式

固定的代码,完全可以将其字节代码动态生成出来。字节代码增强和源代码生成是

不同的概念。源代码生成之后,就已经成为了程序的一部分,开发人员需要去维护

Java深度历险

10

它:要么手工修改生成出来的源代码,要么重新生成。而字节代码的增强过程,对于开发人员是完全透明的。妥善使用Java字节代码的操纵技术,可以更好的解决某一类开发问题。

参考资料

●Java字节代码格式

●Java 6.0 Compiler API

●深入探讨Java类加载器

2

Java类的加载、链接和初始化

在上一篇文章中介绍了Java字节代码的操纵,其中提到了利用Java类加载器来加载修改过后的字节代码并在JVM上执行。本文接着上一篇的话题,讨论Java类的加载、链接和初始化。Java字节代码的表现形式是字节数组(byte[]),而Java类在JVM中的表现形式是https://www.wendangku.net/doc/827107949.html,ng.Class类的对象。一个Java类从字节代码到能够在JVM中被使用,需要经过加载、链接和初始化这三个步骤。这三个步骤中,对开发人员直接可见的是Java类的加载,通过使用Java类加载器(class loader)可以在运行时刻动态的加载一个Java类;而链接和初始化则是在使用Java类之前会发生的动作。本文会详细介绍Java类的加载、链接和初始化的过程。

Java类的加载

Java类的加载是由类加载器来完成的。一般来说,类加载器分成两类:启动类加载器(bootstrap)和用户自定义的类加载器(user-defined)。两者的区别在于启动类加载器是由JVM的原生代码实现的,而用户自定义的类加载器都继承自Java中的https://www.wendangku.net/doc/827107949.html,ng.ClassLoader类。在用户自定义类加载器的部分,一般JVM都会提供一些基本实现。应用程序的开发人员也可以根据需要编写自己的类加载器。JVM中最常使用的是系统类加载器(system),它用来启动Java应用程序的加载。通过https://www.wendangku.net/doc/827107949.html,ng.ClassLoader的getSystemClassLoader()方法可以获取到该类加载器对象。

类加载器需要完成的最终功能是定义一个Java类,即把Java字节代码转换成JVM中的https://www.wendangku.net/doc/827107949.html,ng.Class类的对象。但是类加载的过程并不是这么简单。Java类加载器有两个比较重要的特征:层次组织结构和代理模式。层次组织结构指的是每个类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。前者称为初始类加载器,而后者称为定义类加载器。两者的关联在于:一个Java类的定义类加载器是该类所导入的其它Java类的初始类加载器。比如类A通过

11

import导入了类B,那么由类A的定义类加载器负责启动类B的加载过程。

一般的类加载器在尝试自己去加载某个Java类之前,会首先代理给其父类加载器。当父类加载器找不到的时候,才会尝试自己加载。这个逻辑是封装在https://www.wendangku.net/doc/827107949.html,ng.ClassLoader类的loadClass()方法中的。一般来说,父类优先的策略就足够好了。在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到的时候再代理给父类加载器。这种做法在Java的Web容器中比较常见,也是Servlet规范推荐的做法。比如,Apache Tomcat为每个Web应用都提供一个独立的类加载器,使用的就是自己优先加载的策略。IBM WebSphere Application Server则允许Web应用选择类加载器使用的策略。

类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅是根据该类的二进制名称,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类的是相同的。因此,即便是同样的Java 字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出https://www.wendangku.net/doc/827107949.html,ng.ClassCastException。这个特性为同样名称的Java类在JVM中共存创造了条件。在实际的应用中,可能会要求同一名称的Java类的不同版本在JVM中可以同时存在。通过类加载器就可以满足这种需求。这种技术在OSGi中得到了广泛的应用。

Java类的链接

Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。在链接之前,这个类必须被成功加载。类的链接包括验证、准备和解析等几个步骤。验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出https://www.wendangku.net/doc/827107949.html,ng.VerifyError错误。准备过程则是创建Java类中的静态域,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对其它类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。

不同的JVM实现可能选择不同的解析策略。一种做法是在链接的时候,就递归的把所有依赖的形式引用都进行解析。而另外的做法则可能是只在一个形式引用真正需要的时候才进行解析。也就是说如果一个Java类只是被引用了,但是并没有被真正用到,那么这个类有可能就不会被解析。考虑下面的代码:

12

第二章Java 类的加载、链接和初始化

13

public class LinkTest {

public static void main(String[] args) {

ToBeLinked toBeLinked = null;

System.out.println("Test link.");

}

}

类LinkTest 引用了类ToBeLinked ,但是并没有真正使用它,只是声明了一个变量,

并没有创建该类的实例或是访问其中的静态域。在 Oracle 的JDK 6中,如果把编译

好的ToBeLinked 的Java 字节代码删除之后,再运行LinkTest ,程序不会抛出错误。

这是因为ToBeLinked 类没有被真正用到,而Oracle 的JDK 6所采用的链接策略使得

ToBeLinked 类不会被加载,因此也不会发现ToBeLinked 的Java 字节代码实际上是不

存在的。如果把代码改成ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相

同的方法运行,就会抛出异常了。因为这个时候ToBeLinked 这个类被真正使用到了,

会需要加载这个类。

Java 类的初始化

当一个Java 类第一次被真正使用到的时候,JVM 会进行该类的初始化操作。初始化

过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的

直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。

在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静

态域。考虑下面的代码: public class StaticTest {

public static int X = 10;

public static void main(String[] args) {

System.out.println(Y); //输出60

}

static {

X = 30;

}

public static int Y = X * 2;

}

在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到

下依次执行。因此变量X 的值首先初始化成10,后来又被赋值成30;而变量Y 的

值则被初始化成60。

Java 类和接口的初始化只有在特定的时机才会发生,这些时机包括:

创建一个Java 类的实例。如

Java深度历险

14

●调用一个Java类中的静态方法。如

●给Java类或接口中声明的静态域赋值。如

●访问Java类或接口中声明的静态域,并且该域不是常值变量。如

●在顶层Java类中执行assert语句。

通过Java反射API也可能造成类和接口的初始化。需要注意的是,当访问一个Java 类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。考虑下面的代码:

class B {

static int value = 100;

static {

System.out.println("Class B is initialized."); //输出

}

}

class A extends B {

static {

System.out.println("Class A is initialized."); //不会输出

}

}

public class InitTest {

public static void main(String[] args) {

System.out.println(A.value); //输出100

}

}

在上述代码中,类InitTest通过A.value引用了类B中声明的静态域value。由于value 是在类B中声明的,只有类B会被初始化,而类A则不会被初始化。

创建自己的类加载器

在Java应用开发过程中,可能会需要创建应用自己的类加载器。典型的场景包括实现特定的Java字节代码查找方式、对字节代码进行加密/解密以及实现同名Java类的隔离等。创建自己的类加载器并不是一件复杂的事情,只需要继承自https://www.wendangku.net/doc/827107949.html,ng.ClassLoader类并覆写对应的方法即可。https://www.wendangku.net/doc/827107949.html,ng.ClassLoader中提供的方法有不少,下面介绍几个创建类加载器时需要考虑的:

第二章Java 类的加载、链接和初始化

15

● defineClass():这个方法用来完成从Java 字节代码的字节数组到https://www.wendangku.net/doc/827107949.html,ng.Class 的转

换。这个方法是不能被覆写的,一般是用原生代码来实现的。

● findLoadedClass():这个方法用来根据名称查找已经加载过的Java 类。一个类加

载器不会重复加载同一名称的类。

● findClass():这个方法用来根据名称查找并加载Java 类。

● loadClass():这个方法用来根据名称加载Java 类。

● resolveClass():这个方法用来链接一个Java 类。

这里比较 容易混淆的是findClass()方法和loadClass()方法的作用。前面提到过,在

Java 类的链接过程中,会需要对Java 类进行解析,而解析可能会导致当前Java 类所

引用的其它Java 类被加载。在这个时候,JVM 就是通过调用当前类的定义类加载器

的loadClass()方法来加载其它类的。findClass()方法则是应用创建的类加载器的扩展

点。应用自己的类加载器应该覆写findClass()方法来添加自定义的类加载逻辑。

loadClass()方法的默认实现会负责调用findClass()方法。

前面提到,类加载器的代理模式默认使用的是父类优先的策略。这个策略的实现是

封装在loadClass()方法中的。如果希望修改此策略,就需要覆写loadClass()方法。

下面的代码给出了自定义的类加载的常见实现模式: public class MyClassLoader extends ClassLoader {

protected Class findClass(String name) throws

ClassNotFoundException {

byte[] b = null; //查找或生成Java 类的字节代码

return defineClass(name, b, 0, b.length);

}

} 参考资料

● Java 语言规范(第三版)- 第十三章:执行

● JVM 规范(第二版) - 第五章:加载、链接和初始化

● 深入探讨Java 类加载器

Java深度历险

16

3

Java线程:基本概念、可见性与同步

开发高性能并发应用不是一件容易的事情。这类应用的例子包括高性能Web服务器、游戏服务器和搜索引擎爬虫等。这样的应用可能需要同时处理成千上万个请求。对于这样的应用,一般采用多线程或事件驱动的架构。对于Java来说,在语言内部提供了线程的支持。但是Java的多线程应用开发会遇到很多问题。首先是很难编写正确,其次是很难测试是否正确,最后是出现问题时很难调试。一个多线程应用可能运行了好几天都没问题,然后突然就出现了问题,之后却又无法再次重现出来。如果在正确性之外,还需要考虑应用的吞吐量和性能优化的话,就会更加复杂。本文主要介绍Java中的线程的基本概念、可见性和线程同步相关的内容。

Java线程基本概念

在操作系统中两个比较容易混淆的概念是进程(process)和线程(thread)。操作系统中的进程是资源的组织单位。进程有一个包含了程序内容和数据的地址空间,以及其它的资源,包括打开的文件、子进程和信号处理器等。不同进程的地址空间是互相隔离的。而线程表示的是程序的执行流程,是CPU调度的基本单位。线程有自己的程序计数器、寄存器、栈和帧等。引入线程的动机在于操作系统中阻塞式I/O 的存在。当一个线程所执行的I/O被阻塞的时候,同一进程中的其它线程可以使用CPU 来进行计算。这样的话,就提高了应用的执行效率。线程的概念在主流的操作系统和编程语言中都得到了支持。

一部分的Java程序是单线程的。程序的机器指令按照程序中给定的顺序依次执行。Java语言提供了https://www.wendangku.net/doc/827107949.html,ng.Thread类来为线程提供抽象。有两种方式创建一个新的线程:一种是继承https://www.wendangku.net/doc/827107949.html,ng.Thread类并覆写其中的run()方法,另外一种则是在创建https://www.wendangku.net/doc/827107949.html,ng.Thread类的对象的时候,在构造函数中提供一个实现了https://www.wendangku.net/doc/827107949.html,ng.Runnable接口的类的对象。在得到了https://www.wendangku.net/doc/827107949.html,ng.Thread类的对象之后,通过调用其start()方法就可以启动这个线程的执行。

一个线程被创建成功并启动之后,可以处在不同的状态中。这个线程可能正在占用

相关文档