Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称作虚拟机的类加载机制。与那些需要在编译时进行链接的语言不同,java的加载,连接,初始化都是在程序运行时完成的
为了支持java的动态绑定特性,解析操作并不需要严格按照图中位置执行,它可以在初始化之后再开始。
加载是整个类加载过程的第一个阶段,在加载阶段,java虚拟机需要完成以下三件事:
通过一个类的全限定名获得定义此类的二进制字节流
这一步是通过类加载器的loadClass方法实现的。
/**
* Loads the class with the specified binary name.
* This method searches for classes in the same manner as the {@link
* #loadClass(String, boolean)} method. It is invoked by the Java virtual
* machine to resolve class references. Invoking this method is equivalent
* to invoking {@link #loadClass(String, boolean) loadClass(name,
* false)}.
*
* @param name
* The binary name of the class
*
* @return The resulting Class object
*
* @throws ClassNotFoundException
* If the class was not found
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
name就是类的全限定名,而resolve则表示是否要在加载阶段执行连接(即验证、准备、解析)操作。
首先,Class> c = findLoadedClass(name);
查找该类是否已被加载过,如果加载过,直接返回,否则,
会触发双亲委派模型,即先交给父类加载器进行加载,当父类加载器无法定位到这个类,再由自己进行加载。自定义的类加载器通过重写findClass(name);
来实现自己的加载逻辑。我们需要找到一个子类加载器的findClass方法来一窥具体的类加载过程,下面是AppletClassLoader的findClass实现
protected Class findClass(String name) throws ClassNotFoundException {
int index = name.indexOf(";");
String cookie = "";
if(index != -1) {
cookie = name.substring(index, name.length());
name = name.substring(0, index);
}
// check loaded JAR files
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
}
// Otherwise, try loading the class from the code base URL
// 4668479: Option to turn off codebase lookup in AppletClassLoader
// during resource requests. [stanley.ho]
if (codebaseLookup == false)
throw new ClassNotFoundException(name);
// final String path = name.replace('.', '/').concat(".class").concat(cookie);
String encodedName = ParseUtil.encodePath(name.replace('.', '/'), false);
final String path = (new StringBuffer(encodedName)).append(".class").append(cookie).toString();
try {
byte[] b = (byte[]) AccessController.doPrivileged(
new PrivilegedExceptionAction() {
public Object run() throws IOException {
try {
URL finalURL = new URL(base, path);
// Make sure the codebase won't be modified
if (base.getProtocol().equals(finalURL.getProtocol()) &&
base.getHost().equals(finalURL.getHost()) &&
base.getPort() == finalURL.getPort()) {
return getBytes(finalURL);
}
else {
return null;
}
} catch (Exception e) {
return null;
}
}
}, acc);
if (b != null) {
return defineClass(name, b, 0, b.length, codesource);
} else {
throw new ClassNotFoundException(name);
}
} catch (PrivilegedActionException e) {
throw new ClassNotFoundException(name, e.getException());
}
}
可以看到,它的确是用自己的方式找到了目标类的二进制字节流,这完成了第一个任务。随后,它将字节流传入了defineClass方法
/**
converts an array of bytes into an instance of class Class, with an optional CodeSource. Before the class can be used it must be resolved.
If a non-null CodeSource is supplied a ProtectionDomain is constructed and associated with the class being defined.
Params:
name – the expected name of the class, or null if not known, using '.' and not '/' as the separator and without a trailing ".class" suffix.
b – the bytes that make up the class data. The bytes in positions off through off+len-1 should have the format of a valid class file as defined by The Java™ Virtual Machine Specification.
off – the start offset in b of the class data
len – the length of the class data
cs – the associated CodeSource, or null if none
Returns:
the Class object created from the data, and optional CodeSource.
Throws:
ClassFormatError – if the data did not contain a valid class
IndexOutOfBoundsException – if either off or len is negative, or if off+len is greater than b.length.
SecurityException – if an attempt is made to add this class to a package that contains classes that were signed by a different set of certificates than this class, or if the class name begins with "java.".
**/
protected final Class<?> defineClass(String name,
byte[] b, int off, int len,
CodeSource cs)
{
return defineClass(name, b, off, len, getProtectionDomain(cs));
}
注释解释到这个方法的作用就是将字节流转化成一个Class的实例对象,即我们加载阶段的2、3部分。具体的转变过程是JNI调用native 方法执行
private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);
看来逻辑在HotSpot虚拟机源码中,也就是C++实现的了。但通过阅读Java部分的源码,我们也了解了类加载器的作用和工作流程,包括双亲委派机制。
验证是为了确保加载阶段引入的字节流符合虚拟机的规范要求,如果不检查输入的字节流,很可能会因为载入了有错误或者恶意企图的字节流而导致整个系统崩溃
准备阶段为类中定义的变量(被static修饰的变量)分配内存并设置变量初始值。
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用以一组符号来描述所引用的目标,符号可以是任意形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定是已经加载到虚拟机内存中的内容,各种虚拟机实现的内存布局可能不同,但它们能接受的符号引用必须是一致的。而直接引用是直接指向目标的指针,相对偏移量,或者是一个句柄。直接引用与内存布局直接相关。如果有了直接引用,那么引用的目标在虚拟机内存中一定存在。解析动作主要针对类或接口,字段,类方法,接口方法,方法类型,方法句柄和调用点限定符
初始化阶段是类加载的最后阶段,在前面的几个阶段中,除了加载阶段可以由用户自定义的类加载器进行外,其他都是在虚拟机内部进行的,直到这一步才将控制权转交给用户。初始化阶段就是类构造器