在网上看到有些人说“一个类在一个jvm中只能加载一次”,对此产生了怀疑。
另外,在用flink、spark做计算的时候,有一个疑惑,如果用相同的jar包启动了相同的任务,而这两个任务被分配到了同一个进程的不同线程里,是不是意味着这两个任务是共用类的?如果是这样,写flink程序的时候,操作类成员变量岂不是成为一种很危险的动作?
然后我就尝试去研究了一下flink的源码,只看到了一小部分,结合一些实验+一些大胆猜测得出结论如下:一个类在一个jvm里可以加载多次,而flink的两个任务不会共用相同的类(即便他们是来自相同jar包的同名类)。
得出这个结论只需要了解java的类加载机制。java中有四种类加载器,程序运行需要的类都会由他们加载到内存中。这四种种类加载器分别是BootStrap ClassLoader、Extension ClassLoader、Application ClassLoader、自定义ClassLoader。
一个类由它的全路径限定符和加载它的classLoader决定,也就是说同一个jar中的class如果被不同的类加载器进行了加载,那么这个类在内存就会存在两份,而且两个类并不相同。
看如下例子:(参考自https://www.iteye.com/blog/yongxin10020071209191159-252393)
待加载类:
import java.util.Random;
public class IntProducer
{
//用随机值来加载静态域赋值,如果被加载两次age应该会是不同的值
public static int age = getRandom();
public static int getRandom()
{
Random ran = new Random(System.nanoTime());
int rand = ran.nextInt(10);
System.out.println("IntProducer 被加载,产生随机数:"+rand);
return rand;
}
}
加载测试:
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception
{
System.out.println(Main.class.getClassLoader());
URL[] us = { new URL("file:///Users/zgy/test/") };
ClassLoader loader1 = new URLClassLoader(us);
Class c1 = loader1.loadClass("IntProducer");
System.out.println(c1.getClassLoader()+":"+c1.getField("age").getInt(c1));
//再加载一次
ClassLoader loader2 = new URLClassLoader(us);
Class c2 = loader2.loadClass("IntProducer");
System.out.println(c2.getClassLoader()+":"+c2.getField("age").getInt(c2));
}
}
输出如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
IntProducer 被加载,产生随机数:1
java.net.URLClassLoader@61bbe9ba:1
IntProducer 被加载,产生随机数:0
java.net.URLClassLoader@1d44bcfa:0
可以看到同一个类被加载了两次,对应的classLoader是不一样的。
根据flink的任务提交代码,可以看到在任务分发的时候,同时将classLoader也发送了出去,可以推测出,在任务运行的线程中会恢复这个classLoader,并使用此classLoader加载需要的类。
然后,转折来了。标题里的说法就是错的吗?也不全是,只是需要加上限定条件。在不指定ClassLoader的时候,程序两次加载同一个类,确实只是会加载一次,这个是完全必要的,因为只有这样才能保证类加载的安全。否则,想用到一个类,加载两次反而是不一样的,岂不是疯了。jvm专门设计了双亲委派模型,来保证这种加载机制。