JVM(运行时数据区结构)详解
JVM(Java虚拟机)
JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行,达到"一次编写,到处运行"的目的
为什么可以"一次编写,到处运行"
首先说明计算机只识别0、1序列组成的机器指令,而每一个平台上(可以理解为不同系统,Windows、Linux等)认识的0、1序列并不一样。这也就导致了你在Windows上编写的一段代码编译成指令为0101,当你拿到LInux系统上运行时,Linux识别出的可能是1010,那执行出的结果肯定也是不一样的,
而JVM又是怎样解决的呢?既然我Windows的指令0101到你Linux上变成1010,那就找一个翻译官当做中间桥梁进行‘翻译’,化解差别
我们的.java文件编译后成为字节码文件,也就是.class,你想要执行我的.class文件,那你就要安装JVM来将字节码翻译为相依与平台的计算机指令,当然不同的平台肯定是要安装适用自己的JVM。
JVM结构
程序计算器
当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与
此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指
令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令
这里放上一张图详细讲解程序计数器(pc寄存器)的作用,java源代码翻译成字节码以后对于字节码的执行肯定是有顺序的,而我们的cpu怎么会知道执行顺序,我们的程序计数器就是来记住字节码的执行顺序,可以看到图中的的字节码每一行前面都是有数字,可以把它当做是字节码内存中对应的地址,当执行完一行字节码时,解释器就会去程序计数器中获取下一次应该执行的字节码内存地址,将之转换成机器码供我们的cpu执行,同时程序计数器会存放下一次应该执行的字节码地址,以供解释器来获取。
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程分配一个程序计数器,生命周期与线程的生命周期保持一致,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况,也就是说程序计数器是属于**线程私有**
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地 址;如果正在执行的是本地(Native)方法,这个
计数器值则应为空(Undefined)。此内存区域(指程序计数器)是唯 一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况
的区域
可能有些人对于:如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)不太理解,解释:在程序计数器的定义中,程序计数器存放的是Java字节码的地址,而native(本地)方法的方法体是非Java的,所以程序计数器的值才未定义
那在native方法执行后,线程又如何确保下一次执行的位置?
这是因为每个Java线程都直接映射到一个OS线程上执行。所以native方法就在本地线程上执行,无需理会JVM规范中的程序计数器的概念。仔细看一下JVM规范,如果一个线程执行Native方法,程序计数器的值未定义,可不是一定为空,任何值都可以。native方法执行后会退出(栈帧pop),方法退出返回到被调用的地方继续执行程序。
执行Native方法,计数器为空参考文章:https://blog.csdn.net/weixin_41884010/article/details/103593628
虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期 与线程相同。每一个方法被执行
时虚拟机都会创建一个栈帧,将此栈帧放入栈中,称之为入栈,当方法执行完毕,此栈帧就会出栈,栈中通常用于存储局部变量表、操作数栈、动
态连接、方法出口等信息
用图片进行讲解进栈 出栈,可以看到程序先执行的是test1方法,所以在执行到test1方法的时候进栈了,而test1方法中又调用了test2方法,此时test2方法对应的栈帧也进栈了,而当方法执行完毕后,是test2对应的栈帧先出去,然后test1对应的栈帧出去,也就是先进后出,感兴趣的可以自己测试
如果线程请求的栈深度大于虚 拟机所允许的深度,将抛出*Error异常;如果Java虚拟机栈容量可以动态扩展,当栈扩 展时无法申请到足够的内存会抛出OutOfMemoryError异常也就是常说的OOM。面试会经常问,不过HotSpot虚拟机的栈容量是不可以动态扩展的所以不会出现OOM异常,不过如果栈空间申请不成功依旧会抛OOM异常。
本地方法栈
本地方法栈和虚拟机栈很相似,虚拟机栈是为执行java方法服务,而本地方法栈则是为Native(本地)方法服务,说的直白一些就是,Native
方法就是用java语言调用其他语言实现的方法(就是一个java调用非java代码的接口),例如C/C++实现的方法,与虚拟机栈一样,本地方法栈
也会在栈深度溢出或者栈扩展失败时分别抛出*Error和OutOfMemoryError异常
本地方法栈为线程私有,功能和虚拟机栈非常类似。本地方法被执行的时候,本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息
堆(Java Heap)
Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所 有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域
的唯一目的就是存放对象实例,而且常说的垃圾回收也是回收的堆内存,Java堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流
的Java虚拟机都是按照可扩展来实现的(通过参数-Xmx和-Xms设定)。如果在Java堆中没有内存完成实例分配,并且堆也无法再 扩展时,
Java虚拟机将会抛出OutOfMemoryError异常
对象出生到回收过程:
Minor GC:清理年轻代
Major GC:清理老年代
Full GC :清理整个堆空间
Eden(出生):
大多数情况下,对象会在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机会发起一次 Minor GC。通过Minor GC之后,Eden区中绝大部分对象会被回收,而那些存活对象,将会送到Survivor的From区(若From区空间不够,则直接进入Old区) 。
Survivor(缓冲)
Survivor区相当于是Eden区(年轻代)和Old区(老年代)的一个缓冲,由于对象的生命周期长短不一,有的可能使用时间长,有的短一些,所以用分代进行回收,Survivor又分为2个区,一个是From区,一个是To区。每次执行Minor GC,会将Eden区中存活的对象放到Survivor的From区,而在From区中,仍存活的对象会根据他们的年龄值来决定去向。(From Survivor和To Survivor的逻辑关系会发生颠倒(复制算法实现): From变To , To变From,目的是保证有连续的空间存放对方,避免碎片化的发生,后面会出文章讲)
当对象进入到Survivor区中,每执行一次Minor GC,对象的年龄都会加1(虚拟机给每个对象定义了一个对象年龄(Age)计数器),分界岭就是15次Minor GC,当对象经过第16次Minor GC之后则会到老年代,这样的好处就是导致了年轻代中不会执行一次或者几次Minor GC 对象就会到老年代中,这样的话,老年代内存就会迅速耗尽,从而执行Major GC, Major GC消耗的时间是要大于Minor GC的。
从回收内存的角度看,由于现代垃圾收集器(用来回收垃圾)大部分都是基于分 代收集理论设计的,所以Java堆中经常会出现“新生代”“老年代”“永久代”“Eden空间”“From Survivor空 间”“To Survivor空间”等名词,这些区域划分仅仅是一部分垃圾收集器的共同特性或者说设计风格而已,而非某个Java虚拟机具体 实现的固有内存布局,而垃圾收集器一直到G1(一款垃圾收集器)为止,还都是分代进行回收,然而到目前为止出现了不采用分代设计的新垃圾收集器如ZGC等。
由于堆涉及的内容太多,不在此讲多,后面会出文章细讲
方法区
方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器
编译后的代码缓存等数据。虽然《Java虚拟机规范》中把 方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目
的是与Java堆区分开来。
方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。运行时常量池都分配在 Java 虚拟机的方法区之中
JDK 8开始元空间代替永久代,方法区存放在元空间中,占用本地内存。根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出 OutOfMemoryError异常。
运行时常量池(所属方法区)
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字 段、方法、接口等描述信息外,还有一项信
息是常量池表(Constant Pool Table),用于存放编译期生 成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量
池中。
常量:
运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法(将字符串对象加入常量池中)。 既然运行时常量池是方法区的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存 时会抛出OutOfMemoryError异常。
直接内存
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中 定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。
本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存的限制。
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
对于直接内存和堆内存的申请效率和读写效率后面也会出文章专门讲解。
参考:《深入理解java虚拟机》–周志明
本文地址:https://blog.csdn.net/qq_45537574/article/details/107361878