当前位置: 首页 > Web前端 > HTML5

面试中经常被问到的Java虚拟机内存模型,看完这篇文章就够了!

时间:2023-04-05 20:35:38 HTML5

1。不同平台的虚拟机同一个java代码生成的机器码肯定是不同的,因为不同操作系统的底层硬件指令集是不同的。同样的java代码在windows上生成的机器码可能是0101...,而在linux上生成的机器码可能是1100...,那么这是怎么实现的呢?在这里,小编建了一个前端学习交流按钮群:132667127,自己整理的最新前端资料和进阶开发教程。有需要的可以加群一起学习交流。不知道同学们是否还记得。在下载jdk的时候,我们在oracle官网上根据不同的操作系统或者数字版本下载不同的jdk版本,也就是说对于不同的操作系统,jdk虚拟机有不同的实现。那么什么是虚拟机呢?如图所示,它从软件层面屏蔽了不同操作系统在底层硬件和指令上的差异,这就是跨平台的由来。说到这里,同学们可能还是有点不明白,说的太宏观了,下面我们来了解下java虚拟机的组成。二、虚拟机的组成1、栈先说其中的内存区栈。大家都知道栈是存放局部变量的,也是线程独有的区域,也就是每个线程都会有自己独立的栈区。说到栈大家都不陌生,数据结构里面就有学习。这里线程栈中存放数据的部分使用栈,先进后出。大家都知道每个方法都有自己的局部变量,比如上图中main方法中的math,compute方法中的abc。那么,为了区分不同方法中局部变量作用域的内存区域,java虚拟机,每个方法在运行时都会分配一个单独的栈帧内存区域。我们试着根据上图中的程序简单画出代码执行的内存活动。在main方法中执行第一行代码就是在栈中分配main()方法的栈帧,存放math局部变量,然后执行compute()方法,然后stack会分配compute()的栈帧区域。这里栈存储数据的方式和数据结构中学习到的栈是一样的,先进后出。当compute()方法执行时,会出栈释放,符合先进后出的特点,最后调用的方法先出栈。Stackframe所以stackframe其实不仅仅存放局部变量,它还有一些其他的东西,主要由四部分组成。所以要说到这里,就会涉及到一个更底层的原理——字节码。我们先来看一下我们上面代码的字节码文件。它看起来像一个16字节的文件,看起来像乱码。其实每一个都有对应的含义。Oracle官方有专门的jvm字节码指令手册查询每组指令对应的含义。那么,我们研究的当然不是这个。jdk有一个内置的javap命令,它可以从上面的类文件生成一个更易读的字节码文件。我们使用javap-c命令反编译class文件,输出为TXT文件。这时候jvm的指令代码就清晰多了,大体的结构也看得懂了,比如类、静态变量、构造方法、compute()方法、main()方法等。方法里面的说明还是有点啰嗦。我们拿一个compute()方法来看:CODE:0:iconst_11:Istore_12:istore_23:Istore_24:ILOAD_15:IADD7:Bipush109:Imul109:Imul109:Imul109:Imul109:Imul109:Imul10:istore_311:iload_312:ireturn这几行代码对应我们代码中compute()方法中的四行代码。大家都知道代码越往下,代码实现的行数就越多,因为在运行时会包含一些隐藏在java代码底层的细节和原理。所以同样的,官方的jvm指令也有手册可以查阅,网上也有很多翻译版本。如果想了解更多,可以自行百度。这里我只解释一下这篇博文设计代码中的一些指令的含义:0.将int类型常量1压入操作数栈0:iconst_1这一步很简单,就是将1压入操作数栈1。将int类型的值存入Localvariable11:istore_1局部变量1,也就是我们代码中的第一个局部变量a,首先在局部变量表中为a分配内存,然后存入int类型的值,也就是当前的只有1,进入局部变量a2。将int类型常量2压入操作数栈2:iconst_23。将int类型值存入局部变量23:istore_2这两行代码与前面两行类似。4、从局部变量1:iload_15中加载int类型值4。从局部变量2加载int类型值5:iload_2这两段代码分别加载局部变量1和2,即a和b的值到操作数6中。执行int类型6的加法:一旦执行了iaddiadd指令,将操作数栈中的1和2从栈底出栈相加,然后将运算结果3压入操作数栈底。7.将一个8位有符号整数压入栈中7:bipush10这条指令是将10压入栈中8.对int类型9进行乘法运算:imul这个和上面的加法类似,从栈中弹出3和10,并将结果30压入栈中9.将int类型的值存入局部变量310:istore_3,这个大家都很熟悉了,和第二步、第三步一样,将30存入局部变量变量3,即c10。从局部变量3中加载int类型值11:iload_3之前也说过11。返回int类型值12:ireturn不用说了,就是把操作数栈中的30返回到这里,让我们在compute()方法讲解之后,是不是加深了对局部变量表和操作数栈的理解讲座?说白了就是赋值号=后面跟着操作数。当这些操作数被赋值时,在运行过程中需要将它们存储在内存中,存储在操作数栈中作为一个小的内存区域,用于暂存操作数。接下来说一下方法导出。说白了,methodexit就是method执行完之后会去的地方。那么我们就知道,上面的compute()方法执行完之后,应该会返回到main()方法的第三行。然后当main()方法调用compute()时,compute()方法出口在栈帧中存储当前要返回的位置,那么当compute()方法执行时,会返回到对应的位置main()方法根据保存在方法中的相关信息退出。然后main()这边也有自己的栈帧,这里说说一些不同点。我们已经知道局部变量会存放在栈帧中的局部变量表中,那么main()方法中的math会存放在里面,但是这里的math是一个对象,我们知道新建的对象是存储在堆中那么这个数学变量和堆中的对象有什么联系呢?是同一个概念吗?当然不是,局部变量表中的math在堆中存放的是math对象在堆中的内存地址。当前线程正在运行或即将运行的jvm指令代码对应的地址,或者行号位置。上面的代码中,每条指令代码前面都有一个行号,你可以把它看作是当前线程执行到某行代码位置的标志,这个值就是程序计数器的值。那么jvm虚拟机为什么要设置程序计数器结构呢?就是为了多线程的出现以及多线程之间的切换。当一个程序被挂起时,它必须总是被恢复。那么应该在哪里恢复呢?无法重启,所以程序计数器就解决了。这个问题。3、在jdk1.8之前,方法区有个名字叫做persistentzone/permanentgeneration。应该很多同学都听说过。jdk1.8之后,oracle正式更名为metaspace。存储常量、静态变量、类元信息。公共静态intinitData=666;这个initData是一个静态变量,毫无疑问是存放在方法区的publicstaticUseruser=newUser();那么这个user就有点不同了,user变量是放在方法区的,新的User是存放在堆中的。这里我们可以体会到栈、堆、方法区都是相连的。栈中的局部变量,方法区中的静态变量,如果是对象类型,都会指向堆中的新对象,那么红色链接代表什么?让我们首先了解对象。对象组合你对对象了解多少?如果你每天都使用对象,你知道对象在虚拟机中的存储结构吗?对象在内存中存储的布局可以分为三个区域:对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)。下图是普通对象实例和数组对象实例的数据结构:objectheaderHotSpot虚拟机的objectheader包括两部分信息:MarkWord第一部分markword用于存放对象本身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位分别为32bit和64bit64位虚拟机(未启用压缩指针)。64bit,正式名称为“MarkWord”。KlassPointer对象头的另一部分是klass类型指针,也就是对象指向其类元数据的指针。虚拟机通过这个指针来判断对象是哪个类实例。数组的长度(只有数组对象才有)如果对象是一个数组,对象头中必须有一段数据记录数组的长度。实例数据实例数据部分是对象实际存储的有效信息,也是程序代码中定义的各类字段的内容。无论是从父类继承还是在子类中定义,都需要记录下来。Alignmentpadding第三部分alignmentpadding不一定存在,也没有什么特殊意义,只是起到占位符的作用。因为HotSpotVM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话说,对象的大小必须是8字节的整数倍。对象头部分恰好是8字节的倍数(1或2倍),因此,当对象实例数据部分不对齐时,需要通过对齐填充来完成。其中,klass类型指针就是红色的连接。它是如何连接的?新线程()。开始();类加载实际上是以类元信息的形式存放在方法区的。math和math2都是同一班的新学生。当对象是new的时候,会在对象头中存放一个指向类元信息的指针,也就是KlassPointer。这里我们对栈、程序计数器和方法区进行说明。接下来简单介绍下局部方法区,最后在最后讲解一下堆。4.本地方法栈其实本地方法栈现在已经用的比较少了。土法大家应该都听说过。经常使用的线程类如何使用,底层调用start0()的一个方法privatenativevoidstart0();此方法未实现,但它不是接口。它是用native装饰的,属于一种土方法。底层用C语言实现。那为什么在java代码中会有一个用C语言实现的native方法呢?大家都知道JAVA出来了。在此之前,一个公司99%的系统都是用C语言实现的,但是java出现后,很多项目都要用java开发,所以新系统和旧系统是不一样的。必然要有交互,所以需要一个局部的方法来实现。最底层是调用C语言的dll库文件,类似于java中的jar包。当然现在有很多跨语言的交互方式,比如thrift、http接口方法、webservice等,当时没有这些方法,只能通过本地方法实现。那么本地方法永远是一个方法。每个线程在运行时,如果运行到本地方法,还必须生成本地变量等,然后需要存放到本地方法栈中。如果没有本地方法,就不会有本地方法栈。5.堆最后,我们说一下堆。堆是最重要的内存区域。相信大多数人都熟悉堆。但是对于它的内部结构,想弄清楚它的运行细节就没那么简单了。每个人都应该知道这个基本组成。是的,它是由年轻一代和老一代组成的。新生代分为Eden区和Survivor区。在survivor区,有from区和to区。我们都有新对象。知道是放在堆里的,那到底放在堆里的什么地方呢?其实新的对象一般都是放在Eden区的,那为什么叫Eden区呢?伊甸园是亚当和夏娃居住的地方,不就是创造人类的地方吗?那么我们的新对象就放在这里了,那么当Eden区满了怎么办呢?假设我们给pair分配了600M内存,这个可以通过参数来调整,后面会讲到。那么老年代默认占2/3,差不多400M,年轻代200M,Eden区160M,Survivor区40M。GC程序只要在运行,就不会保留新的对象,所以总会有Eden区满的时刻,那么一旦Eden区满了,虚拟机怎么办呢?没错,就是gc,但是这里的gc属于minorgc,也就是垃圾回收,对垃圾对象进行收集清理,那么什么是垃圾对象呢?比如我们上面提到的math对象,我们假设我们是一个web应用程序,主线程执行完后程序不会结束,而是main方法结束,那么main()方法的栈帧就会被释放,局部变量将被释放。但是这个局部变量对应的堆中的对象仍然存在,只是没有指针指向它,那么它就是一个垃圾对象,应该被回收。如果以后有新的Math对象,之前就不会用了,因为已经找不到了,如果一直保留这个对象,只会占用内存,显然是不合适的。这里涉及到一个GCRoot和可达性分析算法的概念,也是面试中偶尔会问到的。可达性分析算法以GCRoots对象为起点,从这些起点开始搜索引用对象。找到的对象都标记为非垃圾对象,其余未标记的对象为垃圾对象。那么什么是GCRoots根对象呢?GCRoots根是判断一个对象能否被回收的依据。只要能通过GCRootsroot搜索到可搜索的对象,那么这个对象就不会被认为是垃圾对象,而是可以作为GCRoots的root,里面有线程栈的局部变量,静态变量,本地方法栈等。说白了就是找到连接到根节点的对象是有用的对象,剩下的就认为是垃圾对象,需要回收。在第一次minorgc之后,没有被清理的对象会被移动到From区域,如上图所示。说到对象组成的时候,上面写到GC分代年龄存放在对象头的MarkWord中。对象每进行一次GC,其GC的分代年龄都会+1,如上图所示。那么如果第二个新对象又把Eden区填满了,那么就会再次执行minorgc,不过这次是和From区一起gc,然后把Eden区和From区存活的对象移动到至区域。objectheader中的generationalage为+1,如上图所示。然后当Eden区第三次再次满时,minorgc会回收Eden区和To区,TEden区和To区还活着的对象会被移到From区,如如上图所示。说白了就是Survivor区中总有一块区域是空的,将存活的对象依次存放在From区和To区,也就是互相复制,也就是中的copy-recovery算法垃圾回收算法。如果一个对象经历了15次GC的限制,它将被移到老时代。如果还没有达到限制,From区或者To区装不下,就直接移到老年代。这只是两个通用规则的示例,还有其他规则也会将对象存储在老年代。然后随着应用程序的不断运行,老年代最终会被填满,然后这个时候也会进行gc,而这个时候的gc就是Fullgc。GC案例通过一个简单的演示案例让我们更清楚地了解GC。这段代码显然是一个死循环,不断地向列表中添加新的对象。这里我们使用jdk自带的一个jvm调优工具jvisualvm来观察这段代码执行的内存结构。运行代码打开后,我们可以看到这样一个界面:我们在左边可以看到我们在应用程序中运行的代码,右边可以看到它的一些jvm和内存信息。这里我们先不关注,我们需要用到的是最后一个AVisualGC面板,这是一个插件,如果没有这个栏目,可以在工具栏的插件里面下载安装.打开可视化GC,让我们看一下界面的大致布局,其中老年代(Olc)、伊甸园(Eden)、S0(From)、S1(To)的内存和动态分配图一目了然可见的。一对应。给大家选中间的图对应上面的内容:1:对象放在Eden区2:Eden区满了,发生minorgc3:把第二步存活的对象移到From(Survivor0)area4:Eden区满时发生Minorgc5:第四步将存活对象移动到To(Survivor1)区。在这里你可以注意到From和To区域和我们上面说的一样,其中一个总是空的。还可以注意到这里的oldgeneration是一条直线,中间突然增加,说明在minorgc中,一批批符合规则的对象被分批移入oldgeneration。那么当我们老了会发生什么?当然是我们上面提到的FullGC,但是如果仔细看我们写的程序,我们所有新建的HeapTest对象都是存放在heapLists中的,它们都会被这个局部变量引用,所以FullGC不会有一些可以回收的垃圾对象,但是内存又满了,怎么办?没错,就是我们没见过也一直听说的OOM。jvm内存模型的简单介绍到此结束。