Pinpoint是当前APM非侵入采集的代表;Pinpoint实际上是一个非常强大的javaagent,对Pipoint中类加载原理进行理解有助于对整个Pinpoint非侵入式探针有一个全面的系统的认知。
Classloder 简介
JVM默认class loader
JVM默认有三个class loader, boot class loader, ext class loader 和App class loader,
boot class loader 是所有类加载器的根,在JVM启动时负责加载JVM内核(具体可以通过System.getProperty("sun.boot.class.path")查看具体加载哪些类文件) boot class loader 在系统中是不可获得的, 例如在通过其加载的类Sting.class.getClassLoader()时,可以看到获得的class loader 为null。
但是我们可以通过Java agent的入口参数Instrument接口提供的方法 appendToBootstrapClassLoaderSearch 向 boot class path中添加jar包,这样的这个jar中的类 在被使用的时候就会被boot class loader 加载。
ext class loader 负责加载 JVM扩展类 可以通过 System.getProperty("java.ext.dirs")查看具体加载的jar包 ext class loader的parent是null,实际上是boot class loader。 可以通过ClassLoader.getSystemClassLoader().getParent().getParent()进行验证。
App class loader 负责加载 class path下所有的类,可以通过 System.getProperty("java.class.path")查看具体加载的jar路径 加载路径ClassLoader.getSystemClassLoader() 就是App class loader App class loader的parent是 ext class loader. ClassLoader.getSystemClassLoader().getParent()进行验证。
####Class Loader的双亲委托机制。 直接贴一段java.lang.ClassLoader的代码
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;
}
}
从上面代码中很容易看出,首先在系统中查看该类是否已经加载,如果已经加载则直接返回,如果没有加载,则根据parent是否是null由parent加载还是bootstrap classloader加载, 如果没有加载到,则由自己加载。这个就是双亲委托机制。那么这么做的好处是什么呢?
请尝试思考一下,如果不是用双亲委托机制,当一个用户自己定义了一个 java.long.String 类的时候,并放在classpath路径下的时候,如果子classloader优先加载的时候, jvm就会加载用户自定义的java.lang.String, 这里只是举了一个简单的例子,如果某些核心的类被别有用心的人劫持很可能发生意想不到的灾难。
Thread Context classloader 简介
Thread Context classloader打破了双亲委托机制的限制,父classloader可以使用当前线程的classloader加载类,颠覆了父classloader不能使用子classloader或者没有 直接父子关系的classloader中加载类这种情况。在双亲委托机制的下,当 A 类使用了B类的时候, B 类必须在 A类的class loader及classloader parent 的classpath之内; 否则会报 Class Not Found Exception。
这么说可能比较抽象,以SPI (JDBC Driver)为例来进行说明; 这里有必要说明一下SPI(Service Provider Interface), 常见的SPI主要有, JDBC, JCE, JNDI, JAXP, JBI等。这些SPI的接口都是由Java核心库来提供,而这些SPI具体 的实现确是由各个厂商实现,所以当我们在使用的时候,SPI的接口是由Bootstrap classloader来加载,而具体的实现确是由App classloader加载的。
下面以JDBC 来分析一下SPI类的加载过程,看完下面的介绍,相信大家会对classloader有一个新的认识。
Driver driver = Class.forName("com.mysql.jdbc.Driver").newInstance()
Connection conn = driver.getConnection("jdbc:mysql://host:port/db");
这是以前没有使用SPI的一个Mysql JDBC链接的经典写法。 那么在Java6之后,我们就只需要把mysql-connector.jar加到classpath中之后,像下面这样来创建JDBC链接
Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://host:port/db")
z这个代码是如何加载到Mysql 的Driver的呢?我们就需要从DriverManager这个类入手了。
public class DriverManager {
// List of registered JDBC drivers
private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>();
/* Prevent the DriverManager class from being instantiated. */
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch