JVM除妖降魔之JVM基本结构

JVM除妖降魔之JVM基本结构_第1张图片
JVM虚拟机基本结构

一、类加载子系统

负责从文件系统或者网络中加载class信息,加载的类信息存放于一块称为方法区的内存空间中。

二、方法区

方法区只是JVM规范中定义的一个 规范 ,需要JVM各厂商自己实现,对应于HotSpot那就是所谓的永久代或者JDK8之后的元空间,和java堆一样,是一块所有线程共享的内存区域。 方法区主要存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码和数据 。其大小决定了系统可以保存多少个类。

针对Hotspot虚拟机在JDK6,7,8之间做过许多调整:

[图片上传失败...(image-f95f65-1610002343019)]

JDK7 之前

在jdk7之前,可以理解为永久区(Perm),可通过-XX:PermSize和-XX:MaxPerSize指定大小。符号引用(Symbols)、字符串常量池(interned strings)、类的静态变量(class static variables)、运行时常量池以及其它信息都存在于这个区域,此区域是和堆逻辑隔离的,共享一片连续空间的内存

JDK7

移除永久代的工作从JDK1.7开始,但是并未完全移除。只是将永久代中的 字符串常量类的静态变量池 转移到了 Java Heap 中,还有符号引用转移到了 Native Memory

JDK8

在jdk8之后,永久区被彻底移除,取而代之的是元数据区(MetaSpace)并且将以前存在于永久代中的部分杂项移动到了堆中。

Oracle JRockit以及IBM JVM类似,在本地内存中分配空间,是一块堆外的直接内存。元空间的容量取决于机器的内存容量,若不指定大小,默认虚拟机会耗尽所有可用系统内存,可通过-XX:MaxMetaSpaceSize指定大小。

永久代向元空间的转换原因

1.永久代有一个JVM的固定大小的上限,无法进行调整,而元空间使用的是直接内存,受本地内存的限制,虽然可能发生溢出,但是相比较永久代几率会更小一点。

2.JRockit并没有永久代的概念,也是为了和JRockit保持一致方便合并等等

http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-September/006679.html

https://www.oracle.com/webfolder/technetwork/tutorials/mooc/JVM_Troubleshooting/week1/lesson1.pdf

三、堆

JVM除妖降魔之JVM基本结构_第2张图片
JVM堆结构

Java堆在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。堆是运行时数据区,几乎所有类实例和数组的内存都从该区分配(之所以说几乎,因为栈上分配)。堆空间是所有线程共享的,这是一块与java应用密切相关的内存区域。

其中堆划分为年轻代和老年代。年轻代存放刚产生的新对象(生命周期较短),而老年代则存放不容易销毁、比较大的对象。

四、直接内存

直接内存是在Java堆外的,直接向系统申请的内存区域。通常访问直接内存的速度会优于Java堆。因此在读写频繁的场合可能会考虑使用直接内存,由于直接内存在java堆外,所有它的大小不会直接受限于Xmx指定最大堆大小,但是系统内存是有限的。Java的NIO库允许JAVA程序使用直接内存。

五、垃圾回收系统

垃圾回收系统是java虚拟机的重要组成部分,垃圾回收器可以对方法区、java堆和直接内存进行回收。

六、虚拟机栈

JVM除妖降魔之JVM基本结构_第3张图片
JVM虚拟机栈结构

每一个虚拟机线程都有一个私有的java栈。java虚拟机栈在线程创建的时候被创建。栈中保存着栈帧信息,局部变量、方法参数等。每一个方法在执行的同时都会创建一个栈帧并入栈,栈帧中存储局部变量表操作数栈动态链接返回地址等信息;相反的,当方法调用结束返回时,对应的栈帧也会出栈。

此区域可能会发生StackOverflowErrorOutOfMemoryError

HotSpot虚拟机中,栈的大小可通过-Xss指定,栈的大小也决定了函数调用的可达深度。

6.1 局部变量表

局部变量表是栈帧的重要组成部分之一,在栈帧中与性能调优关系最密切的就是局部变量表。局部变量表用于保存方法的参数及局部变量。对于非static方法,虚拟机还会将当前对应引用作为入参传递给即将执行的方法。

局部变量表以“字(计算机内存中占据一个单独的内存单元编号的一组二进制串。一般32位机器一个字就是4个字节的长度)”为单位进行内存划分,一个“字”有32位,所以针对long、double类型就需要使用两个“字”进行存储。“字“”空间是可以重用的,比如在一个方法内定义的两个变量a,b,在定义b的时候,a的作用域已经失效,那么b将占用a原有所在的字空间。这里通过一个例子进行说明:

首先需要在我们的IDEA中装一个jclasslib插件,在方法中有一个指标叫"Maximum local variables",表示此方法执行时,栈帧中局部变量所需要的最大容量(以字为单位)。看如下方法

    private void  test(){
        int a;
        int b;
    }

该方法中定义了两个局部变量a和b,并且他们的作用域相同,此时查看通过jclasslib查看"Maximum local variables"


JVM除妖降魔之JVM基本结构_第4张图片
image.png

