继续创造,加速生长!这是我参加「日新计划 10 月更文挑战」的第28天,点击查看活动概况

前语

本博文将从内存管理的视点,进一步探索 Java 虚拟机(JVM)。废物搜集机制为咱们打理了许多繁琐的作业,大大提高了开发的功率,可是,废物搜集也不是全能的,懂得 JVM 内部的内存结构、作业机制,是设计高扩展性运用和诊断运行时问题的根底,也是 Java 工程师进阶的必备才能。

本篇博文的要点是,谈谈 JVM 内存区域的区分,哪些区域或许产生 OutOfMemoryError?

概述

通常能够把 JVM 内存区域分为下面几个方面,其间,有的区域是以线程为单位,而有的区域则是整个 JVM 进程仅有的。

首要,程序计数器(PC,Program Counter Register)。在 JVM 标准中,每个线程都有它自己的程序计数器,而且任何时间一个线程都只要一个办法在履行,也便是所谓的当时办法。程序计数器会存储当时线程正在履行的 Java 办法的 JVM 指令地址;或许,如果是在履行本地办法,则是未指定值(undefined)。

第二,Java 虚拟机栈(Java Virtual Machine Stack),前期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 办法调用。

前面谈程序计数器时,提到了当时办法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫作当时帧,办法地点的类叫作当时类。如果在该办法中调用了其他办法,对应的新的栈帧会被创建出来,成为新的当时帧,一直到它返回结果或许履行完毕。JVM 直接对 Java 栈的操作只要两个,便是对栈帧的压栈和出栈。

栈帧中存储着局部变量表、操作数(operand)栈、动态链接、办法正常退出或许反常退出的定义等。

第三,(Heap),它是 Java 内存管理的核心区域,用来放置 Java 目标实例,几乎一切创建的 Java 目标实例都是被直接分配在堆上。堆被一切的线程共享,在虚拟机启动时,咱们指定的 “Xmx” 之类参数便是用来指定最大堆空间等指标。

天经地义,堆也是废物搜集器要点照料的区域,所以堆内空间还会被不同的废物搜集器进行进一步的细分,最有名的便是新生代、老年代的区分。

第四,办法区(Method Area)。这也是一切线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、办法代码等。

因为前期的 Hotspot JVM 完结,许多人习惯于将办法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,一起增加了元数据区(Metaspace)。

第五,运行时常量池(Run-Time Constant Pool),这是办法区的一部分。如果仔细剖析过反编译的类文件结构,你能看到版别号、字段、办法、超类、接口等各种信息,还有一项信息便是常量池。Java 的常量池能够存放各种常量信息,不管是编译期生成的各种字面量,仍是需求在运行时决定的符号引证,所以它比一般言语的符号表存储的信息愈加宽泛。

第六,本地办法栈(Native Method Stack)。它和 Java 虚拟机栈是十分相似的,支撑对本地办法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地办法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技能完结的决定,并未在标准中强制。

正文

首要,为了让你有个愈加直观、清晰的形象,我画了一个简略的内存结构图,里边展示了我前面提到的堆、线程栈等区域,并从数量上说明了什么是线程私有,例如,程序计数器、Java 栈等,以及什么是 Java 进程仅有。另外,还额外区分出了直接内存等区域。

【JAVA】JVM 内存区域的划分

这张图反映了实践中 Java 进程内存占用,与标准中定义的 JVM 运行时数据区之间的差别,它能够看作是运行时数据区的一个超集。究竟理论上的视角和实际中的视角是有区别的,标准侧重的是通用的、无差别的部分,而关于运用开发者来说,只要是 Java 进程在运行时会占用,都会影响到咱们的工程实践。

这儿简要介绍两点区别:

  • 直接内存(Direct Memory)区域,它便是在博文 【JAVA】文件拷贝方式 中谈到的 Direct Buffer 所直接分配的内存,也是个简单呈现问题的地方。尽管,在 JVM 工程师的眼中,并不以为它是 JVM 内部内存的一部分,也并未表现 JVM 内存模型中。
  • JVM 自身是个本地程序,还需求其他的内存去完结各种基本任务,比方,JIT Compiler 在运行时对热门办法进行编译,就会将编译后的办法储存在 Code Cache 里边;GC 等功能需求运行在本地线程之中,相似部分都需求占用内存空间。这些是完结 JVM JIT 等功能的需求,但标准中并不触及。

