公司网站建设高端网站建设网页设计,三亚做网站多少钱一平方,山东省住房和城乡建设厅举报电话,办公室装修效果图图片大全文章目录简明易懂的JVM和GC理解写在前面Java虚拟机(JVM)的组成基本介绍结构类加载子系统(ClassLoader SubSystem)介绍类加载过程类加载过程小结双亲委派模型(Parent-Delegation Model)简介优点Java9的类加载的委派关系变动双亲委派模型小结运行时数据区(Runtime Data Areas)介绍…
文章目录简明易懂的JVM和GC理解写在前面Java虚拟机(JVM)的组成基本介绍结构类加载子系统(ClassLoader SubSystem)介绍类加载过程类加载过程小结双亲委派模型(Parent-Delegation Model)简介优点Java9的类加载的委派关系变动双亲委派模型小结运行时数据区(Runtime Data Areas)介绍程序计数器 (Program Counter Register)程序计数器小结虚拟机栈 (JVM Stacks)虚拟机栈小结本地方法栈Native Method StacksJava堆 (Java Heap)方法区方法区小结直接内存直接内存小结执行引擎 (Execution Engine)介绍执行引擎的结构解释器(Interpreter)即时编译器(Just In Time Compiler)中间代码生成器分析器(Profiler)垃圾收集器(Garbage Collerctor)执行引擎的结构简明总结参考文献简明易懂的JVM和GC理解
写在前面
写这篇文章是为了用简明易懂的写法尽可能的在较短的篇幅内写出对Java虚拟机和内存垃圾策略的理解。解析Java虚拟机和GC策略的文章很多有些讲的还很深入。但是对平时不常接触Java虚拟机和GC策略的人来说有些过于晦涩了概念很多层次复杂既不方便理解也难以记忆。作者也有这方面难题对Java虚拟机有一定了解但在面试等场合很难有条理的讲解清楚因此用此文以简洁的写法予以呈现。目标就是读完此文后对JVM和GC有个简明的全局的有序的理解对面试时一些八股文的问题可以有条理的解答。
需要快速了解的可以只看章节介绍和章节小结。
本文参考多个文章已在结尾予以注明需要更详细内容的可以通过链接学习。
Java虚拟机(JVM)的组成
基本介绍
Java虚拟机(JVM)是什么官方解释JVM 是 Java Virtual Machine 的缩写它是一个虚构出来的计算机一种规范。通过在实际的计算机上仿真模拟各类计算机功能实现... 作者的理解Java虚拟机就是运行Java程序的基础环境开发人员编写的Java程序并不直接在操作系统上运行而是交由JVM运行每个Java应用程序启动都是先调用启动了一个JVM环境然后由JVM环境再去执行用户编写的Java程序。这也是Java得以跨平台运行的秘密Java虚拟机对用户屏蔽了不同操作系统乃至硬件架构的区别用户只需适配Java JVM自己的规范即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fL5e0XmZ-1676968052619)(link “https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/d947f91e44c44c6c80222b49c2dee859-new-image19a36451-d673-486e-9c8e-3c7d8ab66929.png”)]
对于现代程序员JVM的概念应该很好理解。现在主流语言都有类似的虚拟机运行时结构比如C#有.net CLR,javascript有nodejsgo有go runtime等。 而且有许多直观的实例能作为对比譬如在windows-PC电脑上玩安卓手游需要下载安卓模拟器可以简单的认为安卓模拟器就是充当了一个安卓程序Java程序在PC上跨平台运行的运行时虚拟机没有这个虚拟机则为安卓开发的程序不可能在windows-PC上跑起来。
结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-giNGxgOv-1676968052619)(link “https://img-my.csdn.net/uploads/201209/29/1348934141_8447.jpg”)]
如上面的架构图所示JVM分为三个主要子系统
类加载子系统运行时数据区执行引擎
类加载子系统(ClassLoader SubSystem)
介绍
上面说了每个Java程序需要启动一个JVM环境来运行用户的Java程序那么用户的程序是如何被Java虚拟机识别的呢直接看用户写的程序也不过是英文的文本文件。JVM 是不认识文本文件的它只能识别按其规范格式生成的二进制文件。
简单来说用户的程序需要先被编译成二进制的字节码文件javac编译器便是这个步骤的主导而JVM加载识别的也是字节码文件。比如一个用户编写的HelloWorld.java程序javac编译器将其编译为二进制的HelloWorld.class文件JVM再读入此字节码文件编译在这里不详谈。
JVM接收字节码文件后由类加载子系统加载字节码文件它就像一个搬运工一样会把所有的.class 文件全部搬进 JVM 里面来并识别为JVM可运行的数据结构那这些数据储存在哪里呢 就是储存在下一节详谈的运行时数据区。
官方的说法类加载子系统动态加载连接类信息并在运行时而非编译时首次引用类时初始化类文件。
可以看到类加载子系统可以动态的加载类所以Java程序中的类并不是程序启动后就一成不变的而是可以根据需要动态的加载和卸载类。Java动态代理Spring事务AOP切面编程都是利用这一特性实现其功能的。在程序初始化时加载器会加载尽可能少的类资源只有当它们真正需要的时候才进行加载。这不仅缩短了程序初始化时间而且减少了运行程序消耗的资源。
类加载过程
类加载过程加载-连接-初始化。连接过程又可分为三步验证-准备-解析 加载 应用程序的全部Java类都由类加载器加载到Java虚拟机之中JVM类加载子系统提供了三个类加载器。启动类加载器平台/扩展 类加载器应用程序类加载器。 三个类加载器是存在优先级层次结构的其按层次去执行加载类的任务这一按层次优先级加载的过程被称为双亲委派模型是一个常见的八股文考点。 启动类加载器 BootStrap ClassLoader 最顶层的类加载器 有许多文章介绍启动类加载器 BootStrap ClassLoaderC实现负责加载 %JAVA_HOME%/lib目录下的 jar 包和类或者被 -Xbootclasspath参数指定的路径中的所有类。 这个介绍对Java9之前的JVM类加载器系统都适用但是请注意在Java9版本时Java官方更改了JDK的类库结构使用JPMS模块化系统替换了JDK中原有的类库结构所以启动类加载器并不再时简单的加载%JAVA_HOME%/lib目录下的 jar 包了而是根据规则加载JDK系统库目录下指定的模块jar包。 并且Java9之后启动类加载器现在是在JVM内部和Java类库共同协作实现的类加载器(不再是C实现)但是为了与之前代码兼容在获取启动类加载器的场景中仍然会返回null而不会得到BootClassLoader实例。 简单来说就是加载Java虚拟机系统的基础类库的类加载器。 平台/扩展 类加载器 Platform/Extension ClassLoader 中层的类加载器上级为启动类加载器 有许多文章介绍扩展类加载器Extension ClassLoader主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类或被 java.ext.dirs 系统变量所指定的路径下的 jar 包。 同样的,这个介绍对Java9之前的JVM类加载器系统都适用而在Java9版本中这个类加载发生了重大变化。扩展机制被移除了扩展类加载器由于向后兼容性的原因被保留不过被重命名为平台类加载器(platform classloader).可以通过ClassLoader的新方法getPlatformClassLoader()来获取。同样需要负责根据规则加载JDK系统库目录下指定的模块jar包。 简单来说就是加载Java虚拟机系统的扩展类库的类加载器。 应用程序类加载器 Application ClassLoader 面向我们用户程序的加载器上级为平台/扩展 类加载器 又称为系统类加载器(System ClassLoader) 负责加载当前应用classpath下的所有jar包和类。 同样的在Java9版本中此类加载器也不仅仅负责加载用户程序的类信息同样需要负责根据规则加载目录下指定的模块jar包。 除了以上三个JVM提供的类加载器还有两种类加载器。 线程上下文类加载器 ThreadContext ClassLoader 很特殊的一种类加载器相比启动类加载器平台/扩展 类加载器应用程序类加载器以上三种它们是真实存在的类而且遵从双亲委派模型。而线程上下文类加载器其实只是一个概念。线程上下文类加载器的作用是为了破坏Java类加载委托机制使程序可以逆向使用类加载器。建议先阅读双亲委派模型 Java中的类加载机制是双亲委派模型即按照AppClassLoader → ExtendClassLoader → BootstrapClassLoader 的顺序子ClassLoader将一个类加载的任务委托给父ClassLoader父ClassLoader会再委托给父的父ClassLoader来完成只有父ClassLoader无法完成该类的加载时子ClassLoader才会尝试自己去加载该类。所以越基础的类由越上层的ClassLoader进行加载但如果基础类又要调用回用户的代码那该怎么办为了解决这个问题Java设计团队只好引入了一个不太优雅的设计Thread ContextClassLoader线程上下文类加载器 Java服务提供者接口Service Provider InterfaceSPI机制就存在以上场景。Java服务提供者接口机制允许Java核心库只定义接口而不定义实现在用户提供的包中提供实现类作为 Java 应用所依赖的 jar 包被包含进类路径CLASSPATH里。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。 既然SPI接口是在Java核心库中定义的则当然由启动类加载器加载类信息。而用户提供的包中提供实现类在用户类路径中被加载必然是用户类加载器加载类信息。根据Java规范一个类由类加载器A加载,那么这个类依赖类也应该由相同的类加载器加载。作为加载SPI接口的启动类加载器无法加载 SPI 的实现类的因为它限定了只能加载 Java 的核心库并不能加载到放在用户目录的SPI实现类。它也不能代理给应用类加载器因为它是应用类加载器的祖先类加载器已经是最顶层的类加载了不能委托给其他类加载器。也就是说双亲委派模型无法解决这个问题。 线程上下文类加载器通过线程类 java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)来获取和设置线程的上下文类加载器。如果你整个应用中都没有对此作任何处理那么 所有的线程都会以应用类加载器作为线程的上下文类加载器。 在线程中运行的代码可以通过此类加载器来加载类和资源。 用户自定义类加载器 除了上面3个JVM提供的类加载器之外程序员还可以自己定义类加载器。显然用户自定义类加载器也不是一个具体的类而是一个概念一种类加载器的统称。 自定义类加载器是为了实现一些特殊目标譬如隔离加载类修改类加载模式扩展加载源防止源码泄漏等。譬如Apachetomcat,阿里云Pandora框架就使用了自定义类加载器实现了隔离加载类等功能。 连接 验证 字节码验证程序将验证生成的字节码是否正确如果验证失败我们将收到验证错误。准备 将为所有静态变量分配内存并为其分配默认值。解决 将所有符号内存引用替换为“方法区域”中的原始引用。 初始化 这是类加载的最后阶段在此所有静态变量将被分配原始值并且将执行类的静态块(static标签包围的部分)。
类加载过程小结
类加载过程包含三个步骤加载-连接-初始化。
加载类信息由三个存在优先级的类加载器执行分别为提供了的三个类加载器启动类加载器平台/扩展 类加载器应用程序类加载器。
除了三个存在优先级的类加载器外还有两种特殊的类加载器线程上下文类加载器和用户自定义类加载器。线程上下文类加载器目的是支持Java 服务扩展SPI机制实现逆向使用类加载器是一种概念而不是实体。用户自定义类加载器目的是实现用户自定义的特殊类加载能力。
类加载器是32组合三个JVM提供的2种特殊的。
双亲委派模型(Parent-Delegation Model)
常见八股考点
双亲委派模型Parent-Delegation Model其实是个错译因为类加载器里并没有双亲如父亲类或者母亲类只有一个“单亲”即parent亲级加载器或者说上级加载器。因此翻译成优先级委派模型或者上级委派模型比较合适目前只能说将错就错吧
简介
每一个Java类都有一个对应它的类加载器。类加载器是通过类的全限定名或者说绝对路径来找到一个class文件。
1.JVM在类加载时会默认使用双亲委派模型。即在类加载的时候系统会首先判断当前类是否被加载过。已经被加载的类会直接返回否则才会尝试加载。2.加载的时候首先会把该请求委派给父类加载器的 loadClass() 处理因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。3.当父类加载器无法处理时才由自己来处理。当父类加载器为 null 时会使用启动类加载器 BootstrapClassLoader 作为父类加载器。4.如果最底层类加载器仍然没有找到所需要的class文件则抛出异常。(即java.lang.ClassNotFoundException错误)
优点
确保类的全局唯一性。
可以避免类的重复加载JVM 区分不同类的方式不仅仅根据类名相同的类文件被不同的类加载器加载产生的是两个不同的类。
如果你自己写的一个类与核心类库中的类重名会发现这个类可以被正常编译但永远无法被加载运行。因为你写的这个类不会被应用类加载器加载而是被委托到顶层被启动类加载器在核心类库中找到了。如果没有双亲委托机制来确保类的全局唯一性谁都可以编写一个java.lang.Object类放在classpath下那应用程序就乱套了。
从安全的角度讲通过双亲委托机制Java虚拟机总是先从最可信的Java核心API查找类型可以防止不可信的类假扮被信任的类对系统造成危害。
Java9的类加载的委派关系变动
当平台以及应用程序类加载器收到类加载的请求的时候在委派给父类加载器之前要先判断该类是否能够归属到某一个系统模块中如果可以找到这样的归属关系就要优先委派给负责该模块的加载器完成加载。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aIA6yuou-1676968052619)(link “https://upload-images.jianshu.io/upload_images/23797822-e925eb9e9a715678.png?imageMogr2/auto-orient/strip|imageView2/2/w/1062/format/webp.webp”)]
双亲委派模型小结
Java中的类加载机制是双亲委派模型即按照AppClassLoader → SystemClassLoader → BootstrapClassLoader 的顺序子ClassLoader将一个类加载的任务委托给父ClassLoader父ClassLoader会再委托给父的父ClassLoader来完成只有父ClassLoader无法完成该类的加载时子ClassLoader才会尝试自己去加载该类。越基础的类由越上层的ClassLoader进行加载。
双亲委派模型可以确保类的全局唯一性保证Java程序运行的安全。
运行时数据区(Runtime Data Areas)
介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pYq2qQOp-1676968052620)(link “https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL2xlaXl1MTk5Ny9QaWNCZWRAbWFzdGVyL2Jsb2dzL3BpY3R1cmVzL2phdmElRTUlODYlODUlRTUlQUQlOTglRTclQkIlOTMlRTYlOUUlODQucG5n?x-oss-processimage/format,png”)]
运行时数据区顾名思义就是JVM在运行一个Java程序时存储运行时所需的各种数据的区块。 上节讲了类加载子系统将类加载到JVM后就存储在运行时数据区所以运行时数据区也必然存在存储类信息的区块这一区块即方法区。其他还有存储共享变量的堆区代码运行时存放局部变量方法内容的栈区等等。Java虚拟机在执行过程中会将所管理的运行时数据区内存划分为不同的区域有的随着线程产生和消失有的随着Java进程产生和消失根据《Java虚拟机规范》的规定运行时数据区分为以下数个区域:
程序计数器 (Program Counter Register)虚拟机栈 (JVM Stacks)Java堆 (Java Heap)方法区 (Method Area)直接内存 (Direct Memory)
程序计数器 (Program Counter Register)
程序计数器又称之为程序计数寄存器。 就是当前线程所执行的字节码的行号指示器通过改变计数器的值来选取下一行指令通过他来实现跳转、循环、恢复线程等功能。 在任何时刻一个处理器内核只能运行一个线程多线程是通过线程轮流切换分配时间来完成的这就需要有一个标志来记住每个线程执行到了哪里这里便需用到程序计数器。每个处理器内核执行指令时都会同时修改程序计数器中的下一个指令号所以即便发生了线程切换当前线程被休眠此时程序计数器中记录的是休眠时所执行的指令的下一个指令计数号。等处理器内核重新唤醒此线程时仍能通过程序计数器从线程休眠的位置继续执行。
所以程序计数器是线程私有的每个线程都有自己的程序计数器。 程序计数器也是运行时数据区唯一不会出现内存溢出故障的区块。 想想也能理解存储下一条指令号所需要的内存空间是固定的在创建每个线程时已经预先分配好的情况下自然不会出现溢出。 注意这不代表创建新的线程时不会出现内存溢出只不过此时发生的是虚拟机栈溢出。
程序计数器小结
程序计数器其实是个计算机组成原理中的概念处理器从程序计数器中取指令执行并改变程序计数器使指向其下一条指令的计数号而Java虚拟机使用其作为线程的指令计数器。在Java虚拟机中只需要记住线程私有储存下个指令号不会内存溢出即可。
虚拟机栈 (JVM Stacks)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ed4H0MG6-1676968052620)(link “https://imgconvert.csdnimg.cn/aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL2xlaXl1MTk5Ny9QaWNCZWRAbWFzdGVyL2Jsb2dzL3BpY3R1cmVzLyVFOCU5OSU5QSVFNiU4QiU5RiVFNiU5QyVCQSVFNiVBMCU4OC5wbmc?x-oss-processimage/format,png”)]
虚拟机栈就是日常所说的运行时栈虚拟机栈是线程私有的随线程生灭。虚拟机栈描述的是线程中的方法的内存模型。说人话就是方法真正执行时的数据存储区域方法中的各步骤如何执行每一个指令执行后的结果储存都是在虚拟机栈中实现。虚拟机栈是线程私有的随线程生灭。
对于每个线程都会创建一个当前栈。当线程中的每个方法被执行的时候都会在虚拟机当前栈中同步创建一个栈帧(stack frame)栈帧保存的方法内容是从方法区中该方法的元数据复制而来当方法执行结束后该栈帧取消。这一过程可以称之为虚拟机栈的入栈和出栈。正在被线程执行的方法称为当前线程方法而该方法的栈帧就称为当前帧执行引擎运行时只对当前栈帧有效。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FmTg7Y6e-1676968052620)(link “https://img-blog.csdnimg.cn/20191211183241890.png?x-oss-processimage/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3UwMTA4ODYyMTc,size_16,color_FFFFFF,t_70”)]
每个栈帧的包含如下的内容
局部变量表 局部变量表中存储着全部的方法本地域内的局部变量java基本数据类型byte/boolean/char/int/long/double/float/short为值变量对象变量则为引用操作数栈 入栈、出栈、复制、交换、产生消费变量实际实现当前线程调用过程Java虚拟机的解释执行引擎称为“基于栈的执行引擎”其中所指的“栈”就是操作数栈。解释执行时对方法局部变量的操作就是在操作数栈中进行动态链接 每次运行时动态链接的符号引用方法返回地址 顾名思义方法退出时从此地址返回上个栈帧的位置
Java虚拟机栈就是每个线程运行时的数据存储区。编程时的方法调用类似方法套方法A方法调用B方法B方法再调用C方法像一个糖葫芦一样串起来。线程执行时自然执行到方法调用位置需要进入被调用的方法待被调用的方法执行完毕后才能继续执行原有的代码此时明显是一个后执行的方法先出结果的结构自然想到后进先出的栈结构。 所以线程执行到方法调用的位置会创建一个新的入栈对象栈帧用来存储被调用的方法数据然后放入线程的运行栈当前栈中。执行引擎执行的就是当前帧待被调用方法当前帧执行完毕栈帧出栈被调用方法退出。
虚拟机栈可能会抛出两种异常
如果线程请求的栈深度(包括当前栈和操作数栈)大于虚拟机所规定的栈深度则会抛出StackOverFlowError即栈溢出如果虚拟机的栈容量可以动态扩展那么当虚拟机栈申请不到内存时会抛出OutOfMemoryError即OOM内存溢出
虚拟机栈小结
一个方法执行时肯定有它的本地变量包括入参出参中间值等所以栈帧有本地变量表。方法中各个变量通过运算改变运算在计算中就是取值运算返回结果这一过程需要在栈中进行所以栈帧有操作数栈。
方法中许多变量所指向的方法在程序初始化时不一定存在前面说了类是动态加载的所以栈帧中存在很多符号引用在运行时通过链接到方法区中的运行时常量池查找到引用的类方法的真正内存地址
方法退出之后都需要返回到方法被调用的位置程序才能继续执行。方法返回地址就是记录的上层方法的返回位置
虚拟机栈会发生内存溢出、栈溢出两种异常
本地方法栈Native Method Stacks
本地方法栈与虚拟机栈的作用是相似的,都会抛出OutOfMemoryError和StackOverFlowError都是线程私有的主要的区别在于 虚拟机栈执行的是java方法 本地方法栈执行的是机器原生(native)方法
Java堆 (Java Heap)
Java堆内存,又称对象堆。是虚拟机管理的内存中最大的一块也是垃圾回收最频繁的一块区域。我们程序所有的对象实例都存放在堆内存中。当然由于java虚拟机的发展堆中也多了许多东西不止局限于Java对象现在主要有 对象实例 类初始化生成的对象基本数据类型的数组也是对象实例 字符串常量池 字符串常量池原本存放于方法区jdk7开始放置于堆中。字符串常量池存储的是String对象的直接引用而不是直接存放的对象是一张String Table 1、字符串的分配和其他的对象分配一样耗费高昂的时间与空间代价作为最基础的数据类型大量频繁的创建字符串极大程度地影响程序的性能。 2、JVM为了提高性能和减少内存开销在实例化字符串常量的时候进行了一些优化。 1为字符串开辟一个字符串常量池类似于缓存区。 2创建字符串常量时首先检查字符串常量池是否存在该字符串。 3存在该字符串返回引用实例不存在实例化该字符串并放入池中。 静态变量 静态变量是有static修饰的变量jdk7时从方法区迁移至堆中 线程分配缓冲区Thread Local Allocation Buffer 线程私有但是不影响java堆的共性增加线程分配缓冲区是为了提升对象分配时的效率
Java堆既可以是固定大小的也可以是可扩展的通过参数-Xmx和-Xms设定如果堆无法扩展或者无法分配内存时也会报OutOfMemoryError即OOM内存溢出
方法区
方法区是网上文章比较混乱的部分有的文章也提到了不同的实现如永久带实现元空间实现一般都比较混乱没头没尾的。比如为什么叫实现实现了什么怎么还有不同的实现
具体是这样的方法区是【Java虚拟机规范】规定的储存Java类数据信息的内存空间但是【Java虚拟机规范】只规定了标准并没有要求怎么做到。所以不同的虚拟机有不同的实现不关注Java虚拟机的同学可能不清楚但确实Java虚拟机是有很多种实现的过去有的三大免费商用虚拟机实现Sun HotSpot,BAE JRockit,IBM J9和收费的商用虚拟机实现Azul Zing。随着Java8版本以后OpenJDK的快速发展又诞生了一些各式的虚拟机,例如阿里集团自用的aliJDK(龙井JDK)。所以这一节的内容是区分不同虚拟机的。
最常用的Sun/Oracle的HotSpot虚拟机的方法区在Java8版本之前是用永久带(PermSize)这一结构实现的放在JVM内存中受JVM永久带大小参数的限制。
随着Oracle收购了Sun然后又收购了BAE的JRockit虚拟机Oracle决定在Java8版本时将JRockit和HotSpot两种虚拟机合并而JRockit虚拟机是没有永久带的。因此在Java8的HotSpot虚拟机实现中才移除了永久带转而用JRockit的元空间结构来实现【Java虚拟机规范】的方法区标准。这也是很多文章中说的Java7到Java8不同的方法区实现但这仅针对HotSpot虚拟机。例如IBM/Eclipse OpenJ9虚拟机就根本没有元空间这种结构J9虚拟机的方法区是放置于Java对象堆上的。
HotSpot虚拟机使用元空间(Meta Space)这一结构实现的方法区直接放存储到本地内存中不受JVM内存大小参数的限制当然如果物理内存被占满了或者达到JVM启动参数标定的元空间最大值方法区也会报OOM并且将原来放在方法区的字符串常量池和静态变量都转移到了Java堆中方法区与其他区域不同的地方在于方法区在编译期间和类加载完成后的内容有少许不同不过总的来说分为这两部分
类元信息Klass 类元信息在类编译期间放入方法区里面放置了类的基本信息包括类的版本、字段、方法、接口以及常量池表Constant Pool Table类文件常量池表Constant Pool Table存储了类在编译期间生成的字面量、符号引用这些信息在类加载完后会被解析到运行时常量池中 字面量 Java语言中定义的常量如使用final修饰的值符号引用 表示JVM定义的Java关键字或基本类型与实际结构转换关系 运行时常量池Runtime Constant Pool 运行时常量池主要存放在类加载后被解析的字面量与符号引用就是类被JVM加载后在JVM中的版本。 常量池只有类文件在编译的时候才会产生而且是存储在类文件中的。而运行时常量池是在方法区而且可在JVM运行期间动态向运行时常量池中写入数据。
方法区中的类元信息就是类加载器加载进JVM内存的类原始数据结构可以认为仅为文本类型的定义信息。类文件常量池表通称常量池保存了Java类的字面量和符号引用可以认为是保存了类和变量类和类之前的关系。 运行时常量池才是类被JVM加载后在JVM中可用的数据虚拟机栈调用的类实际上是从运行时常量池中加载的。运行时常量池中的类数据已经分配了字面量符号引用已经替换为类在内存中的真实地址这时一个类才能被执行。
方法区会产生内存溢出OOM异常在HotSpot虚拟机中当元空间加载的类过多时如果无法再分配内存空间就回溢出。
方法区小结
针对HotSpot虚拟机方法区的实现由Java8版本之前的永久带(PermSize)转为Java8及之后的元空间实现(Meta Space)。元空间包括类元信息类文件常量池表运行时常量池三个部分。方法区会产生内存溢出。
直接内存
直接内存位于本地内存不属于JVM内存又称之为堆外内存当然看了上面内容后可知对堆外内存这个名称也并不准确。直接内存就是操作系统的原生内存(native堆)使用直接内存的原因就是为了提升性能因为减少了JVM堆内存IO操作时从操作系统空间复制(native堆)到内存空间(JVM堆)的IO损耗。
可以使用Java内部库Unsafe来操作直接内存此时直接内存的使用比较类似C语言C加中的本地内存分配需要程序开发者管理内存的分配和销毁。当然这样做也非常危险指针越界可不是闹着玩的。
在Java4(JDK1.4)中加入了NIONew Input/Output类它可以使用native函数直接分配堆外内存然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作这样可以在一些场景下大大提高IO性能避免了在java堆和native堆来回复制数据。
如Netty等一些框架就使用了通过DirectByteBuffer对象操作的存放在操作系统原生内存(native堆)的直接内存。通过NIO相关类管理的直接内存对象可以被JVM执行垃圾回收显然比Unsafe操作来的安全的多但是需要注意的是直接内存仅能在Full GC时被回收。譬如Netty框架就在使用后直接内存后显式调用System.gc()方法以促进Java虚拟机执行Full GC,如果此时虚拟机通过参数配置了禁用显式调用System.gc()则很有可能出现内存溢出异常。
直接内存小结
直接内存位于本地内存不属于JVM内存在物理内存耗尽的时候报OutOfMemoryError即OOM内存溢出异常
执行引擎 (Execution Engine)
介绍
分配给运行时数据区的字节码将由执行引擎执行。执行引擎读取字节码并逐段执行。
类加载系统负责装载字节码到Java虚拟机内部但字节码并不能够直接运行在操作系统之上因为字节码指令并非等价于本地机器指令它内部包含的仅仅只是一些能够被Java虚拟机所识别的字节码指令、符号表以及其他的辅助信息。
那么如果想让一个 java 程序运行起来执行引擎Execution Engine) 的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。
执行引擎所执行的字节码指令完全由执行时线程的程序计数器决定。每当执行一项指令操作以后程序计数器就会更新下一条需要被执行的指令地址。
当然方法在执行的过程中执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在 Java 堆区中的对象实例信息以及通过对象头中的元数据指针定位到目标对象的类型信息。
从外观上来看所有的 Java 虚拟机的执行引擎输入、输出都是一致的输入的是字节码二进制流处理过程是字节码解释执行的等效过程输出的是执行结果。
执行引擎的结构 解释器(Interpreter) 解释器解释字节码的速度较快但执行速度较慢。解释器的缺点是当多次调用一种方法时每次都需要新的解释。 即时编译器(Just In Time Compiler) – 即时编译器即JIT编译器它消除了解释器的缺点。执行引擎将使用解释器的帮助来转换字节码但是当发现重复的代码时它将使用JIT编译器该编译器将编译整个字节码并将其更改为本地代码。此本地代码将直接用于重复的方法调用从而提高系统的性能。 中间代码生成器 –产生中间代码代码优化器 –负责优化上面生成的中间代码目标代码生成器 –负责生成机器代码或本机代码。 分析器(Profiler) 一个特殊的组件负责查找热点即是否多次调用该方法。确定为热点的方法将被中间代码生成器,即时编译器编译为机器码提高执行效率。 垃圾收集器(Garbage Collerctor) 收集并删除未引用的对象。Java虚拟机中自动进行的内存垃圾回收就是由垃圾收集器(GC)自动管理垃圾回收可以通过调用触发System.gc()但不能保证执行。 Java本地接口JNI JNI将与本地方法库进行交互并提供执行引擎所需的本机库。 本地方法库 这是本地库的集合这是执行引擎所需的。
解释器(Interpreter)
解释器在运行时采用逐行解释字节码执行程序解释器真正意义上所承担的角色就是一个运行时“翻译者”将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。 解释器真正意义上所承担的角色就是一个运行时“翻译者”将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。 当一条字节码指令被解释执行完成后接着在根据 PC 寄存器中记录的下一条需要被执行的字节码指令执行解释操作。 在 Java 的发展历史里一共有两套解释执行器即古老的字节码解释器、现在普遍使用的模板解释器。 字节码解释器在执行时通过纯软件代码模拟字节码的执行效率非常低下。 而模版解释器将每一条字节码和一个模板函数相关联模板函数中直接产生这条字节码执行时的机器码从而很大程度上提高了解释器的性能。在 HotSpot VM 中解释器主要由解释执行(Interpreter) 模块和 本地指令缓存(Code) 模块组成。
解释执行(Interpreter) 模块实现了解释器的核心功能本地指令缓存(Code) 模块 用于管理 HotSpot VM 在运行时生成的本地机器指令。
即时编译器(Just In Time Compiler)
中间代码生成器
分析器(Profiler)
垃圾收集器(Garbage Collerctor)
执行引擎的结构
简明总结 参考文献
1.大白话带你认识 JVM
2.这可能是最清晰易懂的 G1 GC 资料
3.Java 基础JVM虚拟机结构