可以看到这里显示的数量为3,因为除了我们定义的两个局部变量,还有一个当前对象的引用this。

接下来将代码稍作改变:

    private void  test(){
        {
            int a;
        }
        int b;
    }

再次通过jclasslib查看局部变量表最大所需容量


JVM除妖降魔之JVM基本结构_第5张图片
image.png

局部变量表中的变量也是重要的垃圾回收根节点,被局部变量表中直接或简介引用的对象都是不会被回收的。通过几个典型的示例来进行说明:

    public static void test1() {
        //申请6M空间
        {
            byte[] bytes = new byte[6 * 1024 * 1024];
        }
        System.out.println("test1 开始垃圾回收");
        System.gc();
        System.out.println("test1 结束垃圾回收");


    }

    public static void test2() {
        //申请6M空间
        {
            byte[] bytes = new byte[6 * 1024 * 1024];
        }
        int c = 10;
        System.out.println("test2 开始垃圾回收");
        System.gc();
        System.out.println("test2 结束垃圾回收");
    }

上述程序中test1和test2函数都申请了6M的堆空间,并且使用局部变量引用这块空间。之后通过System.gc()来进行垃圾回收。通过使用JVM参数-XX+PrintGC执行,可得到执行结果:

test1 开始垃圾回收
[GC (System.gc())  8765K->6992K(123904K), 0.0081964 secs]
[Full GC (System.gc())  6992K->6793K(123904K), 0.0117884 secs]
test1 结束垃圾回收

test2 开始垃圾回收
[GC (System.gc())  14248K->6921K(123904K), 0.0017406 secs]
[Full GC (System.gc())  6921K->626K(123904K), 0.0091824 secs]
test2 结束垃圾回收

通过执行结果显示test1函数在发生FullGC后,堆空间无明显变化,故此byte数组申请的6M空间未被回收,而test2中在执行了gc之后,堆空间由6921变为了626,大约回收了6M的空间,可知在test2中申请的6M空间已被JVM进行回收。那么为什么呢?

在test1中,在垃圾回收之前使得局部变量bytes失效,虽然变量bytes已经离开了作用域,但是变量bytes依然存在于局部变量表中,依然引用指向着6M空间的byte数组,故此byte数组依然无法被回收。

在test2中,在局部变量bytes失效之后,又定义了新的变量c,此时变量a会复用已经失效的变量bytes在局部变量表中的槽位(即字),所以变量bytes将被销毁,进而6M的空间无人引用,垃圾回收器可以顺利回收。

6.2 操作数栈

操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

6.3 帧数据区

除了局部变量表和操作数栈,java栈帧还需要一些数据来支持常量池解析、正常方法返回和异常处理等。大部分java字节码指令需要进行常量池访问,在帧数据区中保存着访问常量池的指针,方便程序访问常量池。

针对异常处理,JVM需要有一个异常处理表,方便在发生异常时找到处理异常的代码,因此异常处理表也是帧数据区中重要一部分。典型异常处理表如下所示

Exception table:
from to target type
  4  16  19     any

上述异常处理表在字节码偏移量4~16字节可能抛出任意异常,如果遇到异常,则跳转到字节码偏移19处执行。当方法抛出异常时,JVM会查找类似异常表进行处理,如果无法在异常表中找到合适的处理方法,将结束当前函数调用,返回调用函数,并在调用函数中抛出相同异常。

6.4 栈上分配

栈上分配是JVM提供的一项优化技术。基本思想:针对那些线程私有的对象(指不可能被其他线程访问的对象),可以将它们打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,不需要垃圾回收器的介入,从而提高系统性能。

栈上分配的技术基础是逃逸分析,逃逸分析目的是判断对象的作用域是否有可能逃出函数体。

相关JVM参数:
-XX:+DoEscapeAnalysis 启用逃逸分析 (只可以在server模式下使用)
-XX+EliminateAllocations 开启标量替换(默认开启),允许将对象打散分配在栈上。

对于大量的零散小对象,栈上分配提供一个一种很好的对象分配优化策略,栈上分配的速度块,并且可以有效避免垃圾回收带来的负面影响,但是由于和堆空间相比,栈空间较小,因此,大对象无法也不适合在栈上分配。

七、本地方法栈

和虚拟机栈非常类似,最大的不同在于虚拟机栈用于java方法的调用,而本地方法栈用于本地方法的调用。

八、PC寄存器(程序计数器)

当线程数量超过CPU数量,那么多线程的执行就需要依靠时间片轮询争夺资源,所以需要有一块线程独有的空间记录当前线程执行到哪个地方,这块区域就称为
PC(Program Counter,程序计数器)寄存器。PC寄存器里保存有当前正在执行的JVM指令的地址。

PC寄存器不针对与native方法

九、执行引擎

JVM最核心组件之一,负责执行虚拟机的字节码。

附录:参考

https://dzone.com/articles/java-8-permgen-metaspace

https://blog.csdn.net/qq876551724/article/details/78845366

http://www.cnblogs.com/paddix/p/5309550.html

https://blog.csdn.net/qq_26222859/article/details/73135660

你可能感兴趣的:(JVM除妖降魔之JVM基本结构)