研究JVM也有一段时间了,其间也发现了它的很多不足之处,在此一一道来,由于本人对JVM的理解有限,如有错误的地方,还请大家指正;本文不介绍名词性术语和概念性知识,如有不了解的地方可Search Google或者参考一些JVM相关的书籍;
1. 众所周知,所有Java类的元数据在经编译之后,会被保存到Class文件里,个人认为这是一个很扯淡的想法,原因如下:
- 整个Class文件显得很臃肿,我们在非产品环境中所有的调试信息,如JVM指令与源代码行的对应信息,也会被保存在Class文件里;
- 降低了获取元数据的速度,比如我们要获取一个包中所有标注了某个Annotation的类(使用Spring MVC时标注了@Controller的Java类),必须加载这个包中所有公共类的Class文件,然后再一一遍历;
- 无法在不加载Class文件的情况下,获取类型的元数据信息;我们有时候会想得到一个第三方库中的信息,但是不能确定这些包中是否包含了恶意代码,如果加载这些Class文件,势必会执行初始化,这会给恶意代码制造了破坏整个应用程序的机会。[注:这里我说得有问题,事实上是可以做到的]
2. JVM指令有限,虚拟机规范指明在Class文件中每个指令保存为一个字节,也就是说虚拟机中最多允许存在256个指令,这在JVM诞生时,明显足够了;但是计划跟不上变化,随着技术的不断进步,新的特性会越来越多的出现在Java体系中,这些特性需要来自虚拟机层面的支持,因此要求增加更多的虚拟机指令(如JDK1.7为增加对动态语言的支持而新增的invokedynamic),但由于指令空间的局限性导致JVM发展有限。
3. 不合理的范型设计,Java采用了伪范型的方式来支持对集合算法的封装,而在编译之后所有的范型类型参数会被擦除为Object,我无法考证设计者基于什么样的考量而采用这种方式,但从某本书中获得这样的讯息:采用擦除的方式避免了“代码爆炸”的危险;这样的说法明显存在一些问题:
- 动态生成运行时代码的地方有很多,比如运用得最广的动态代理技术,即在java.lang.reflect.Proxy中,使用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流;
- 无论运行时生成的代码还是编译时生成的代码,都保存在永久代中(以HotSpot VM为例),而JVM也会在内存不足的情况下对永久代进行回收,所以一般情况下不会出现代码爆炸的危险。
4. 垃圾回收发生的时间问题;我所理解的垃圾回收,应该在三种情况下发生:
- 内存不足(这里所说的内存不足,都是指分配到某个代上对象的大小超过了该代的容量限制);
- 代码调用System.gc();
- 虚拟机根据运行时环境自动触发;
但据我所了解,很多虚拟机只支持前两者,这样虽然减少了GC执行的频率,但是却增多了由于JVM在垃圾回收时停顿所有线程而导致应用程序无响应的危险。