类加载机制

版本:java8

类加载过程是怎样的?

运行时,虚拟机把描述类的数据从Class文件加载到内存中,并对数据进行验证,准备,解析,初始化的一个过程,最终是可以被虚拟机直接使用的java类型
加载 -> 连接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载
如果输入数据不是 ClassFile 的结构,则会抛出 ClassFormatError

  • 加载
    通过全限定类名加载二进制流,静态存储结构转换方法区中运行时的数据结构,生成java.lang.Class对象,作为类数据的访问入口;(Class对象干嘛的?)
  • 连接
    1、验证:文件格式、元数据、字节码、符号引用
    2、准备:类静态变量分配内存、初始化系统默认值,主要在分配内存,不会执行具体的指令
    3、解析:常量池的符号引用转为直接引用 (符号引用与直接引用的概念?)
  • 初始化
    执行类里面的初始化代码逻辑,对 自定义类变量的赋值,先初始化加载父类再到子类
    触发:创建对象实例;反射;调用静态方法静态字段;启动main方法类

Class 对象

  • 定义
    Class 类的一种,只有私有构造函数,只能jvm创建加载,为运行时类型识别提供类型信息,每个类有一个Class对象,不管多少个实例对象;
  • 获取Class
    1、Object的getClass方法:Object是最顶层类,所以这个时候会触发类的加载初始化;
    2、Class.forName(“”) :通过全限定类名获取Class对象,会触发类的加载初始化;
    3、字面量.class:Class对象在类加载阶段,所以不会触发初始化。
  • 泛型Class
    1、通过指定泛型,保证使用时正确的类型,运行时会擦除;
    2、Class<?> 使用任意类型的泛型;
    3、Class<? extends 父类> 指定某父类下的子类,将类型检查提前到编译期。
  • 强制转换
    强制转换时,本质上是获取Class对象,调用cast方法进行转换;
  • 反射TODO

符号引用与直接引用

  • 符号引用:符号文本的形式表示引用之间的关系;
  • 直接引用:jvm里面的内存区域字节码的起始位置,或者偏移量,用于间接定位的句柄;
  • 转换:将常量池那块的符号引用,转换为直接引用,jvm运行时能定位到具体的内存位置,转换过程具体用到了方法表以及更底层的 TODO

什么是双亲委派模型?

为了避免重复加载类,子类加载器会委托父加载类加载去加载,如果父加载器找不到,子加载器才会进行加载。

有哪些类加载器,分别起什么作用?

  • Bootstrap Class loader
    jvm内部本地实现,XX.class.getClassLoader显示为null,加载$JAVA_HOME/jre/lib下的包,同时也加载其他的类加载器;

    1
    2
    3
    4
    5
    6
    7
    // java8及以下
    # 指定新的 bootclasspath,替换 java.* 包的内部实现
    java -Xbootclasspath:<your_boot_classpath> your_App
    # a 意味着 append,将指定目录添加到 bootclasspath 后面
    java -Xbootclasspath/a:<your_dir> your_App
    # p 意味着 prepend,将指定目录添加到 bootclasspath 前面
    java -Xbootclasspath/p:<your_dir> your_App
  • Extension Class loader
    加载Java标准扩展库,$JAVA_HOME/lib/ext

    1
    2
    // 覆盖ext目录
    java -Djava.ext.dirs=your_ext_dir HelloWorld
  • Application Class loader
    加载环境变量-classpath下的包

  • 自定义类加载器
  • JDK9 的layer层级 TODO

如何自定义类加载器? 应用场景?

  • 1、容器的实现上采用自定义类加载器来加载容器需要的包:tomcat;
  • 2、热部署;
  • 3、导入不同版本的同名包:import其中一个版本,另一个版本通过类加载器加载,用反射方式获得另一个版本包的方法;
  • 4、代码保护:先字节码加密,加载类的时候必须对字节码进行解密,通过重写findClass读取URL中的字节码,然后解密,最后把字节数组交给defineClass()加载;

    1
    2
    3
    4
    5
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    byte[] data = this.loadClassData(name);// 文件流进行加载,然后解密,可以通过异或0xff的方式先加密再解密
    return this.defineClass(name, data, 0, data.length);
    }
  • 5、加载网络来源的字节码信息。

