Java类加载机制(一)
译:ayi 第一次翻译,翻译得不是很好,请多多指点
我的邮箱:nonopo12345@https://www.sodocs.net/doc/4d5492140.html,
原文:https://www.sodocs.net/doc/4d5492140.html,/pub/a/onjava/2005/01/26/classloading.html
类加载是java特性的一个重量级的组成部分。尽管,java中“advanced topics”的发展,使java的类加载机制的地位有所下降。但每位编程着都应该知道这部分的工作机制,以及怎样去更好与其配合。这可以使我们节省很多时间,二不必要浪费在调试ClassNotFoundException, ClassCastException, 等。
这篇文章将从最基本的开始,比如代码和数据的关系和区别,以及他们怎么样关系起来形成一个实例或者对象。然后将会说到,java中怎样通过类加载器把代码加载到JVM中,以及java中实现的主要的几种类型的类加载器。然后在这篇文章中我们将会了解到java类加载机制的内幕,我们将使用最基本的代码来描述,这些代码执行与类加载器之后,但在加载一个类之前。在接下来的部分将使用一些例子来强调,对于开发者继承和开发自己的类加载器的必要性。接着将告诉你们怎样编写自己的类加载器,以及怎样使用它们去创建一个一般的能加载包括远程客户端辅助代码的类加载器引擎,以及怎样把它在JVM中定义,实例化,然后执行。习惯上,把J2EE-specific components 中说明的作为java类加载的规范,这篇文章正是从这本手册总结来的。
类和数据
一个类代表一段要执行的代码,然而数据则代表与这些代码相关联的某种状态。状态可以改变,代码不能改变。我们把一种特定状态与一个类关联起来时,就得到了这个类的一个实例。所以同一个类的不同实例有不同的状态,但都参照相同的代码。在java中,一个类通常它的代码就包含在一个 .class 文件中,虽然其中也包括异常。然而,在java运行时,每个类都会构造一个超类对象(first-class object),它们其实是https://www.sodocs.net/doc/4d5492140.html,ng.Class的实例。不论何时编译一个java文件,编译器都会在编译后的字节码中嵌入一个public, static, final型的字段class,这个字段表示的就是一个https://www.sodocs.net/doc/4d5492140.html,ng.Class型的实例。因为它是public类型的,我们可以通过标识符来访问它,像这样:
https://www.sodocs.net/doc/4d5492140.html,ng.Class klass = Myclass.class;
只要一个类被加载到JVM,相同的类(强调:相同的类)将不会被重复加载。这将产生一个问题,什么才是相同的类?一个对象有一种特定状态和标识,对象总是与它所属类联系在一起,与这种状况相似,一个被加载到JVM中类也有特定的标识,接下来我们就阐述:
在java中,一个类通过认证的类全名来唯一标识。认证的类全名是由包名和类名两部分组
成。但是在一个类被加载到JVM中则是通过认证的类全名,还有加载这个类的加载器来唯一标识。因此,一个类的类名为C1,包名为Pg,被类加载器类KClassLoader的一个实例k1加载,则C1,也就是C1.class ,的类实例,在JVM中将被解释为(C1,Pg,k1)。这就意味着两个不同的类加载器实例(Cl, Pg, kl1) 和 (Cl, Pg, kl2) ,加载的类在JVM 中将有不同的类实例对象,不是类型可比型(type-compatible)的。在JVM中有多少个类加载器实例呢?下面,我们将讲解这个。
类加载器
在java中,每个类都会被https://www.sodocs.net/doc/4d5492140.html,ng.ClassLoader的一个实例加载。ClassLoader类处于https://www.sodocs.net/doc/4d5492140.html,ng
包下面,开发者可以自由的创建它的子类,添加自己功能的类加载器。
每当敲入java MyMainClass,一个新的JVM开始时,引导类加载器(bootstrap class loader
)首先会把java中的一些关键类,像https://www.sodocs.net/doc/4d5492140.html,ng.Objent,和运行时的代码载入内存。这些运行时类打包在JRE\lib\rt.jar
文件中。因为是一个本地的接口,我们并不能从java文档中得到引导类加载器(bootstrap class loader )信息。也正是这个原因,引导类加载器(bootstrap class loader )的表现也根据JVM的不同而异。
比如,如果我们试图得到一个核心java运行时类的一个类加载器,我们将得到null值,如下:
log(https://www.sodocs.net/doc/4d5492140.html,ng.String.class.getClassLoader());
下面要说到的是java扩展类加载器。在java.ext.dirs 路径下面,我们可以放java扩展类库,这样我们可以获得超出java核心运行时类的特性。扩展类加载器(ExtClassLoader)将会加载java.ext.dirs目录下的所有 .jar 文件。开发者可以为自己的应用增加新的 .jar 文件或者类库,只要他把它们添加到java.ext.dirs目录下面以至于能被扩展类加载器找到。
在Sun的java指南中,文章“理解扩展类加载”(Understanding Extension Class Loading)对以上三个类加载器路径有更详尽的解释,这是其他几个JDK中的类加载器
https://www.sodocs.net/doc/4d5492140.html,.URLClassLoader
java.security.SecureClassLoader
java.rmi.server.RMIClassLoader
sun.applet.AppletClassLoader
https://www.sodocs.net/doc/4d5492140.html,ng.Thread,包含了public ClassLoader getContextClassLoader()方法,这一方法返回针对一具体线程的上下文环境类加载器。上下文加载器是线程创建着提供的,用以来加载线程运行时需要的类和资源。如果没有设定,默认的是父线程的上下文类加载器。最原始的上下文类加载器由加载application应用程序的类加载器建立。
类加载器怎样工作
所有的类加载器,除了引导类加载器,都一个父类加载器。而且,它们都是https://www.sodocs.net/doc/4d5492140.html,ng.ClassLoader类型的。上面两句话是不同的,而且对与开发者开发的任何一个类加载器的正常工作来说都非常重要。在这里,最重要的是怎样正确的设置父类加载器。类加载器的父类加载器实例会负责加载此类加载器类。(记住:一个类加载器本身也是一个类。)
在一个类加载器外部请求一个类时,使用loadClass() 方法。这个方法的具体工作,我们可以从源代码来看:
protected synchronized Class> loadClass
(String name, boolean resolve)
throws ClassNotFoundException{
// First check if the class is already loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// If still not found, then invoke
// findClass to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
设置父类加载器,我们有两种方法,在ClassLoader的构造方法中。
public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(MyClassLoader.class.getClassLoader());
}
}
or
public class MyClassLoader extends ClassLoader{
public MyClassLoader(){
super(getClass().getClassLoader());
}
}
第一种方法更常被使用因为在构造方法中使用getClass() 方法是不提倡的,因为对象初始化仅在构造方法结束后才会完成。因此,如果正确设置了类加载器的父类加载器,不论什么时候从类加载器实例请求一个类时,如果此类加载器不能加载此类,则首先会交给它的父类加载器处理。如果此父类加载器也不能加载那个类,则又会交给上一层的父类加载器,依此类推。但是如果findBootstrapClass0()方法也未能加载那个类时,就会唤醒findClass()去处理。findClass()的默认的实现是抛出ClassNotFoundException 异常。所以开发者需要继承https://www.sodocs.net/doc/4d5492140.html,ng.ClassLoader来实现用户自编写的类加载器。findClass()默认的实现如下:
protected Class> findClass(String name)
throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
深入findClass()方法,类加载器需要从其它资源获取字节码。这些资源可以是文件系统、网络URL、数据库、其它的可以把字节码转换为流的应用程序,或者能够产生与java特性
相适应的字节码的类似资源。你可以使用BCEL (Byte Code Engineering Library),它能非常方便的根据运行时的迹象创建类。BCEL已经成功的应用在了一些地方,如编译器、优化程序、模糊程序(obsfuscators)、代码生成器、分析工具。只要这个这些字节码被重新获取,findClass()方法将会调用defineClass()方法,而且这时运行时(runtime )环境是非常特殊的对于具体哪一个类加载器实例去调用defineClass()这个方法。所有,两个类加载器实例从两个相同的、或者不同的资源加载字节码时,这些加载的类都是不同的。
在java语言详解(Java language specification)中,给出了在java执行引擎(Java Execution Engine)中加载、链接、初始化的类和接口等过程的详细解释。
图1 展示了一个带有main方法的应用程序类MyMainClass。正如前面所说的,MyMainClass.class 将被AppClassLoader加载,MyMainClass 创建两个加载器类实例,CustomClassLoader1和 CustomClassLoader2,他们都能从某些资源(比如说:网络)中加载第四个类Target 的字节码。这就意味着Target 这个类的定义超出了应用程序的class path 或者扩展class path 范围。在这种情况下,如果MyMainClass 让客户加载器实例去加载Target 类。Target 将同时被CustomClassLoader1和CustomClassLoader2加载和定义。在java中这样就会有严重的问题。如果在Target 中包含一段静态(static)的初始化代码,如果我们要求这段代码执行且仅贝被执行一次,在我们目前的情况下,这段代码将被执行两次。在两个CustomClassLoader中,都执行了一次。如果Target 被两个CustomClassLoader同时初始化,他将会有两个实例target1 和target2 ,正如下面图1所示的,target1 和target2 是不可比的。也就是说,在java中不能执行这段代码:
Target target3 = (Target) target2;
上面的代码将会抛出ClassCastException异常。这是因为JVM把他们看做两个独立的不同的类类型,因为它们被不同类型的ClassLoader 实例加载。如果MyMainClass 使用的不是不同类型的ClassLoader(这里:CustomClassLoader1 和CustomClassLoader2)实例,使用同一类型的ClassLoader 的两个实例来加载,情况也将是一样的。这将在后面通过代码来证实。
图1 在同一个JVM中多个ClassLoader加载同一个类Target
关于类加载、定义、链接的过程更加详细的解释在Andreas Schaefer的文章Inside Class Loaders中。
为什么我们要使用我们自己的类加载器?
开发者编写自己的类加载器的一个理由是控制JVM的行为。在java中一个类被区分是通过包名加类名。因为这些类实现了java.io.Serializable接口,serialVersionUID 将在类的版本化中担当一个重要角色。这种流唯一标志是一个由类名、接口名、方法以及字段生成的一个64位的哈希码。除了这些,没有其它的直接的结构来版本化一个类。单从技术上来说,如果上面所说的匹配的话,这些类就是相同的版本。
想一下这种情况,当我们想要去开发一个一般性的执行引擎,它能够执行任何实现了某个特定接口的任务。当这些任务被提交到引擎,引擎首先需要去加载这些任务的代码。假如很多
不同的客户提交不同的任务(就是:不同的代码)给我们的引擎,碰巧,这些任务都有相同的类名和包名。问题产生了,引擎将要为不同的客户调用上下文加载不同的客户版本,使客户能得到它们想要的正确结果吗?这将在下面,通过加载相同的客户代码来证实。在samepath 和 differentversions这两个目录中,均包括独立的代码来证实这个原理。
图2 展示了示例在samepath, differentversions, 和 differentversionspush 这三个子目录中。
图2 示例的目录结构
在samepath目录,我们有均version.Version 类分别在它的子目录v1和v2下面。这两个类均有相同的类名和包名。唯一的不同是:
public void fx(){
log("this = " + this + "; Version.fx(1).");
}
在v1中,我们有Version.fx(1)在log 语句中;而在v2中,是Version.fx(2)。在相同的路径下,这两个不同版本的类仅有这点区别。现在执行Test 如下:
set CLASSPATH=.;%CURRENT_ROOT%\v1;%CURRENT_ROOT%\v2
%JAVA_HOME%\bin\java Test
在图3中,我们将看到控制台的输出,Version.fx(1) 被加载了,因为ClassLoader在classpath中首先找到了v1中的类。
图3 在版本1的test类的路径放在前面
我们稍微改变一下classpath中个路径的先后顺序,再次运行一次:
set CLASSPATH=.;%CURRENT_ROOT%\v2;%CURRENT_ROOT%\v1
%JAVA_HOME%\bin\java Test
控制台的输出改变了,如图4。现在,Version.fx(2) 被执行了,因为class loader 先找到了这个版本的类。
图4 在版本2的test类的路径放在前面
从上面的可以看出,class loader 会加载在classpath中先找到的类。如果我们不这样,从v1、v2中删除version.Version ,而把它们打包在一个myextension.jar 的.jar文件中,并把它们放到java.ext.dirs目录下面,重新测试,我们将看到,它将不再被AppClassLoader加载,而是被扩展类加载器加载。如图5所示:
图5 AppClassLoader 和 ExtClassLoader 类加载器
进一步来研究这个例子,目录differentversions 下面有一个RMI 执行引擎。客户能够提交任何实现了common.TaskIntf 接口的任务给这个引擎。两个子目录client1 和client2 均包含了稍有区别的这个类client.TaskImpl。它们的区别如下代码所示:
static{
log("client.TaskImpl.class.getClassLoader
(v1) : " + TaskImpl.class.getClassLoader());
}
public void execute(){
log("this = " + this + "; execute(1)");
}
在client1的log语句中执行的是getClassLoader(v1) 是 execute(1),在client2的log语句中执行的是getClassLoader(v2) 是 execute(2)。而且,在启动RMI服务器引擎的脚本中,我们不妨把client2路径放在前面:
CLASSPATH=%CURRENT_ROOT%\common;%CURRENT_ROOT%\server;
%CURRENT_ROOT%\client2;%CURRENT_ROOT%\client1
%JAVA_HOME%\bin\java server.Server
在图6、7、8的屏幕截屏中展示了这时的情况。这时,在两个客户虚拟机中,单独的client.TaskImpl类,被加载、初始化,并传递到了服务器端虚拟机的执行引擎中。从服务器端的控制台可以看出,client.TaskImpl仅被加载了一次。这个单独“版本”的代码在服务器端
被用来产生许多client.TaskImpl 实例,来执行任务。
图6 服务器端控制台输出
图6展示了服务器端引擎控制台的输出,它加载、执行两个各自的客户请求(如图7、8所示)。要指出的是,在这里代码仅被执行了一次(很明显,从静态初始化语句块中的log语句来看),但是这个方法被执行了两次,每个客户的一次调用。
图7 客户1的控制台
在图7中,服务器端提供的类TaskImpl 包括语句client.TaskImpl.class.getClassLoader(v1) 被加载入了客户虚拟机。在图8中,客户虚拟机加载了不同的,包含client.TaskImpl.class.getClassLoader(v2)的,TaskImpl 类代码。
图8 客户2的控制台
这里,各自的client.TaskImpl 类被加载、初始化,并且交给服务器端虚拟机来执行。再次看一下图6所展示的服务器端的控制台,显示了client.TaskImpl 加载且仅被加载了一次。这个单独的代码将被用来在服务器端生成client.TaskImpl 实例。Client1将不高兴了,因为它的语句client.TaskImpl(v1)并没有被执行,而是其它的代码在执行当它在客户端调用时。我们怎样处理这种情况呢?答案是实现客户的类加载器。
客户类加载器
较好的控制类加载的解决办法是实现自己的客户类加载器。任何客户类加载器都必须直接或间接继承https://www.sodocs.net/doc/4d5492140.html,ng.ClassLoader。而且在构造函数中,我们也必须把父类设置为类加载器。然后,我们要重写findClass()方法。在differentversionspush目录中包括了一个名为FileSystemClassLoader的类加载器。目录结果如图9所示:
图9 客户类加载器关系
下面是在common.FileSystemClassLoader实现的主要方法:public byte[] findClassBytes(String className){
try{
String pathName = currentRoot +
File.separatorChar + className.
replace('.', File.separatorChar) + ".class";
FileInputStream inFile = new
FileInputStream(pathName);
byte[] classBytes = new
byte[inFile.available()];
inFile.read(classBytes);
return classBytes;
}
catch (java.io.IOException ioEx){
return null;
}
}
public Class findClass(String name)throws
ClassNotFoundException{
byte[] classBytes = findClassBytes(name); if (classBytes==null){
throw new ClassNotFoundException();
}
else{
return defineClass(name, classBytes, 0, classBytes.length);
}
}
public Class findClass(String name, byte[]
classBytes)throws ClassNotFoundException{
if (classBytes==null){
throw new ClassNotFoundException(
"(classBytes==null)");
}
else{
return defineClass(name, classBytes,
0, classBytes.length);
}
}
public void execute(String codeName,
byte[] code){
Class klass = null;
try{
klass = findClass(codeName, code);
TaskIntf task = (TaskIntf)
klass.newInstance();
task.execute();
}
catch(Exception exception){
exception.printStackTrace();
}
}
这个类被客户用来转换client.TaskImpl(v1)为byte[]。这个byte[]将被传递给服务器端执行引擎。在服务器端,相同的类将被用来从字节数组逆转定义这个类。客户端代码如下:
public class Client{
public static void main (String[] args){
try{
byte[] code = getClassDefinition
("client.TaskImpl");
serverIntf.execute("client.TaskImpl",
code);
}
catch(RemoteException remoteException){
remoteException.printStackTrace();
}
}
private static byte[] getClassDefinition
(String codeName){
String userDir = System.getProperties().
getProperty("BytePath");
FileSystemClassLoader fscl1 = null;
try{
fscl1 = new FileSystemClassLoader
(userDir);
}
catch(FileNotFoundException
fileNotFoundException){
fileNotFoundException.printStackTrace();
}
return fscl1.findClassBytes(codeName);
}
}
从服务器端来看,从客户端接受的代码将交给客户类加载器。客户类加载器首先从接收到的字节数组中逆定义类,实例化,然后执行。这里值得指出的是,对于每个客户请求,都将使用各自的FileSystemClassLoader实例来加载提供的client.TaskImpl。而且,client.TaskImpl类并不在服务器端的classpath范围内。这就意味着,当我们在FileSystemClassLoader中调用findClass() 时,findClass()会从内部调用defineClass(), client.TaskImpl将被各自的类加载器实例加载。当有一个新的FileSystemClassLoader 实例来加载时,照样从逆定义类开始重新做一遍。所以,对每个客户调用,类client.TaskImpl 都被重新定义了,使我们能够在同一个虚拟机中执行“不同版本”的client.TaskImpl 代码。
public void execute(String codeName, byte[] code)throws RemoteException{
FileSystemClassLoader fileSystemClassLoader = null;
try{
fileSystemClassLoader = new FileSystemClassLoader();
fileSystemClassLoader.execute(codeName, code);
}
catch(Exception exception){
throw new RemoteException(exception.getMessage());
}
}
目录differentversionspush下的examples。服务器和客户端控制台输出,如图10 、11、12中所示:
图10 服务器端的Custom class loader 的执行结果
图10 展示了客户类加载器的虚拟机控制台。我们可以看到client.TaskImpl 被执行了不只一次。事实上,对每个客户执行上下文环境,这个类被新加载和初始化一次。
图11 客户类加载器引擎Client 1
在图11中,包含了语句client.TaskImpl.class.getClassLoader(v1) 的TaskImpl类的这些代码在客户端被加载,然后被放到服务器端执行。图12,包含了语句client.TaskImpl.class.getClassLoader(v2) 的TaskImpl的不同类在客户端2的加载情况,并在服务器端执行。
图12 客户类加载引擎,client2
这例子向我们展示了,当在同一个JVM中有“不同版本”的代码时,我们怎样使用各自的类加载器实例来实现一一对应的边对边执行。
类加载器在J2EE中
在j2ee中,类加载器倾向于在不同的时间段移除和重新加载类。这种情况在某些实现中存在,在某些中没有。web服务器可能会移除以前的一个已经加载的servlet实例,可能因为是管理员明确地要这么做,也可能是这个servlet已经空闲的很长一段时间。当第一次请求一个jsp(假设这个jsp还没有被初次编译),JSP引擎将转译这个jsp为一个页面实现类,切实一个标准的servlet类。只要这个页面实现类servlet一创建,它将被JSP引擎编译为一个class文件、备用。每当客户请求这个jsp时,编译器将首先会检查这个jsp是否被修改了。如果是的话,JSP引擎将把它重新转译,以确保给客户端的响应是最新的jsp页面实现所生成的。以.ear, .war, .rar形式的企业应用程序部署单元,也需要在需要的时候或配置策略改变时,被加载或重新加载。对所有的情况来说,都有可能被加载、移除以及重新加载,除非我们已经控制了应用程序服务器的类加载策略。这通过扩展类加载器可以做到,因为它能执行在它范围内的代码。
Brett Peterson已经在发表在 https://www.sodocs.net/doc/4d5492140.html,的"Understanding J2EE Application Server Class Loading Architectures"上给出了J2EE application server 的类加载模式的解释。
总结
这篇文章讲述了怎样把类加载到JVM并唯一标识,和具有相同类名和包名时的一些限制。
因为没有直接的类版本结构,如果我们想按我们自己得到想法来加载类时,我们不得不使用客户类加载器来扩展实现。许多J2EE应用程序服务器有“热部署”的能力,能够使我们以一个新的类定义来重新加载应用程序,而不需要关闭服务器。这些应用程序服务器利用了客户类加载器。尽管我们不使用应用程序服务器,我们可以创建和使用客户类加载器来更好的控制java应用程序的类加载。 Ted Neward的书Server-Based Java Programming
非常好的描述了java类加载的详细细节,并告诉了我们一些在 J2EE APIs中没有提到的东西以及怎样更好的去使用它们。
参考
Sample code for this article
JDK 1.5 API Docs
The Java language specification
"Understanding Extension Class Loading " in the Java tutorial "Inside Class Loaders" from ONJava
"Inside Class Loaders: Debugging" from ONJava
"What version is your Java code?" from JavaWorld
"Understanding J2EE Application Server Class Loading Architectures"
from TheServerSide
Byte Code Engineering Library
Server-Based Java Programming by Ted Neward
作者:Binildas Christudas
北大青鸟推荐:Java精选笔试题(含答案解析)如果你是计算机专业出生,但是还没有找到工作的话,你就得补补技术了,一些关于面试、笔试的题要多刷一刷。有可能你知道答案,但是由于语言组织能力有所欠缺,所以面试官的印象不是很好,下面分享一些Java精选的鄙视题,希望对面试这者有帮助。 1,volatile关键字是否能保证线程安全?() 答案:否 volatile关键字用在多线程同步中,可保证读取的可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的读取值,而非cache中。但多个线程对volatile的写操作,无法保证线程安全。 假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值,在线程1对count进行修改之后,会write到主内存中,主内存中的count变量就会变为6;线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6;导致两个线程及时volatile关键字修改之后,还是会存在并发的情况。 2,下面哪个流类属于面向字符的输入流( ) A、BufferedWriter B、FileInputStream C、ObjectInputStream D、InputStreamReader 答案:D Java的IO操作中有面向字节(Byte)和面向字符(Character)两种方式。
面向字节的操作为以8位为单位对二进制的数据进行操作,对数据不进行转换,这些类都是InputStream和OutputStream的子类。 面向字符的操作为以字符为单位对数据进行操作,在读的时候将二进制数据转为字符,在写的时候将字符转为二进制数据,这些类都是Reader和Writer的子类。 3,Java能不能不通过构造函数创建对象() A、能 B、不能 答案:A Java创建对象的几种方式: (1) 用new语句创建对象,这是最常见的创建对象的方法。 (2) 运用反射手段,调用https://www.sodocs.net/doc/4d5492140.html,ng.Class或者https://www.sodocs.net/doc/4d5492140.html,ng.reflect.Constructor类的newInstance()实例方法。 (3) 调用对象的clone()方法。 (4) 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。 (1)和(2)都会明确的显式的调用构造函数;(3)是在内存上对已有对象的影印,所以不会调用构造函数;(4)是从文件中还原类的对象,也不会调用构造函数。 4,下列哪个叙述是正确的() A.子类继承父类的构造方法。 B.abstract类的子类必须是非abstract类。 C.子类继承的方法只能操作子类继承和隐藏的成员变量。 D.子类重写或新增的方法也能直接操作被子类隐藏的成员变量。 答案:C 子类是不继承父类的构造方法的,而是必须调用其父类的构造方法。
永隆 JAVA笔试题 一、选择题 1、关于Java 类的加载过程,下面哪些描述是正确的() A、在 Java 中,有四种类型的类加载器:BootStrapClassLoader、ExtClassLoader、AppClassLoader 以及用户自定义的ClassLoader。//Extension ClassLoader, System ClassLoader+用户自定义的classloader B、使用 new 关键字创建类实例时,其实就显示地包含了类的加载过程 C、在 Java 中,类的实例化流程分为两个部分:类的加载和类的实例化。类的加载又分为显式加载和隐式加载。 D、Class.forName 来加载类时,是通过 ExtClassLoader进行加载的。 //system classLoader 加载 2、关于HashMap的实现机制,下面哪些描述是正确的() A、HashMap中key-value 当成一个整体进行处理,系统总是根据数组的坐标来获得key-value 的存储位置。//没有存储顺序,无下标之说! B、HashMap基于哈希表的 Map 接口的实现,允许使用 null 值和 null 键。 C、如果HashMap中,如果Key的hash相同的话,HashMap将会出错。//会替换相应的value D、HashMap每次容量的扩增都是以2的倍数来增加。//大约获得2倍的桶数! 3、下面的代码执行输出正确的是() 1. public class test( 2. public int aMethod()[ 3. static int i=0; 4. i++; 5. return I; 6. ) 7. public static void main (String args[]){ 8. test test = new test(); 9. test.aMethod(); 10.int j = test.aMethod(); 11.System.out.printIn(j); 12.] 13.} A. 编译错误 B. 编译成功,打印出是“0” C. 编译成功,打印出是“1” D. 编译成功,打印出是“2” A 4、如何获取下面表单 select