生命周期
1)加载
获取类的二进制字节流,并转换为方法区的运行时数据结构,内存中创建该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
2)验证
确保加载进来的字节流符合JVM规范,包括文件格式验证 、元数据验证、字节码验证、符号引用验证。
3)准备
为静态变量在方法区分配内存,并设置初始值。
public static int v = 8080; // 初始值为0,赋值putstatic指令被编译到clinit方法中
public static final int v = 8080; // 被编译成ConstantValue属性,并根据ConstantValue属性赋值
4)解析
将常量池中的符号引用替换为直接引用。
a)符号引用
与虚拟机实现布局无关,引用目标不一定已加载到内存中,符号引用字面量形式明确定义在JVM规范Class文件格式中。
b)直接引用
指向目标指针、相对偏移量或是一个能间接定位到目标的句柄,如果有了直接引用,那被引用对象必定在内存中。
5)初始化
执行类构造器clinit方法的过程,clinit方法由编译器按顺序收集静态变量赋值动作和静态语句块中语句产生的,静态语句块只能访问定义在之前的变量,定义在之后的变量,可以赋值但不能访问。虚拟机会保证clinit方法执行之前,父类的clinit方法已经执行完毕。如果类没有静态变量赋值或静态语句块,编译器可不生成clinit方法。
执行初始化:
a)创建对象(比如new、反射、序列化)
b)静态字段读取或赋值(执行getstatic或者putstatic指令),final修饰除外
c)调用静态方法(执行invokestatic指令)
d)调用反射方法,如Class.forName(“xxxx”)
e)被标明的启动类(包含main()方法)
f)子类初始化,会先初始化父类;子类引用父类静态字段,只会引发父类初始化
不执行初始化:
a)子类引用父类静态字段,只会触发父类初始化,不会触发子类初始化
b)定义对象数组,不会触发类初始化
c)静态常量会被编译到常量池,不会触发类初始化
d)通过类名获取Class对象,不会触发类初始化
e)Class.forName加载类时,如果指定参数initialize为false,不会触发类初始化
f)通过ClassLoader默认的loadClass方法,不会触发类初始化
class Singleton{
private static Singleton singleton = new Singleton();
public static int value1;
public static int value2 = 0;
private Singleton(){
value1++;
value2++;
}
public static Singleton getInstance(){
return singleton;
}
}
class Singleton2{
public static int value1;
public static int value2 = 0;
private static Singleton2 singleton2 = new Singleton2();
private Singleton2(){
value1++;
value2++;
}
public static Singleton2 getInstance2(){
return singleton2;
}
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println("Singleton1 value1:" + singleton.value1);
System.out.println("Singleton1 value2:" + singleton.value2);
Singleton2 singleton2 = Singleton2.getInstance2();
System.out.println("Singleton2 value1:" + singleton2.value1);
System.out.println("Singleton2 value2:" + singleton2.value2);
}
输出:
Singleton1 value1: 1
Singleton1 value2: 0
Singleton2 value1: 1
Singleton2 value2: 1
准备阶段:
singleton = null
singleton2 = null
value1 = 0
value2 = 0
初始化阶段:
Singleton类clinit方法:
singleton = new Singleton()
value2 = 0
=> value1: 1,value2: 0
Singleton2类clinit方法:
value2 = 0
singleton2 = new Singleton2()
=> value1: 1,value2: 1
类加载器
1)启动类加载器
顶层类加载器,负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
2)扩展类加载器
负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
3)应用程序类加载器
也叫做系统类加载器,可以通过getSystemClassLoader()获取,负责加载用户路径上的类库。
4)双亲委派模型
当类加载器对类进行加载时,会首先提交给父类加载器去加载,只有当父类加载器无法加载时,才会尝试执行加载。
Java探针
在不影响线上业务的前提下,动态修改类字节码植入相关调试或监控代码,操作类字节码的工具主要有Javassist或ASM。
下面以一个具体例子进行说明:
1)被监控程序
package app;
public class OnlineProgram {
public static void main(String[] args) throws Exception {
System.err.println("======OnlineProgram执行======");
sayHello("message1", 1000);
sayHello("message2", 2000);
}
private static void sayHello(String message
, long sleepTime) throws Exception {
System.out.println(String.format("sayHello:%s", message));
Thread.sleep(sleepTime); // 模拟耗时
}
}
创建src/main/resources/META-INF/MANIFEST.MF文件,添加内容:
Manifest-Version: 1.0
Main-Class: app.OnlineProgram
然后打包成JAR文件,这里假设打包文件为:app.jar
2)监控程序
a)创建ClassFileTransformer 接口实现类OnlineTransformer
实现ClassFileTransformer接口,目的就是在class被加载到JVM之前,将class字节码替换掉,从而达到动态注入代码的目的,代码如下:
package agent;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.*;
public class OnlineTransformer implements ClassFileTransformer {
private static final String monitorMethod = "test.OnlineProgram.sayHello"; // 被监控方法
private static String monitorClassName; // 监控类名称
private static String monitorMethodName; // 监控方法名称
static {
int pos = monitorMethod.lastIndexOf(".");
monitorClassName = monitorMethod.substring(0, pos);
monitorMethodName = monitorMethod.substring(pos + 1);
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
className = className.replace("/", "."); // 类名称字符转换
if (!Objects.equals(className, monitorClassName)) {
return null;
}
try {
CtClass clas = ClassPool.getDefault().get(className);
if (clas == null) {
System.err.println("Class " + className + " not found");
} else {
return addTiming(clas, monitorMethodName);
}
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 添加执行耗时
* @param clas Javassist类对象
* @param mname 方法名称
* @return 返回类字节码
*/
private byte[] addTiming(CtClass clas, String mname)
throws NotFoundException, CannotCompileException, IOException {
// 获取方法信息
CtMethod mold = clas.getDeclaredMethod(mname);
// 重命名原始方法,并复制一个与原始方法同名的拦截方法
String nname = mname + "$impl";
mold.setName(nname);
CtMethod mnew = CtNewMethod.copy(mold, mname, clas, null);
// 定义方法体,植入相关代码
String type = mold.getReturnType().getName();
StringBuilder body = new StringBuilder();
body.append("{\nlong start = System.currentTimeMillis();\n");
if (!"void".equals(type)) {
body.append(type).append(" result = ");
}
// 调用原始方法
body.append(nname).append("($$);\n");
// 打印耗时信息
body.append("System.out.println(\"Call to method ").append(mname);
body.append(" took \" + \n (System.currentTimeMillis() - start) + ").append("\" ms.\");\n");
if (!"void".equals(type)) {
body.append("return result;\n");
}
body.append("}");
// 替换拦截方法内容,并添加到类当中
mnew.setBody(body.toString());
clas.addMethod(mnew);
return clas.toBytecode();
}
}
b)创建代理类
package agent;
import java.lang.instrument.Instrumentation;
public class OnlineAgent {
/**
* 该方法在main方法之前运行
* @param agentOps 入参
* @param inst 对象
*/
public static void premain(String agentOps, Instrumentation inst) {
System.out.println("======premain方法执行======");
System.out.println(String.format("入参:%s", agentOps));
// 添加Transformer
inst.addTransformer(new OnlineTransformer());
}
}
创建src/main/resources/META-INF/MANIFEST.MF文件,添加内容:
Manifest-Version: 1.0
Premain-Class: agent.OnlineAgent
Can-Redefine-Classes: true
Boot-Class-Path: javassist-3.20.0-GA.jar
然后打包成JAR文件,这里假设打包文件为:agent.jar
3)执行程序
拷贝app.jar、agent.jar、javassist-3.20.0-GA.jar到同一目录,执行如下命令:
java -javaagent:agent.jar="Java Agent" -jar app.jar
运行结果:
======premain方法执行======
入参:Java Agent
======OnlineProgram执行======
sayHello:message1
Call to method sayHello took 1001 ms.
sayHello:message2
Call to method sayHello took 2002 ms.
3)根据自定义MANIFEST.MF打包,POM配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>agent</groupId>
<artifactId>OnlineAgent</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>