版本: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_AppExtension Class loader
加载Java标准扩展库,$JAVA_HOME/lib/ext1
2// 覆盖ext目录
java -Djava.ext.dirs=your_ext_dir HelloWorldApplication Class loader
加载环境变量-classpath下的包- 自定义类加载器
- JDK9 的layer层级 TODO
如何自定义类加载器? 应用场景?
- 1、容器的实现上采用自定义类加载器来加载容器需要的包:tomcat;
- 2、热部署;
- 3、导入不同版本的同名包:import其中一个版本,另一个版本通过类加载器加载,用反射方式获得另一个版本包的方法;
4、代码保护:先字节码加密,加载类的时候必须对字节码进行解密,通过重写findClass读取URL中的字节码,然后解密,最后把字节数组交给defineClass()加载;
1
2
3
4
5
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
10try {
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 | // 增强 |
1 | // agent 实现类 |
1 | #添加src/META-INF/MANIFEST.MF |
1 | // test |
agentmain实现
1 | // agent 增强 |
1 | #添加src/META-INF/MANIFEST.MF |
1 | // 测试类 |
原理
原理,具体细节见大神的分析,有待深入研究确认
- 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
24struct _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