java.lang.ClassLoader

  • loadClass : 先在已加载中寻找,然后调用父加载器进行加载,最后通过findClass进行加载,resolve=false则只检查类是否存在不进行解析;
  • defineClass:通过byte数组转换为对象实例;
  • findClass:由子类加载器进行扩展实现;

如何降低类加载的开销?

APPCDS,通过对类进行预处理到一个归档文件,jvm将归档文件映射到内存,减少类的加载开销;CDS概念在Java5提出,Java10得到扩展。TODO
java10新特性APPCDS

什么是 Jar Hell 问题?

出现classNotFoundException / NoClassDefFoundError,或者在某个环境服务器正常,另一个环境服务器异常,或者新加包更新包出现异常;这些状况就是出现了jar hell的问题,一般情况是依赖不清晰,传递依赖问题,包版本冲突导致;借助构建工具减少这类问题,例如maven、gradle. 或者使用组件系统来分隔依赖程度。

1
2
3
4
5
6
7
8
9
10
try {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
String resourceName = "net/sf/cglib/proxy/MethodInterceptor.class";
Enumeration<URL> urls = classLoader.getResources(resourceName);
while(urls.hasMoreElements()){
System.out.println(urls.nextElement());
}
} catch (IOException e) {
e.printStackTrace();
}

热部署、java agent

概念

javaagent,加载 class 文件之前做拦截可以对字节码做修改,运行时对已加载的字节码做修改,不侵入性的对应用功能做监控增强;
eclipse的调试功能、热部署实现
通过自定义类加载器实现热部署过程,字节码修改后重新触发类加载的过程;Java agent是不修改原有字节码,当字节码加载进来时,对加载进来的字节码进行修改的过程;

premain实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 增强
public class SimpleClassTransformer implements ClassFileTransformer {
@Override
public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined,
final ProtectionDomain protectionDomain, final byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.endsWith("sun/net/www/protocol/http/HttpURLConnection")) {
try {
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("sun.net.www.protocol.http.HttpURLConnection");
for (final CtConstructor constructor : clazz.getConstructors()) {
constructor.insertAfter("System.out.println(this.getURL());");
}
byte[] byteCode = clazz.toBytecode();
clazz.detach();
return byteCode;
} catch (final NotFoundException | CannotCompileException | IOException ex) {
ex.printStackTrace();
}
}
return null;
}
}
1
2
3
4
5
6
7
// agent 实现类
public class SimpleAgent {
public static void premain(String agentArgs, Instrumentation inst) {
final SimpleClassTransformer transformer = new SimpleClassTransformer();
inst.addTransformer(transformer);
}
}
1
2
3
4
5
6
7
8
9
10
#添加src/META-INF/MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: SimpleAgent的全限定名
#gradle方式
jar {
manifest {
attributes 'Implementation-Version': '1.0'
attributes 'Premain-Class': 'SimpleAgent的全限定名'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// test
public class AgentDemo {

public static void main( String[] args ) throws IOException {
fetch("http://www.baidu.com");
}

private static void fetch(final String address)
throws MalformedURLException, IOException {
final URL url = new URL(address);
final URLConnection connection = url.openConnection();
try( final BufferedReader in = new BufferedReader(
new InputStreamReader( connection.getInputStream() ) ) ) {
String inputLine = null;
final StringBuffer sb = new StringBuffer();
while ( ( inputLine = in.readLine() ) != null) {
sb.append(inputLine);
}
System.out.println("Content size: " + sb.length());
}
}
}
// 运行添加参数 -javaagent:myAgent.jar
// output:
// http://www.baidu.com
// Content size: 2283

agentmain实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// agent 增强
public class AgentmainAgent {
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
inst.retransformClasses(TestAgent.class);// 需要指定对应重定义类Class对象
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
#添加src/META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Agent-Class: AgentmainAgent的全限定类名
Can-Retransform-Classes: true
Can-Redefine-Classes: true
#gradle方式
manifest {
attributes 'Implementation-Version': version
ttributes 'Agent-Class': 'AgentmainAgent的全限定类名'
attributes 'Can-Retransform-Classes': 'true'
attributes 'Can-Redefine-Classes': 'true'
}
#MANIFEST.MF 和AgentmainAgent 一起打成jar包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 测试类
public class AgentmainAgentDemo {
public static void main(String[] args) throws InterruptedException {
for (;;) {
new TestAgent().test();
Thread.sleep(5000);
}
}
}
// 将agent attach到对应jvm的类
public class JVMTIThread {
public static void main(String[] args)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().endsWith("AgentmainAgentDemo")) { // 确定测试类
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent("agent jar包全路径", "参数");
System.out.println("ok");
virtualMachine.detach();
}
}
}
}
// gradle 添加tools.jar
dependencies {
compile files("${System.properties['java.home']}/../lib/tools.jar")
}
// 先执行AgentmainAgentDemo,再执行JVMTIThread, 这样的操作只能修改一次?
// output:
TestAgent test...
agentmain load Class :com/sf/dawn/agentmain/TestAgent
TestAgent test...
TestAgent test...
TestAgent test...

