美文网首页互联网科技Java架构技术进阶Java
一文解析JVM的内存结构,身为程序员还不弄懂JVM怎么行

一文解析JVM的内存结构,身为程序员还不弄懂JVM怎么行

作者: 程序员北游 | 来源:发表于2019-07-01 21:37 被阅读98次

    欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。微信公众号:慕容千语的架构笔记。欢迎关注一起进步。
    前言

    Jvm的内存结构是由《java虚拟机规范》制定的,《java虚拟机规范》只负责制定标准,具体的实现多种多样,比如:sun公司的HotSpot、BEA的JRockit、IBM的J9(前两个目前都已被Oracle收购),另外Apache、Google、微软等组织或公司都有自己的java虚拟机实现。只是我们目前开发比较常用的是HotSpot。

    《java虚拟机规范》与具体的java虚拟机的关系,有点类似于接口和实现类。《java虚拟机规范》负责制定接口,具体的java虚拟机去实现这些接口。我们编写的java代码最终会被编译成class文件,通过java虚拟机解析加载执行。Java语句的“跨平台性”,其实就是通过java虚拟机实现的,不同的平台需要实现不同jvm版本(windows版、linux版等)。我们业务代码只需要开发一份,编译成同一份class文件,却可以在windows、linux等不同的操作系统中执行。

    这里提到的class文件的格式、规范也是由《java虚拟机规范》制定的。换句话讲不管采用什么语言编写的程序,只要按照《java虚拟机规范》编译成class文件就可以在java虚拟机中执行。比如现在常见的可以在jvm(等同java虚拟机)上运行的语言:Scala、Groovy、Jython等等数十种语言(笔者只使用过这里列出三种)。这是jvm虚拟机除了“跨平台性”之外的另一个强大之处—“跨语言性”。

    现在越来越多的语言都有基于jvm实现版本,比如:JavaScript对应的Rhino、Lua对应的Luaj、Python对应的Jyhon等等。利用jvm的“跨语言”特性,可以实现不管你使用什么语言编写的代码最终可以在同一个平台jvm中运行,实现跨语言调用。Jvm不再是java语言的专属,它属于世界上个各种编程语言。

    为什么这么多语言都要争相实现基于jvm的实现版本呢?前面已经提到两点:借助jvm可以实现“跨平台”;借助jvm可以实现“跨语言”。还有一点其实跟今天主题相关:借助jvm实现自动“内存管理”。

    众所周知,与c、c++不同(需要自己控制内存),java可以自动实现垃圾内存回收。其实这份工作不是java语言本身实现的,而是jvm实现的,也就是说任何一种其他语言只要能遵循《java虚拟机规范》可以编译成可以执行class文件,其内存管理就可以放心的交给jvm。

    话又说回来,主流的jvm实现本质上还是使用的c、c++写的(当然理论上用什么语言写都可以,只要符合《java虚拟机规范》),其内存管理还是通过c、c++控制内存空间的开辟和销毁。只是把这部分工作交给了jvm来做,java程序员只需要关心自己的业务逻辑即可。这有点类似架构设计,架构师把通用的功能提到框架中统一处理,普通程序员只需编写业务代码即可。或者可以说Jvm是更顶层的架构设计,只是架构师变成jvm源码实现那帮家伙而已。

    另外《java虚拟机规范》有多个版本,笔者只阅读过“周老师”翻译的《java虚拟机规范 java SE7》,感兴趣的可以直接研究oracle官网最新的版本(英文原版)。

    Jvm的内存结构

    根据java虚拟机规范可以完成java虚拟机的开发,Java虚拟机可以看作是一台抽象的计算机。如同真实的计算机那样,它有自己的指令集以及各种运行时内存区域。本次主题是讨论jvm的内存结构,对应的就是jvm“运行时内存区”。

    该区域大致分为5部分:方法区、java堆、PC寄存器(或者程序计数器)、java虚拟机栈、本地方法栈。大致流程为程序启动时:先把class文件加载到方法区;初始化bean对象放到java堆(比如spring ioc容器中的bean);每个线程执行时会对应一个自己私有的PC寄存器;同时还会创建一个私有的 “java虚拟机栈”或者“本地方法栈”。

    由此可以看出:方法区和java堆是所有线程公有的,PC寄存器、java虚拟机栈和本地方法栈是线程私有的。结构图如下(来至《深入理解Java虚拟机》):

    1、方法区

    在程序启动时jvm会读取class文件,把每个类的结构信息放到“方法区”,这里的class文件包括jar包、war包中的所有class文件。所以程序中应该尽量避免引入无用的、重复的(不同版本)jar包。类的结构信息包括:运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容、还包括一些在类、实例、接口初始化时用到的特殊方法(<init>和<cinit>方法)。该区域所有线程共享。

    在jdk1.8(对应不同的jvm实现)以前,我们常用的hotspot java虚拟机内存分代中有“永久代”的概念,这个“永久代”等价于“方法区”。可以通过PermSize 和 MaxPermSize来设置“永久代”大小。如果超过MaxPermSize,系统会抛出OutOfMemoryError: PermGen异常。

    在jdk1.8之后,hotspot jvm在内存空间中完全移除了“永久代”。“方法区”中“类的元数据信息”被放到“元空间”(Metaspace),“运行时常量池”被放到“java堆”(这部分是从jdk1.7开始)。

    在jdk1.8之后,PermSize 和 MaxPermSize参数设置失效,启动时会有警告信息。改用新的参数MaxMetaspaceSize来限制“元空间”的大小,如果不设置默认最大为本机内存容量(动态调整)。建议通过MaxMetaspaceSize设置最大“元空间”,如果类元数据的空间占用达到参数“MaxMetaspaceSize”设置的值,将会触发对死亡对象和类加载器的垃圾回收。

    2、java堆

    通过-Xms -Xmx指定堆内存大小,Java堆在jvm启动时创建,所有对象的创建和销毁都在这个区域进行,是jvm管理的最大的一块内存(可以不是连续的)。对象的销毁指的就是垃圾回收,为了更合理的回收对象(对象存活时间的长短),通常的jvm实现把java堆分为年轻代和年老代,并采用不同的垃圾回收算法、以及垃圾回收器进行垃圾回收(对象销毁)。这里不对分代算法、垃圾回收器详细讲解。

    该区域中的对象是所有线程共享的,典型的运用场景就是spring的ioc容器,在程序启动时创建一系列对象放到一个全局的map数据结构里,防止被垃圾回收。线程中可以直接使用这些对象,而不需要重复创建和销毁。

    在创建新对象时,如果该区域已没有足够的空间 会抛出OutOfMemoryError异常

    3、程序计数器

    由于jvm是支持多线程执行的,但本质上是cpu在多个线程之间切换,为了记录每个线程执行的位置,每个线程都有自己独有的程序计数器,多个线程之间互不干扰。对于非native方法程序计数器记录的是正在执行的虚拟机字节码指令地址,可以看做所执行字节码的指示器,通过字节解释器改变其值来保证程序按照特定的顺序执行。

    PC寄存器的容量至少应当能保存一个returnAddress类型的数据或者一个与平台相关的本地指针的值,其大小是能确定的。

    该区域需要注意以下三点:

    1.如果线程正在执行的是非native方法,那么计数器记录的是正在执行的虚拟机字节码指令地址

    2.如果执行的native方法,计数器当中的内容应当是空。执行顺序交给jvm的native方法控制。

    3.该区域是java的虚拟机规范当中,唯一一个没有规定OutOfMemoryError的区域。

    4、java虚拟机栈

    每一条java虚拟机线程在创建时会创建自己的java虚拟机栈,因此它是线程私有的。其主要作用是:用于存储局部变量与一些过程计算结果的地方。其工作方式是结合“程序计数器”读入变量到栈,根据不同的指令读取值出栈进行运算(先入后出),运算结果再入栈。

    java虚拟机栈的总容量可以动态扩展,但每个线程的栈大小是固定的,可以通过-Xss参数指定。总内存固定其值越小就可以支持创建更多的线程,同时每个栈的容量就越小,当该线程需要的容量超过这个值时,就会抛出StackOverflowError异常。同理 如果-Xss参数指定的值越大,每个线程可以用的栈内存空间就越大可以存放更多的局部变量等信息,但支持的线程就越小,如果总内存耗尽 没有足够的空间开辟新的线程,会抛出OutOfMemoryError异常。简单的讲如果抛出“抛出StackOverflowError异常”,增大-Xss的值;如果抛出OutOfMemoryError异常,减小-Xss的值。

    Java虚拟机栈里数据结构叫“栈帧”。

    栈帧随着方法调用而创建,随着方法结束而销毁,也就是说一个线程里执行多个方法,对应会产生和销毁多个栈帧,这里的销毁时机是指方法调用结束,也就是说栈的内存回收不像java堆 没有复杂的垃圾回收机制。虽然同一个线程里会有多个栈帧,但同一时间只有一个栈帧,上面提到的-Xms指定的空间,其实是每个栈帧的空间。当线程中一个方法返回时,当前栈帧会传回此方法的执行结果给前一个栈帧,在方法返回之后,当前栈帧就随之被丢弃,前一个栈帧就重新成为当前栈帧了。

    每个“栈帧”都有自己的:局部变量表、操作数栈、动态链接、返回地址。其中局部变量表和操作数栈的容量是在编译期确定,因此栈帧实际消耗容量的大小仅仅取决于Java虚拟机的实现和方法调用时可被分配的内存。

    局部变量表:存放局部变量的列表,一个局部变量类型为boolean、byte、char、short、float、reference和returnAddress的数据,两个局部变量可以保存一个类型为long和double的数据。局部变量使用索引来进行定位访问,第一个局部变量的索引值为零。

    操作数栈:后进先出(Last-In-First-Out,LIFO)栈,长度由编译期决定,在任意时刻,即任意一个栈帧中的操作数栈都会有一个确定的栈深度,一个long或者double类型的数据会占用两个单位的栈深度,其他数据类型则会占用一个单位深度。

    动态链接:简单的理解为指向运行时常量池的引用。在Class文件里面,描述一个方法调用了其他方法,或者访问其成员变量是通过符号引用(Symbolic Reference)来表示的,动态链接的作用就是将这些符号引用所表示的方法转换为实际方法的直接引用。

    返回地址:方法调用的返回,包括正常返回(有返回值)和异常返回(没有返回值),不同的返回类型有不同的指令。

    5、本地方法栈

    用于支持native方法,和java虚拟机栈相似,是线程私有,只是这个栈是采用其他语言实现。同样会有可能抛出StackOverflowError、OutOfMemoryError异常。-Xss设置栈内存的大小同样适用于本地方法栈。

    关于内存溢出和内存泄漏

    内存泄漏一定会导致内存溢出,但内存溢出不一定是内存泄漏导致,也有可能是服务器内存本来就不足,可以通过增加服务器内存 同时增大-Xms –Xmx配置。

    内存泄漏一般比较隐蔽,难于发现。典型的发生场景就是,多线程的的线程中中使用ThreadLocal,在线程执行结束时没有remove,导致对象无法被回收,日积月累内存耗尽,抛出OutOfMemoryError异常。

    关于jvm的内存结构就总结到这里。
    更多资源获取方式:转发+转发+转发后点击这里即可获取免费领取方式!
    欢迎关注专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。微信公众号:慕容千语的架构笔记。欢迎关注一起进步。

    相关文章

      网友评论

        本文标题:一文解析JVM的内存结构,身为程序员还不弄懂JVM怎么行

        本文链接:https://www.haomeiwen.com/subject/zbancctx.html