一个java文件的JVM之旅 | 京东物流技术团队
准备
我是小C同学编写得一个java文件,如何实现我的功能呢?需要去JVM(Java Virtual Machine)这个地方旅行。
变身
我高高兴兴的来到JVM,想要开始JVM之旅,它确说:“现在的我还不能进去,需要做一次转换,生成class文件才行”。为什么这样呢?
JVM不能直接加载java文件的原因:
- Java源代码中包含了许多高级语言特性和语法,比如类、继承、多态、异常处理等等。这些高级特性在JVM中没有直接对应的形式,只有通过编译器的处理才能转化为JVM可以理解的字节码指令。
- Java源代码需要经过编译器的编译过程,才能生成相应的字节码文件,然后再由JVM加载、解释执行。在编译过程中,编译器对源代码进行语法分析、类型检查、优化等操作,最终生成与目标平台兼容的Java字节码文件。
- JVM只能够加载和运行符合Java虚拟机规范的.class字节码文件,而不能够直接加载和运行Java源代码文件。
编译
知道原因后,我又问JVM,我怎么才能变成class文件呢,JVM告诉我可以通过javac命令。
javac
javac 是 Java 编译器命令,用于将 Java 源代码文件编译成字节码文件(.class 文件)。
命令格式
javac [options] [source files]
- options:为编译选项,可以控制编译器的行为,例如指定类路径、生成调试信息、压缩文件等。
- source files:为需要编译的 Java 源代码文件,可以指定多个文件,用空格隔开。如果不指定源代码文件,则 `javac` 命令会在当前目录查找所有扩展名为 `.java` 的文件进行编译。
需要注意的是,`javac` 命令需要在正确配置 JDK 环境后才能使用。JDK(Java Development Kit)是 Java 开发工具包的缩写,是 Java 应用程序开发的核心组件之一。
具体实现
编译器在编译源文件时,需要对源文件进行语法分析、语义分析和类型检查等操作。
- 语法分析:
javac
命令首先将源文件读入内存,然后进行词法分析和语法分析。词法分析器负责将源文件中的字符序列转换成一个个单词(Token),然后语法分析器将单词组合成可以被解释执行的语法结构,形成抽象语法树(AST)。 - 语义分析:
javac
命令在生成AST之后,进行语义分析。语义分析器主要是为了检查程序中是否存在语义错误,例如变量未定义、类型不匹配等,如果发现语义错误,编译器会输出错误信息,并中止编译过程,不会生成字节码文件。 - 类型检查:
javac
命令在语义分析的基础上,进行类型检查。类型检查器主要是检查程序的类型是否匹配和兼容,如果类型不匹配或不兼容,编译器会在编译期间报告错误。 - 代码生成:
javac
命令在生成抽象语法树后,对其进行优化和转化,最终生成字节码文件。编译器会根据目标代码的平台和版本,生成适当的字节码文件。
执行
知道怎么变身后,我立即通过javac命令,让自己变成可以被JVM执行的class文件。
加载
变成class文件后,我怎么能进入JVM内部呢,是走着去还是坐车去呢?JVM告诉我要通过类加载器进入。
类加载器
Java类加载器是Java虚拟机(JVM)中的一个重要组件,它负责将类文件(.class文件)加载到JVM中。
分类
Java 中的类加载器是按照其加载类的特点进行分类的,主要有以下几种类型:
- 启动类加载器(Bootstrap ClassLoader):负责加载 JRE/lib/rt.jar 中的核心 Java 类库,是最顶层的类加载器,不是 Java 类(因为在 JVM 实现时就已经存在)。
- 扩展类加载器(Extension ClassLoader):负责加载 JRE/lib/ext 目录下的扩展类库,也是由 C++ 实现的类加载器。
- 应用程序类加载器(APP ClassLoader):负责加载应用程序的类,包括在 CLASSPATH 中指定的类库或者目录中的 Java 类。
- 自定义类加载器(Custom ClassLoader):继承自 ClassLoader 类,实现自己的类加载器,主要用于加载一些自定义的类或者修改某些类的字节码。
查看使用的类加载器
代码:
public class ClassLoaderTest { public static void main(String[] args) { //启动类加载器 System.out.println(String.class.getClassLoader()); //扩展类加载器 System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader()); //应用程序类加载器 System.out.println(ClassLoaderTest.class.getClassLoader()); //扩展类加载器的父加载器 System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader().getParent()); //应用程序类加载器的父加载器 System.out.println(ClassLoaderTest.class.getClassLoader().getParent()); } }
执行结果:
自定义类加载器
自定义类加载器主要包括两种类型:
- 独立的自定义类加载器,通过重载 ClassLoader 类中的 findClass 方法来实现加载类文件的功能;
- 基于 URLClassLoader 类实现的自定义类加载器,使用 URL 的形式来指定类文件的位置。
重载ClassLoader
代码:
public class CustomClassLoader extends ClassLoader { private String basePath; public CustomClassLoader(String basePath) { this.basePath = basePath; } @Override public Class<?> findClass(String name) throws ClassNotFoundException { byte[] data = getClassData(name); if (data == null) { throw new ClassNotFoundException(); } else { // 使用 defineClass 方法将 byte 数组转换为 Class 对象 return defineClass(name, data, 0, data.length); } } private byte[] getClassData(String className) { String path = basePath + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; try (InputStream inputStream = new FileInputStream(path); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, length); } return outputStream.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } } }
说明:
上述代码继承了ClassLoader
类,并重写了其中的findClass()
方法,实现从指定目录中加载类文件的功能。
在findClass()
方法中,首先通过getClassData()
方法读取并返回类文件的字节数组,如果获取的字节数组为空,则抛出ClassNotFoundException
异常;否则,使用defineClass()
方法将字节数组转换为 Class 对象,并返回该对象。
在getClassData()
方法中,根据传入的类名生成类文件路径,并使用FileInputStream
将类文件读入字节数组中。
使用:
public class CustomClassLoaderTest { public static void main(String[] args) throws Exception { // 创建自定义类加载器,指定类文件所在的目录 CustomClassLoader classLoader = new CustomClassLoader("F:\\classes"); // 使用自定义类加载器加载 Hello 类 Class<?> clazz = classLoader.loadClass("com.example.something.Hello"); Object obj = clazz.getDeclaredConstructor().newInstance(); System.out.println(obj); } }
基于 URLClassLoader
代码:
public class CustomURLClassLoader extends URLClassLoader { public CustomURLClassLoader(URL[] urls, ClassLoader parent) { super(urls, parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { // 调用父类 loadClass 方法进行委托加载 Class<?> clazz = super.findClass(name); return clazz; } catch (ClassNotFoundException e) { // 如果父类无法加载,则尝试在 URL 中加载 byte[] data = getClassData(name); if (data == null) { throw new ClassNotFoundException(); } else { // 使用 defineClass 方法将 byte 数组转换为 Class 对象 return defineClass(name, data, 0, data.length); } } } private byte[] getClassData(String className) { String path = className.replace('.', '/') + ".class"; URL[] urls = getURLs(); for (URL url : urls) { try { URL classUrl = new URL(url, path); // 使用 URLConnection 检查类文件是否存在 try (InputStream is = classUrl.openStream(); ByteArrayOutputStream os = new ByteArrayOutputStream()) { byte[] buffer = new byte[1024]; int length; while ((length = is.read(buffer)) != -1) { os.write(buffer, 0, length); } return os.toByteArray(); } } catch (IOException e) { // ignore and try next URL } } return null; } }
说明:
上述代码继承了 `URLClassLoader` 类,并重写了其中的 `findClass()` 方法,实现先尝试使用父类加载器进行加载,如果无法加载,则尝试使用 URL 加载类文件的功能。在 `getClassData()` 方法中,会遍历 `URLClassLoader` 中定义的 URL,检查类文件是否存在,并返回类文件的字节数组,如果无法找到类文件,则返回 `null`。
使用:
public class CustomURLClassLoaderTest { public static void main(String[] args) throws Exception { // 创建 URL 数组,指定类文件所在的 URL URL[] urls = { new URL("file:F:\\classes") }; // 创建父类加载器,使用系统类加载器 ClassLoader parent = ClassLoader.getSystemClassLoader(); // 创建自定义 URL 类加载器 CustomURLClassLoader classLoader = new CustomURLClassLoader(urls, parent); // 使用自定义 URL 类加载器加载 Hello 类 Class<?> clazz = classLoader.loadClass("com.example.something.Hello"); Object obj = clazz.getDeclaredConstructor().newInstance(); System.out.println(obj); } }
双亲委派
加载器那么多,我具体是哪个类进行加载得呢?双亲委派机制告诉我答案.
定义
双亲委派是一种Java类加载器的工作机制,它将类加载请求委派给父类加载器,直到顶级系统类加载器。基本思想是,除非有特殊需求,否则所有类的加载任务都应该由父类加载器完成,从而保证Java核心库的类型安全和稳定性,并防止恶意代码的自行布置。如果一个类没有在父类加载器中被发现,子类加载器才会尝试加载该类。这种类加载器之间的父子关系被称为“双亲委派模型”.
如图:
意义
为什么通过双亲委派进行加载呢?
- 避免重复加载
- 提高安全性
- 维护Java平台的一致性
- 代码优化
Linking
加载过后,我是否就可以被使用了呢?答案是否定的,我还要经历Lingking 阶段,包括Verification、Preparation 和 Resolution。
Verification(验证)
在验证阶段,Java虚拟机会进行语法与语义的检查,以保证class文件的完整性和正确性,同时保证被加载的class与虚拟机的版本兼容。主要的检查内容包括文件格式、字节码语义、符号引用等。
Preparation(准备)
在准备阶段,Java虚拟机会为类变量分配内存,并且赋予初始值。如果类变量包含有静态变量,那么这时也会初始化静态变量。因此,在这个阶段,类变量所使用的空间已经被分配,将其设置为默认初始值即可。
Resolution(解析)
在解析阶段,将类或接口中的符号引用转化为直接引用的过程。在 Java 虚拟机加载类时,符号引用是一种指向常量池中某个符号的引用,而直接引用则是指向内存中某个位置的直接指针。解析阶段可以理解为是在解决类之间的依赖关系,使各个类之间可以像使用自身成员一样使用别的类中的成员。
初始化
在验证、准备和解析后,我还要经过初始化,才能被使用。
定义
初始化是指在类加载过程的最后一步,JVM要对类进行一些初始化的操作,确保类可以安全地使用。在这个阶段,往往包括静态变量显式赋值和静态代码块执行。
内容
静态变量显式赋值
当类加载器完成类的加载、验证、准备后,在初始化阶段,JVM对类的静态变量进行显式赋值。如果类定义了多个静态变量,JVM会按照代码中声明的顺序进行初始化,并且若发现此过程需要访问到其他未初始化的类,JVM会先完成这些类的初始化。
静态代码块的执行
除了静态变量的显式赋值,类的静态代码块也会在初始化阶段执行。当JVM执行类加载的Initializing阶段时,会执行类中所有静态代码块的内容,如果类中没有定义静态代码块,则不执行。这个过程一般用于在使用之前对类进行初始化。
接口初始化
当一个类在初始化时,如果发现其父类还未进行初始化,JVM会先对其父类进行初始化。如果该类实现了接口,也会对这个接口进行初始化操作,接口的初始化过程和类一样,都会进行静态变量显式赋值及静态代码块执行,同时还会检查接口中的所有静态方法。
功能实现
初始化之后,我才真正的进入JVM中,其它小伙伴需要我的时候,只需要创建我的实例,就可以使用我的功能了,得到我帮助得小伙伴都很感谢我。
GC
在JVM中我过得很开心,也留下了很多足迹。在我走后,如何让我得足迹不对其他小伙伴有影响呢?GC可以帮我解决这个问题。
定义
GC(Garbage Collection)是JVM提供的垃圾回收机制。在Java中,对象是动态分配的,内存是由JVM自动管理,而不是由程序员手动分配和释放。当一个对象不再被程序引用时,就应该由垃圾回收器回收其占用的内存,这样可以防止内存泄漏和提高内存的。
小结
通过我的旅行,你知道JVM是怎么加载一个类的了么?我们通过加载、Linking、初始化和使用等各个阶段,将Java类完整地载入内存并执行其中定义的方法和变量。这个过程中,每个阶段都扮演着不同的角色,并为类的正常运行提供了必要的支持。
作者:京东物流 陈昌浩
来源:京东云开发者社区 自猿其说Tech 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
完蛋!我被 Out of Memory 包围了! | 京东云技术团队
是极致魅惑、洒脱自由的Java heap space? 是知性柔情、温婉大气的GC overhead limit exceeded? 是纯真无邪、活泼可爱的Metaspace? 如果以上不是你的菜,那还有…… 刁蛮任性,无迹可寻的CodeCache! 性感火辣、心思细腻的Direct Memory 高贵冷艳,独爱你一人的OOM Killer! 总有一款,能让你钟情!BUG 选择权,现在交由你手! Java heap space 这是最常见的一个 OOM 问题了,谁还没经历过一个 Heap OOM呢? 当堆内存被塞满之后,一边 GC 无法及时回收,一边又在继续创建新对象,Allocator 无法分配新的内存之后,就会送一个 OOM 的错误: java.lang.OutOfMemoryError: Java heap space 分析解决起来无非是那几步: dump 堆内存 通过 MAT、YourKit、JProfiler 、IDEA Profiler 等一系列工具分析dump文件 找到占用内存最多、最大的对象,看看是哪个小可爱干的 分析代码,尝试优化代码、减少对象创建 增加 JVM...
- 下一篇
体验问题哪里找?点击链接获取答案~ | 京东云技术团队
用户体验(User Experience,简称UX)是指用户在与产品、系统或服务进行交互的过程中所产生的主观感受和情感反应。它涵盖了用户对产品或服务的使用过程中所体验到的方方面面,包括界面设计、交互流程、功能性、易用性、可靠性、响应速度、视觉美感等。 我们为什么需要注重“用户体验”呢?引用《用户体验要素》中一句话:“用户体验就是商机”。良好的用户体验可以提高用户的满意度、提高转化率并直接关系到品牌形象和口碑,进一步赢得竞争优势。基于此,从运营、业务到产品、研发、测试都须开始自查自纠,提升用户满意度。 根据近期APP“体验5分钟”的报告,测试人员可以从以下几个方面寻找用户体验问题: 1、安全与合规:根据广告法要求:广告应当真实、准确,不得含有误导性信息。比如模糊关键信息诱导用户点击,或者弱化解绑银行卡、注销账号入口等都不被允许。同时约束默认勾选,包括协议、消息提醒、同步加购、同步产品开通。以个人实名页、资产页为例,用户隐私类信息也不应该支持分享,降低平台安全感。 2、过度营销:在推广和营销活动中过分频繁、过于侵入性、过度夸张或过度推销的行为,这些行为会引起用户的反感。此外,如果营销活动中...
相关文章
文章评论
共有0条评论来说两句吧...