原理

原理,具体细节见大神的分析,有待深入研究确认

  • 1、JVMTI:JVM Tool Interface, JVM 暴露出来的一些供用户扩展的接口集合,JVMTI 基于事件驱动,JVM 每执行到一定的逻辑就会调用一些事件的回调接口,这些接口可以供开发者扩展自己的逻辑;
  • 2、Instrument接口,则是JVMTI接口中的一个;
  • 3、JVMTIAgent:动态库,JVMTI逻辑回调的实现;通常会实现3个函数的全部和部分:
    3.1、Agent_OnLoad函数,agent在jvm启动时候进行加载,则是通过vm参数指定的;
    3.2、Agent_OnAttach函数, agent不在jvm启动时候进行加载,则通过attach机制,进行进程间通信,向目标进程发送load命令;
    3.3、Agent_OnUnload函数, agent卸载时调用;
  • 4、Instrument agent是其中一个JVMTIAgent,又叫JPLISAgent(Java Programming Language Instrumentation Services Agent),实现了Agent_OnLoad、Agent_OnAttach;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    struct _JPLISAgent {
    JavaVM * mJVM; /* handle to the JVM */
    JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */
    JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */
    jobject mInstrumentationImpl; /* handle to the Instrumentation instance */
    jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
    jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */
    jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */
    jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */
    jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */
    jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
    jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
    char const * mAgentClassName; /* agent class name */
    char const * mOptionsString; /* -javaagent options string */
    };

    struct _JPLISEnvironment {
    jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
    JPLISAgent * mAgent; /* corresponding agent */
    jboolean mIsRetransformer; /* indicates if special environment */
    };
    // mInstrumentationImpl 指向sun.instrument.InstrumentationImpl
    // mPremainCaller 指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法
    // mAgentmainCaller 指向 sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain 方法
  • 5、premain方式过程
    5.1 创建并初始化 JPLISAgent
    5.2 监听 VMInit 事件,在 vm 初始化完成之后做下面的事情:
    创建 InstrumentationImpl 对象
    监听 ClassFileLoadHook 事件
    调用 InstrumentationImpl 的loadClassAndCallPremain方法,在这个方法里会调用 javaagent 里 MANIFEST.MF 里指定的Premain-Class类的 premain 方法
    5.3 解析 javaagent 里 MANIFEST.MF 里的参数,并根据这些参数来设置 JPLISAgent 里的一些内容

  • 6、agentmain方式过程
    6.1 创建并初始化 JPLISAgent
    6.2 解析 javaagent 里 MANIFEST.MF 里的参数
    6.3 创建 InstrumentationImpl 对象
    6.4 监听 ClassFileLoadHook 事件
    6.5 调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会调用 javaagent 里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法

字节码修改类库:Javassist、ASM、cglib

web容器类加载

  • jetty
  • tomcat

FIXME

参考