美文网首页
浅谈 JVM 内存区域

浅谈 JVM 内存区域

作者: 温驭臣 | 来源:发表于2019-01-15 11:36 被阅读0次

        今天,我们从内存管理角度,进一步探索 JAVA 虚拟机(JVM)。垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率,但是,垃圾收集也不是万能的,懂得 JVM 内部的内存结构,工作机制,是设计高扩展性应用和诊断运行时间问题的基础,也是 Java 工程师进阶的必备能力。

第一,内存区域的划分

        通常可以把 JVM 内存区域分为下面几个方面,其中,有的区域是以线程为单位,而有的区域则是整个 JVM 进程唯一的。

        1,程序计数器。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值。

        2,Java 虚拟机,早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的 Java 方法调用。前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫做当前帧,方法所在的类叫做当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。栈帧中存储着局部变量表,操作数栈,动态链接,方法正常退出或者异常退出的定义等。

        3,堆,它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的 “Xmx” 之类参数就是用来指定最大堆空间等指标。理所当然,堆也是垃圾收集器重点照顾的区域,所以堆空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代,老年代的划分。

        4,方法区,这是所有线程共享的一块内存区域,用于存储所谓的元数据,例如类结构信息,以及对应的运行时常量池,字段,方法代码等。由于早期的 Hotspot JVM 实现,很多人习惯将方法区称为永久代。Oracle JDK 8中将永久代移除,同时增加了元数据区。

        5,运行时常量池,这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号,字段,方法,超类,接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要爱运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。

        6,本地方法栈,它和 Java 虚拟机是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块区域,这完全取决于技术实现的决定,并未在规范中强制。

第二,什么是 OOM 问题,它可能在那些内存区域发送?

        为了更直观,清晰的印象,看下面的内存结构图:

        OOM 通俗一点说,就是 JVM 内存不够用了,javadoc 中对 OOM 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。当然,也不是在任何情况下垃圾收集器都会被触发,比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM 可以判断出垃圾收集器并不能解决这个问题,所以直接抛出 OutOfMemoryError。

        从数据区的角度,除了程序计数器,其他区域都有可能会因为可能的空间不足发生 OutOfMemoryError。简单总结如下:

        1,堆内存不足时最常见的 OOM 原因之一,抛出的错误信息是 “java.lang.OutOfMemoryError:Java heap space”,原因可能千奇百怪,例如,可能存在内存泄漏的问题;也可能就是堆的大小不合理,比如我们要处理比较可观的数据量,但是没有显示指定 JVM 堆大小或者指定数值偏小;或者出现 JVM 处理引用不及时,导致堆积起来,内存无法释放等。

        2,对于 Java 虚拟机和本地方法栈,这里要稍微复杂一点。如果我们写了一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。类似这种情况,JVM 实际会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展空间的时候失败,则会抛出 OutOfMemoryError。

        3,对于老版本的 Oracle JDK,因为永久代的大小是有限的,并且 JVM 对永久代垃圾回收(如,常量池回收,卸载不再需要的类型)非常不积极,所以当我们不断添加新类型的时候,永久代出现 OutOfMemoryError 也非常多见,尤其是在运行时存在大量动态类型生成的场合;类似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的异常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError:PermGen space”。

        4,随着元数据区的引入,方法区内存已经不再那么窘迫,所以相应的 OOM 有所改观,出现 OOM,异常信息则变成了:“java.lang.OutOfMemoryError:Metaspace”。

        5,直接内存不足,也会导致 OOM。

第三,如何监控和诊断 JVM 堆内和堆外内存使用?

        了解 JVM 内存的方法很多,具体能力范围也有区别,简单总结如下:

        1,可以使用综合性的图形化工具,如 JConsole,VisualVM(注意,从 Oracle JDK 9 开始,VisualVM 已经不再包含在 JDK 安装包中)等。这些工具具体使用起来比较直观,直接连接到 Java 进程,然后就可以在图形化界面里掌握内存使用情况。以 JConsole 为例,其内存页面可以显示常见的堆内存各种堆外部分使用状态。

        2,使用命令行工具进行运行时查询,如 jstat 和 jmap 等工具都提供了一些选项,可以查看堆,方法区等使用数据。

        3,使用 jmap 等提供的命令,生成堆转储文件,然后利用 jhat 或者 Eclipse MAT 等堆转储分析工具进行详细分析。

        4,GC日志等输出,也包含着丰富的信息。

        有一个相对特殊的部分,就是堆内存中的直接内存,前面的工具基本不适用,可以使用 JDK 自带的 Native Memory Tracking (NMT) 特性,它会从 JVM 本地内存分配的角度进行解读。

相关文章

网友评论

      本文标题:浅谈 JVM 内存区域

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