上一期讲了高并发服务中的GC调优,对G1、CMS的作了一下简单的优缺点分析,和业务中的Full GC、Young GC、Mixd GC进行了原理分析和优化建议。
这次和各位小伙伴聊一下虚拟机中类加载这块的东西,以及实际业务中使用的场景。
一、Class文件的结构
package org.apache.dubbo.vm;
public class Halo {
public static void main(String[] args) {
System.out.println("\"hello\" = " + "hello");
}
}
将Halo这个Java文件编译成.class
文件,然后用十六进制的编辑打开:
Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: CA FE BA BE 00 00 00 34 00 1D 0A 00 06 00 0F 09 J~:>...4........
00000010: 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07 ................
00000020: 00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 .....<init>...()
00000030: 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E V...Code...LineN
00000040: 75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69 umberTable...mai
00000050: 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 n...([Ljava/lang
00000060: 2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 /String;)V...Sou
00000070: 72 63 65 46 69 6C 65 01 00 09 48 61 6C 6F 2E 6A rceFile...Halo.j
00000080: 61 76 61 0C 00 07 00 08 07 00 17 0C 00 18 00 19 ava.............
00000090: 01 00 0F 22 68 65 6C 6C 6F 22 20 3D 20 68 65 6C ..."hello".=.hel
000000a0: 6C 6F 07 00 1A 0C 00 1B 00 1C 01 00 18 6F 72 67 lo...........org
000000b0: 2F 61 70 61 63 68 65 2F 64 75 62 62 6F 2F 76 6D /apache/dubbo/vm
000000c0: 2F 48 61 6C 6F 01 00 10 6A 61 76 61 2F 6C 61 6E /Halo...java/lan
000000d0: 67 2F 4F 62 6A 65 63 74 01 00 10 6A 61 76 61 2F g/Object...java/
000000e0: 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 lang/System...ou
000000f0: 74 01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 t...Ljava/io/Pri
00000100: 6E 74 53 74 72 65 61 6D 3B 01 00 13 6A 61 76 61 ntStream;...java
00000110: 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 01 /io/PrintStream.
00000120: 00 07 70 72 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 ..println...(Lja
00000130: 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 va/lang/String;)
00000140: 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 00 V.!.............
00000150: 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 ................
00000160: 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 ...*7..1........
00000170: 00 06 00 01 00 00 00 03 00 09 00 0B 00 0C 00 01 ................
00000180: 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 .....%........2.
00000190: 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00 00 ...6..1.........
000001a0: 0A 00 02 00 00 00 05 00 08 00 06 00 01 00 0D 00 ................
000001b0: 00 00 02 00 0E
由于是16进制的格式,所以00
、01
中的每列中的一个元素表示一个字节。.class
文件存储的基本单位就是一个8位字节,如果需要占用一个字节以上,就按照高位在前Big-endian
进行存储。
Big-endian
:
int
类型的数据0x01234567
。假设在内存中的起始地址为0x200
,则内存中的存储地址就是下面这样的:
0x200 | 0x201 | 0x202 | 0x203 |
---|---|---|---|
01 | 23 | 45 | 67 |
.class
文件中主要以两种数据类型组成:
- 无符号数,class文件中的最基础的数据类型,比如
u1
,u2
,u4
,u8
。u后面的数字表示这个无符号数为几个字节。 - 表,表是由多个无符号数或者其他表组成的数据类型。比如
cp_info
,method_info
,习惯以_info
结尾来表示。
.class
文件中依次存储的数据结构如下:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count - 1 |
u2 | access_flags | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
比如看下魔数,是一个无符号数
u4
,按照高位在前的存储顺序,可以看到实际的值为0xCAFFBABE
,刚好是我的公众号名字,哈哈。
后面的类加载机制中验证阶段
的文件格式验证
的阶段,主要就是校验当前的.class
文件是否以0xCAFFBABE
开头,minor_version
、major_version
是否在当前虚拟机兼容范围内,constant_pool
中的常量是否有不支持的数据类型等等。
比如对于constant_pool_count
,值为0x001D
,转化为十进制之后为29,表示的的是constant_pool
中的常量个数。反编译上面的class文件看到:
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // \"hello\" = hello
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // org/apache/dubbo/vm/Halo
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Halo.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 \"hello\" = hello
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 org/apache/dubbo/vm/Halo
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
反编译看到的是28,这是为什么?因为constant_pool_count
的第一位存储的是另外的信息,所以实际constant_pool
中常量个数为28,是一致的。
这块的内容在面试中基本上不会问的很深,知道是大概的存储结构,以及其中的某几个数据类型就可以了。这里先管中窥豹一下,哈哈。
二、类加载
正在准备面试中的大伙对这块肯定很熟悉了,这里我也比较概括性的写一下。后面的实战环节中细看会比较深刻一些。
- 类的生命周期
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载,这里着重说一下加载
这个阶段,因为这块在实际中的工作场景中使用的比较多。
- 比如运行时动态生成,无论是从文件中生成,比如
SPI
技术,还是从内存中使用字符串动态编译成.class二进制字节流
,动态代理
等都需要对类加载有一定的了解,这块也是我们日常写代码可控性最强的一个阶段。
加载
过程
- 获取class的二进制字节流
- 将class的存储结构转化为方法区的数据结构
- 内存中生成Class对象,作为方法区对于这个类的访问入口
加载过程是通过类加载器完成的,此外使用equals()
,isAssignableFrom()
,isInstance()
方法时,类加载器也起着很大的作用。
虚拟机中比较两个类是否相等,需要判断类的加载器是否相等,以及类本身是否相等。类加载器可以分为以下3种:
- 启动类加载器,主要加载
/lib
目录下的,比如rt.jar
等 - 扩展类加载器,主要加载
/lib/ext
中的类 - 应用程序加载器,负责加载用户的
ClassPath
中的类,应用中默认的加载器就是它。 - 自定义加载器
这三种加载器,上面的加载器为下面的父类,只有启动类加载器没有父类。
双亲委派
比如类加载器收到了加载类的请求,他会把这个过程委派给父类加载器去完成,每一种加载器都是如此,所以最终的所有加载请求都会传到顶层的启动类加载器。
只有当父类加载器说我没有办法加载,子加载器才会尝试自己去加载。
这样设计的好处就是避免了Java类型的混乱,如果用户没有自己定义加载器,那么最终的类加载都是由启动类加载器,这样就保证了类型唯一。
比如看ClassLoader
的loadClass
方法
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;
}
}
ClassLoader
中有一个属性parent
,表示的是父类加载器,先会调用父类的loadClass()
方法,因此保证了双亲委派
机制。
看到如果父类加载器无法自己去加载,则会调用自己的findClass()
方法,因此,在自定义类加载逻辑的时候应该写在这个方法里。
我们自己在定义类加载器的时候,也不要覆盖loadClass
这个方法,不要随意的破坏这个机制,避免引起混乱。
双亲委派
可以被破坏吗?
比如三方库的用的虚拟机SPI
机制,去实现不同的接口,比如MyBatis
等。
SPI
机制需要将实现的接口代码放在应用程序的ClassPath中,而JDK的SPI接口都是放在<JAVA_HOME>/lib中的,由启动类加载器加载。
由于启动类加载器不能加载程序类路径中的类,因此诞生了线程上下文加载器。
如果创建线程时没有调用Thread.setContextClassLoader
方法自己定义,会从父线程中继承一个,如果没有全局都没有设置的话,这个类加载器就是默认的应用程序加载器。
因此,实际中的SPI代码是通过父类去委托子类进行加载的,因此某种意义上破坏了双亲委派模型。
还有比如就是我们常用的自定义类加载的逻辑,比如dubbo
框架中的SPI
机制,实现了javassist
和覆盖JDKClassLoader
去完成Adaptive
类的加载。
这种都是打破了双亲委派模型,由子类去定义类加载逻辑。CAFFE
上一周也在看dubbo
相关的源码,就发现了dubbo
中的Adaptive
的实现是如此的。
CAFFE
后面有时间会自己仿照dubbo
写一个rpc
框架出来,到时候在详细讲解。
类加载实战环节
这次主要从实际的业务场景出发,比如一些规则引擎项目需要去实现一些模板的解析,根据业务传进来的字符串,进行相应的模板解析等。
这次就简单讲解下如何将一个字符串编译成Class,加载进虚拟机的内存中。CAFFE
在研究如何写这个样例的时候,是用idea单步调试法,找到需要覆盖的核心方法,在写的,因此实际中也可以照着使用。
首先定义一个加载器
public class ClassLoaderImpl extends ClassLoader {
/**
* 缓存javaFileObject,实际中用来对同一个动态类进行更新,避免重复加载
*/
private Map<String, JavaFileObject> classes = new HashMap<>(16);
public ClassLoaderImpl(final ClassLoader parentClassLoader) {
super(parentClassLoader);
}
public Collection<JavaFileObject> files() {
return Collections.unmodifiableCollection(classes.values());
}
// 应该把自己的类加载逻辑写在findClass中
@Override
protected Class<?> findClass(final String qualifiedClassName) throws ClassNotFoundException {
JavaFileObject javaFileObject = classes.get(qualifiedClassName);
if (javaFileObject != null) {
byte[] bytes = ((JavaFileObjectImpl) javaFileObject).getByteCode();
return defineClass(qualifiedClassName, bytes, 0, bytes.length);
}
throw new ClassNotFoundException("qualifiedClassName not found");
}
public void add(String qualifiedClassName, JavaFileObject javaFileObject) {
classes.put(qualifiedClassName, javaFileObject);
}
// 不提倡覆盖loadClass方法,如果双亲都加载失败,则会调用自己的findClass方法来完成加载
@Override
protected synchronized Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
return super.loadClass(name, resolve);
}
@Override
public InputStream getResourceAsStream(final String name) {
if (name.endsWith(".class")) {
String qualifiedClassName = name.substring(0, name.length() - ".class".length()).replace("/", ".");
JavaFileObjectImpl file = (JavaFileObjectImpl) classes.get(qualifiedClassName);
if (file != null) {
return new ByteArrayInputStream(file.getByteCode());
}
}
return super.getResourceAsStream(name);
}
}
然后实现一个JavaFileObject
子类
public class JavaFileObjectImpl extends SimpleJavaFileObject {
// code
private final CharSequence source;
// byte code
private ByteArrayOutputStream bytecode;
protected JavaFileObjectImpl(URI uri, Kind kind) {
super(uri, kind);
this.source = null;
}
/**
*
* @param className simpleName
* @param source code
*/
public JavaFileObjectImpl(String className, final CharSequence source) {
super(URI.create(className + Kind.SOURCE.extension), Kind.SOURCE);
this.source = source;
}
@Override
public CharSequence getCharContent(final boolean ignoreEncodingErrors) throws UnsupportedOperationException {
if (source == null || source.toString().trim().length() == 0) {
throw new IllegalStateException("source code == null");
}
return source;
}
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(getByteCode());
}
@Override
public OutputStream openOutputStream() {
return bytecode = new ByteArrayOutputStream();
}
public byte[] getByteCode() {
return bytecode.toByteArray();
}
}
然后实现一个Java文件管理器,完成相应的编译以及加载
public class JavaFileManagerImpl extends ForwardingJavaFileManager<JavaFileManager> {
private final ClassLoaderImpl classLoader;
private final Map<URI, JavaFileObject> fileObjects = new HashMap<>();
protected JavaFileManagerImpl(JavaFileManager fileManager, ClassLoaderImpl classLoader) {
super(fileManager);
this.classLoader = classLoader;
}
@Override
public FileObject getFileForInput(Location location, String packageName, String relativeName) throws IOException {
JavaFileObject file = fileObjects.get(uri(location, packageName, relativeName));
if (file != null) {
return file;
}
return super.getFileForInput(location, packageName, relativeName);
}
// genCode
@Override
public JavaFileObject getJavaFileForOutput(Location location, String qualifiedName, JavaFileObject.Kind kind, FileObject outputFile)
throws IOException {
URI uri;
try {
uri = new URI(qualifiedName);
} catch (URISyntaxException e) {
throw new RuntimeException(e.getMessage());
}
JavaFileObject file = new JavaFileObjectImpl(uri, kind);
classLoader.add(qualifiedName, file);
return file;
}
@Override
public ClassLoader getClassLoader(JavaFileManager.Location location) {
return classLoader;
}
@Override
public String inferBinaryName(Location loc, JavaFileObject file) {
if (file instanceof JavaFileObjectImpl) {
return file.getName();
}
return super.inferBinaryName(loc, file);
}
@Override
public Iterable<JavaFileObject> list(Location location, String packageName, Set<JavaFileObject.Kind> kinds, boolean recurse)
throws IOException {
Iterable<JavaFileObject> result = super.list(location, packageName, kinds, recurse);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
List<JavaFileObject> files = new ArrayList<>();
if (location == StandardLocation.CLASS_PATH && kinds.contains(JavaFileObject.Kind.CLASS)) {
for (JavaFileObject file : fileObjects.values()) {
if (file.getKind() == JavaFileObject.Kind.CLASS && file.getName().startsWith(packageName)) {
files.add(file);
}
}
files.addAll(classLoader.files());
} else if (location == StandardLocation.SOURCE_PATH && kinds.contains(JavaFileObject.Kind.SOURCE)) {
for (JavaFileObject file : fileObjects.values()) {
if (file.getKind() == JavaFileObject.Kind.SOURCE && file.getName().startsWith(packageName)) {
files.add(file);
}
}
}
for (JavaFileObject file : result) {
files.add(file);
}
return files;
}
public void putFileForInput(StandardLocation location, String packageName, String relativeName, JavaFileObject file) {
fileObjects.put(uri(location, packageName, relativeName), file);
}
private URI uri(Location location, String packageName, String relativeName) {
try {
return new URI(location.getName() + "/" + packageName + "/" + relativeName);
} catch (URISyntaxException e) {
throw new RuntimeException("uri error");
}
}
}
最后我们来实验一下:
private static final JavaCompiler compile = ToolProvider.getSystemJavaCompiler();
public static void main(String[] args) {
List<String> options = new ArrayList<String>();
options.add("-source");
options.add("1.6");
options.add("-target");
options.add("1.6");
String packageName = "";
String className = "Point";
/**
* 实际中会加代理,这里节约时间就不加了
*/
StringBuilder buf = new StringBuilder("public class Point {\n")
.append("public void halo() {\n")
.append("System.out.println(\"fdfdf\");\n")
.append("}\n")
.append("}\n");
JavaFileObjectImpl javaFileObject = new JavaFileObjectImpl(className, buf.toString());
final DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>();
StandardJavaFileManager manager = compile.getStandardFileManager(diagnosticCollector, null, null);
ClassLoader loader = Thread.currentThread().getContextClassLoader();
ClassLoaderImpl classLoader = AccessController.doPrivileged(new PrivilegedAction<ClassLoaderImpl>() {
@Override
public ClassLoaderImpl run() {
return new ClassLoaderImpl(loader);
}
});
JavaFileManagerImpl fileManager = new JavaFileManagerImpl(manager, classLoader);
fileManager.putFileForInput(StandardLocation.SOURCE_PATH, packageName, className + JavaFileObject.Kind.SOURCE.extension,
javaFileObject);
Boolean result = compile.getTask(null, fileManager, diagnosticCollector, options, null, Arrays.asList(javaFileObject))
.call();
Class<?> clazz = null;
try {
clazz = classLoader.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
System.out.println("1 = " + 1);
}
// 代理这个类
try {
// 追求性能可以使用javaassist
Object obj = clazz.getConstructor().newInstance();
Method main = clazz.getMethods()[0];
main.invoke(obj);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
可以看到最后的运行结果打印出出了fdfdf
。
对应的流程可以复制下来,使用单步调试法,就可以知道几个核心方法的调用上下文,对于理解虚拟机加载这个机制有很大的帮助。
其实现在有很多的字节码三方框架了,比如最近比较流行的ByteBuddy
,以及老一点的javassist
,cglib
等。但是无论选用哪种,都需要对类加载十分熟悉才行。
对于虚拟机类加载的其他阶段,实际写代码中可控性几乎没有,因此感兴趣的可以去看下相关的阶段。
我是CAFFBABE
,一周一肝虚拟机原理实战篇,我们下周再见。