如果深入到 JVM 的完结细节,你会发现一些结论好像有些模棱两可,比方:

  • Java 目标是不是都创建在堆上的呢?

我注意到有一些观念,以为通过逃逸剖析,JVM 会在栈上分配那些不会逃逸的目标,这在理论上是可行的,可是取决于 JVM 设计者的挑选。据我所知,Oracle Hotspot JVM 中并未这么做,这一点在逃逸剖析相关的文档里现已说明,所以能够明确一切的目标实例都是创建在堆上。

  • 目前许多书本仍是基于 JDK 7 曾经的版别,JDK 现已产生了很大改变,Intern 字符串的缓存和静态变量曾经都被分配在永久代上,而永久代现已被元数据区取代。可是,Intern 字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点相同契合前面一点的结论:目标实例都是分配在堆上。

接下来,咱们来看看什么是 OOM 问题,它或许在哪些内存区域产生?

首要,OOM 如果通俗点儿说,便是 JVM 内存不够用了,javadoc 中对 OutOfMemoryError 的解释是,没有空闲内存,而且废物搜集器也无法供给更多内存。

这儿边隐含着一层意思是,在抛出 OutOfMemoryError 之前,通常废物搜集器会被触发,尽其所能去整理出空间,例如:

  • 在博文 【JAVA】强引证、软引证、弱引证、幻象引证有什么区别? 的引证机制剖析中,现已提到了 JVM 会去测验收回软引证指向的目标等。
  • 在 java.nio.BIts.reserveMemory() 办法中,咱们能清楚的看到,System.gc() 会被调用,以整理空间,这也是为什么在很多运用 NIO 的 Direct Buffer 之类时,通常主张不要加下面的参数,究竟是个最后的测验,有或许避免必定的内存缺乏问题。
-XX:+DisableExplicitGC

当然,也不是在任何状况下废物搜集器都会被触发的,比方,咱们去分配一个超大目标,相似一个超大数组超过堆的最大值,JVM 能够判别出废物搜集并不能解决这个问题,所以直接抛出 OutOfMemoryError。

从我前面剖析的数据区的视点,除了程序计数器,其他区域都有或许会因为或许的空间缺乏产生 OutOfMemoryError,简略总结如下:

  • 堆内存缺乏是最常见的 OOM 原因之一,抛出的错误信息是 “java.lang.OutOfMemoryError:Java heap space”,原因或许千奇百怪,例如,或许存在内存走漏问题;也很有或许便是堆的巨细不合理,比方咱们要处理比较可观的数据量,可是没有显式指定 JVM 堆巨细或许指定数值偏小;或许呈现 JVM 处理引证不及时,导致堆积起来,内存无法开释等。
  • 而关于 Java 虚拟机栈和本地办法栈,这儿要略微杂乱一点。如果咱们写一段程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。相似这种状况,JVM 实践会抛出 StackOverFlowError;当然,如果 JVM 试图去扩展栈空间的的时候失败,则会抛出 OutOfMemoryError。
  • 关于老版别的 Oracle JDK,因为永久代的巨细是有限的,而且 JVM 对永久代废物收回(如,常量池收回、卸载不再需求的类型)十分不活跃,所以当咱们不断添加新类型的时候,永久代呈现 OutOfMemoryError 也十分多见,尤其是在运行时存在很多动态类型生成的场合;相似 Intern 字符串缓存占用太多空间,也会导致 OOM 问题。对应的反常信息,会标记出来和永久代相关:“java.lang.OutOfMemoryError: PermGen space”。
  • 随着元数据区的引入,办法区内存现已不再那么窘迫,所以相应的 OOM 有所改观,呈现 OOM,反常信息则变成了:“java.lang.OutOfMemoryError: Metaspace”。
  • 直接内存缺乏,也会导致 OOM,这个现已在博文 【JAVA】NIO 如何完结多路复用? 中介绍过。

后记

以上便是【JAVA】JVM 内存区域的区分的一切内容了;

介绍了首要的内存区域,以及在不同版别 Hotspot JVM 内部的改变,而且剖析了各区域是否或许产生 OutOfMemoryError,以及 OOME 产生的典型状况。

上篇精讲:【JAVA】不会有人不知道 Java 类能够在运行时动态生成吧?

我是,等待你的关注;

创造不易,请多多支撑;

系列专栏:面试精讲 JAVA