type
status
date
slug
summary
tags
category
icon
password
Property
Jan 13, 2025 02:24 AM
本文主要介绍JVM的内存模型与内存分布,并且从内部内存分部来分析垃圾回收原理

前言

Java是站在巨人的肩板上诞生的,Java是基于C++ & c语言编写实现的。Java程序依赖于各种Jar包组合,Jar包内主要包含:Javac(Java Compiler)编译后的二进制字节码文件与其他信息元文件。
Java的字节码文件不能直接应用在各种操作系统环境中,操作系统能执行的二进制指令文件,一般是可被内部执行器识别的比如各种常见文件名后缀的文档文件(.bat,.doc,.txt,.yaml等),二进制应用程序文件(.exe)。我以windows操作系统的习惯,介绍一下基本的流程:如果想执行某一类文件,系统需要你提供执行某类文件的关联应用程序,并且提供对应的解释器。比如我安装了SumatraPDF开源程序,系统在引导安装的时候,不会自动关联.pdf文件与该程序,需要自己手动修改属性关联,这就是第一个阶段,建立关联。我安装的SumatraPDF软件就是一个解释器,它能读取加载.pdf文件,所以当我双击某pdf文件的时候,操作系统根据我预设的文件关联,找到了对应的文件解释器,进而启动文件解释器,这就是一个文件执行的流程。
JVM也是如此,如果想在Windows环境下执行Jar,需要有程序能识别.jar,而java.exe就是一个入口,安装Java后,你可以对jar包进行文件关联,或者你在设置好全局系统环境变量后,也就是将java.exe可执行文件加入了全局环境变量,你可以在任意终端调用java指令,当你双击执行jar文件的时候,系统会根据你设置的文件关联,找到jar的文件解释器,这里的java.exe并不是最终的文件解释器,而是一个入口,java.exe启动后,根据默认执行参数,一般是-jar参数,或者你在终端中执行的时候也是java -jar xxx.jar的方式启动jar文件,java.exe会加载jvm.dll,dll是C++中的概念,称之为动态链接库,Dynamic Link library(DDL文件)。至此,启动jar程序,讲交由java.exe唤起的一系列dll程序来执行。
好的,开始正题。

第一阶段:类加载与执行

 
Jar是一种压缩包格式,你可以通过7Zip或者其他解压程序对其进行解压。Jar包内主要包含元数据文件,以及众多的.class字节码文件。引起,要想启动Jar文件,需要先解压读取对应的字节码文件,将文件以IO的形式从磁盘载入到内存中,这个过程就是加载的过程。
Jar包中除了.class字节码文件,还有项目的META-INF文件,其中,META-INF/MANIFEST.MF包含了对程序Main-Class的定义,也就是JVM根据此文件信息,找打了java程序执行的入口。执行Java程序,主要是jvm在起主要作用。
Java.exe会根据jvm.dll启动java虚拟机,由于虚拟机是动态链接库文件,其所依赖的大量的library都是基于C++实现的,因此,你会看到java的一些核心基类中,会有native修饰的方法,这些native翻译就是本地的意思,native修饰词是作用给jvm的,jvm知道native代表的是调用虚拟机内部使用C++编写的的依赖库。
在JDK8-HotSpot源码中,jvm虚拟机会启动sun.misc.Launcher实例对象。这里的misc是英文单词:miscellaneous,代表杂项,你可以根据此包底下包含的其他类可得知杂项的描述是准确的,这也符合java规范中对包名的命名要求。你会发现,在JDK17中,由于JPMS(Java Platform Module System)模块化机制,Launcher发生了重大变化,后续系列会继续讨论JDK17的变化。
目标jar的字节码文件加载进入内存后,JVM会启动bootstrapClassLoader实例,为C++实例,在Launcher实例对象启动后,会执行getLauncher方法,获取ClassLoader对象,返回AppClassLoader的实例。此过程中,使用同一种方式构造了两个类加载器,一个是扩展类加载器,一个是应用程序类加载器,其中,构造流程有一个Parent属性,意为父加载器,构造扩展类加载器的时候,父加载器是null,构造应用程序类加载器的时候,父加载器是扩展类加载器,最后使用返回的AppClassLoader实例(如下图所示)。这是第一个过程,获取加载器。
notion image
获取到目标加载器之后,开始执行loadClass。根据JDK8的源码,加载字节码的时候,不会立即加载,而是会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类(如下图所示)。
这么设计有两种好处,每次加载都委托父加载器加载,这样可以避免恶意改写的原字节码被应用程序加载器加载上了,相当于沙箱安全机制,防止核心基类被篡改;再就是如果父加载器已经load过了目标class字节码文件,那么就不会再需要当前加载器继续加载一遍了,可以避免重复加载进而确保一个字节码一个类只被加载一次。
notion image
当字节码文件进入内存之后,又继续进行了一系列操作,首先是验证,JVM需要验证当前字节码文件是否符合字节码规范,比如字节码文件头是否以cafe babe开头等等,当然不是这么简单,但是这里面的涉及到的具体的细节不是本文重点哈,进而如果通过验证,就会进入第三步,准备阶段,在准备阶段中,JVM会对静态变量进行默认值赋值,分配内存等操作,之后进入下一阶段,也就是解析阶段,解析阶段的作用是解析方法名到内存地址的阶段,不太规范,其实是解析字面量编码映射到静态链接和动态链接的过程,进而之后完成后,实现最后一步:执行初始化流程。本文主要讲解的是加载流程,后面还会有对内存分配的介绍,由于验证操作是在内存中使用verify.cpp进行实现的,将不作为次系列讨论的重点。
类加载进入内存之后,jvm会继续对内存中的字节码数据,进行验证操作,在Hotspot虚拟机中,执行的对应文件是/src/share/vm/classfile/verify.cpp文件,如下图所示:
notion image
 
2、双亲委派机制在类加载中的体现
引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等;
扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包;
应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
加载类永远不会自己先去加载,而是委托给父类进行加载,如果父类加载了就直接返回,不在重复加载;父类没有的才会向下交由下一级加载器加载,应用类加载器的父类不是扩展类加载器,只是源码中,有一个parent属性是扩展类加载器引导类加载器是在c++里面的;
双亲委派机制的主要优势小结:1、沙盒作用:防止篡改核心代码;2当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。这里还有一个全盘负责委托机制,所谓“全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。
 
3、如何打破双亲委派机制?
核心思想:Application类加载器加载自己的内容,其余内容交由引导类加载器与扩展类加载器加载.
 
4、关于实践操作打破双亲委派机制
后续文章会继续更新…
 
 
致谢:
💡
有关JVM类加载的问题欢迎与我交流~
 
 
相关文章
技术杂文(十一):你真的有理解计算机素养吗?技术杂文(十二):你真的了解Git的线性开发流程吗?
Loading...
fntp
fntp
多一点兴趣,少一点功利
最新发布
JUC核心篇(七):线程池底层原理
2025-2-26
JUC核心篇(六):阻塞队列
2025-2-24
JUC核心篇(四):CAS与AQS
2025-2-22
JUC技术篇(六):Volatile关键字
2025-2-21
JUC技术篇(五):Synchronized锁
2025-2-21
JUC核心篇(三):LockSupport与线程阻塞
2025-2-21
公告
📝 博客只为了记录我的学习生涯
😎 我的学习目标是成为一名极客
🤖 我热爱开源当然我也拥抱开源
💌 我期待能收到你的Email留言
📧 我的邮箱:stickpoint@163.com
欢迎交流~