专栏首页Java架构解析读《深入理解Java虚拟机》解决实际问题及总结JDK和JVM整体架构
原创

读《深入理解Java虚拟机》解决实际问题及总结JDK和JVM整体架构

前言

以前看别人博客说看完《深入理解Java虚拟机》这本书并没有让自己的编程水平提高多少,不过却大大提高了自己的装逼水平。其实,我倒不这么认为,至少在我看完一遍这本书后,有一种醍醐灌顶的感觉,很多模糊的知识和概念也变得清晰起来。今天,也是偶然的机会能够运用书中所学的知识解决实际问题,在这里,与大家分享一下,如有不正确的地方,还请指正。

问题描述

预生产环境突然出现了一个运行时异常,异常信息如下(Error异常):

java.lang.NoClassDefFoundError: javax/servlet/ServletOutputStream at com.soa.xxx.ProductTransForm.transProduct(ProductTransForm.java:10) ...... Caused by: java.lang.ClassNotFoundException: javax.servlet.ServletOutputStream at java.net.URLClassLoader$1.run(URLClassLoader.java:366) at java.net.URLClassLoader$1.run(URLClassLoader.java:355)

报异常的代码如下(根据真实项目场景模拟代码):

public class ProductTransForm { public ProductRespVo transProduct(ProductVo productVo) { ProductRespVo productRespVo = new ProductRespVo(); productRespVo.setProId(productVo.getProId()); productRespVo.setName(productVo.getName()); // TODO:注意下面这行代码,出问题的代码 productRespVo.setImage(FtpUtil.getFtpPath() + File.separator + productVo.getImage()); return productRespVo; } }

import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class FtpUtil { public static String getFtpPath() { return "The path of Ftp"; } public static void downloadFile(HttpServletRequest req, HttpServletResponse resp) { // 下载代码逻辑 } }

问题出在静态方法调用:FtpUtil.getFtpPath(),初看之下,并没有什么问题,静态方法getFtpPath()只是简单地返回一个地址字符串。

原因分析

经过各种尝试、调试以及重新打包等都没有能解决问题。这时候,突然想到《深入理解Java虚拟机》中有关Java类的初始化机制中讲到过类的初始化时机,因为FtpUtil类的getFtpPath()方法为静态方法,而调用一个类的静态方法会触发其初始化,带着这个设想,我写下了以下一行代码:

FtpUtil ftpUtil = new FtpUtil();

启动运行,果然重现了错误。既然原因是出在FtpUtil类的初始化上,那么从FtpUtil这个类着手分析,异常信息显示找不到ServletOutputStream类的定义,而在引入的包"javax.servlet.http.HttpServletResponse"的父接口也确实找到了对ServletOutputStream类的引用,但奇怪的是该类所在的包:servlet-api.jar是有引入的,否则也不能正常导入"javax.servlet.http.HttpServletResponse"包,于是猜测可能是jar包冲突,查看工程,发现工程中确实存在多个不同版本的servlet-api.jar(历史原因):

因此猜测是servlet jar包冲突导致的。

问题解决

定位了原因之后,首先想到的就是《深入理解Java虚拟机》书中讲到过的类的加载机制和双亲委派模型:

“如果一个类加载器收到类收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需要的类)时,子加载器才会尝试自己去加载。”。从上图可以看到,由于启动类加载器和扩展类加载器的搜索范围内都没有servlet-api.jar包,所以无法加载ServletOutputStream类,因此,应用程序类加载器会尝试自己加载类ServletOutputStream,而ClassPath范围内存在多个不同版本的servlet-api.jar包,所以出现包冲突。

基于以上分析,我将一个servlet-api.jar包拷贝到JRE/lib/ext路径下,这样,扩展类加载器能够加载拷贝jar包中的ServletOutputStream类,应用程序加载器就不会再去加载ServletOutputStream类,也就不会冲突了。经过重启程序验证,果然没有再抛异常了。

从上图也可以看出,为什么我们不能够自己定义一些与JDK类名、路径完全一样的类来覆盖JDK的类(如String),因为这些类在rt.jar中,由启动类加载器加载,我们自己定义的同名同路径类根本没有加载的机会,也就不可能覆盖JDK的类了。记得有一场面试,面试官问道:我们有一个项目需要在不同的JDK版本运行,如果保证jar的兼容不冲突?想来也是想考这方面的知识吧。

补充:

一、类的初始化时机

虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4个字节码指令时,如果类没有经过初始化,则需要触发其初始化;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有初始化,则需要触发其初始化;
  3. 当初始化一个类时,如果发现它的父类没有进行过初始化,则需要先触发其父类的初始化;
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类;
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invokke.MethodHandle实例最后解析的结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

二、类加载器

1、启动类加载器(Bootstrap ClassLoader)

负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放到lib目录中也不会被加载)类库加载到虚拟机内存中。

2、扩展类加载器(Extension ClassLoader)

负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3、应用程序类加载器(Application ClassLoader)

负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

三、JVM整体架构

四、JDK体系结构

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 整天跟微服务打交道,你不会连RPC都不知道吧?

    首先了解什么叫RPC,为什么要RPC,RPC是指远程过程调用,也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不...

    Java小朔哥
  • Java多线程:还不懂线程池吗?一文带你彻底搞懂!

    总体来说,线程池有如下的优势: (1)降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 (2)提高响应速度。当任务到达时,任务可以不需要...

    Java小朔哥
  • 阿里P8谈Java工程师怎么进大厂?你没进大厂就是这四个问题!

    进大厂的可能性是因人而异的,有些人基础好,能力强,概率会大很多;有些人底子差,自然会更难。由于咨询这个问题的同学比较多,接下来,我就说说大家关心的几点吧。

    Java小朔哥
  • Xposed加载JNI库

    用户1907613
  • jvm系列(一):java类的加载机制

    1、什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个 java.lang.Cl...

    纯洁的微笑
  • JVM类加载过程与双亲委派模型

    类加载过程为JVM将类描述数据从.class文件中加载到内存,并对数据进行解析和初始化,最终形成被JVM直接使用的Java类型。包含:

    武培轩
  • 2.1 类加载器、 双亲委派模型 -《SSM深入解析与项目实战》

    上一章节中,对Spring、Spring MVC、MyBatis进行了一些简单的介绍,以及它们之间的分工合作。相信大家对于Spring、Spring MVC以及...

    谙忆
  • JVM类加载过程与双亲委派模型

    类加载过程为JVM将类描述数据从.class文件中加载到内存,并对数据进行解析和初始化,最终形成被JVM直接使用的Java类型。包含:

    武培轩
  • 类加载器中的双亲委派模型详解

    在上一篇文章中,我们梳理了类加载器的基本概念:类的生命周期、类加载器的作用、类的加载和卸载的时机等等,这篇文章我们接着前文继续复习类加载器的知识,主要包括:JV...

    阿杜
  • Java类加载机制

    在了解类的加载机制之前,我们需要了解一下类的生命周期。Java类从被加载到JVM内存开始,到卸载出内存为止,它的整个生命周期包括了:加载(Loading),验证...

    CodingDiray

扫码关注云+社区

领取腾讯云代金券