JVM详解【三】JVM的内存结构

JVM的内存区域

  JVM的内存区域分为线程私有区域(程序计数器、虚拟机栈、本地方法区)、线程共享区域(堆、方法区)和直接内存,如图所示
JVM详解【三】JVM的内存结构_第1张图片

  线程私有区域的生命周期与线程相同,随线程启动而创建,随线程结束而销毁。在JVM内部,每个线程都与操作系统的本地线程直接映射,因此线程私有内存区域的存在与否,和本地线程的启动和销毁对应。

  线程共享区域随虚拟机启动而创建,随虚拟机的关闭而销毁。

  直接内存也叫堆外内存,它并不是JVM运行时数据区的一部分,但在并发编程中被频繁使用。JDK的NIO模块提供的基于Channel与Buffer的I/O操作方式就是基于堆外内存实现的,NIO模块通过调用Native函数库直接在操作系统上分配堆外内存,然后使用DirectByteBuffer对象作为这块内存的引用对内存进行操作,Java进程可以通过堆外内存技术避免Java堆和Native堆中来来回复制数据带来的资源浪费和性能消耗,因此堆外内存在高并发应用场景下被广泛使用(Netty、Flink、HBase、Hadoop 都有用到堆外内存)。

程序计数器:线程私有,无内存溢出

  程序计数器是一块很小的内存空间,用于存储当前运行的线程所执行的字节码的行号指示器。每个运行中的线程都有一个独立的程序计数器,在方法正在执行时,该方法的程序计数器记录的是实时虚拟机字节码指令的地址;如果该方法执行的是Native方法,则程序计数器的值为空(Undefined)。

  程序计数器属于“线程私有”的内存区域,他是唯一没有内存溢出(Out Of Memory)的区域。

  在线程切换过程中,程序计数器记录当前线程执行的字节码指令行号,再切换回该线程时,能保证正确运行。所以程序计数器是线程私有的。

虚拟机栈:线程私有,描述Java方法的执行过程

  虚拟机栈是描述Java方法的执行过程的内存模型,它在当前栈帧(Strack Frame)中存储了局部变量表、操作数栈、动态链接、方法出口等信息。同时,栈帧用来存储部分运行时数据及其数据结构,处理动态链接(Dynamic Linking)方法的返回值和异常分派(Dispatch Exception)。

  栈帧用来记录方法的执行过程,在方法被执行时虚拟机会为其创建一个与之对应的栈帧,方法的执行和返回对应栈帧在虚拟机中的入栈和出栈。无论方法是正常运行完成还是异常完成(抛出了在方法内未被捕获的异常),都视为方法运行结束。下图展示了线程运行及栈帧变化的过程。线程1在CPU1上运行,线程2在CPU2上运行,在CPU资源不足时,其他线程将处于等待状态(如图中的等待线程N),等待获取CPU时间片。在线程内部,每个方法的执行和返回都对应一个栈帧的入栈和出栈,每个运行中的线程当前只有一个栈帧处于活动状态。
JVM详解【三】JVM的内存结构_第2张图片

栈中可能出现的问题

Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

  • 如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常

  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError异常。
    设置栈的大小.

  我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

本地方法区:线程私有

● Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。
   本地方法区和虚拟机栈的作用类似,区别是虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。
● 本地方法栈,也是线程私有的。
● 允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)
 ➢如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError 异常。
 ➢如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError 异常。
● 本地方法是使用C语言实现的。
● 它的具体做法是Native Method Stack中 登记native方法,在Execution Engine 执行时加载本地方法库。
● 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虛拟机限制的世界。它和虛拟机拥有同样的权限。
 ➢本地方法可以通过本地方法接口来访问虛拟机内部的运行时数据区。
 ➢它甚至可以直接使用本地处理器中的寄存器
 ➢直接从本地内存的堆中分配任意数量的内存。
● 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
● 在Hotspot JVM中,直接将本地方法栈和虚拟机栈合二为一 。

堆:也叫做运行时数据区,线程共享

​  在JVM运行过程中创建的对象和产生的数据都被存储在堆中,堆是线程共享的内存区域,也是垃圾回收器进行垃圾回收的主要内存区域。由于现代JVM采用分代收集算法,因此Java堆从GC(Garbage Collection,垃圾回收)的角度还可以分为:新生代、老年代和永久代。

方法区:线程共享

  方法区也被称为永久代,用于存储常量、静态变量、类信息、即时编译器编译后的机器码、运行常量池等数据,如下图所示:
JVM详解【三】JVM的内存结构_第3张图片
  JVM把GC分代收集扩展至方法区,即使用Java堆的永久代来实现方法区,这样JVM的垃圾回收器就可以像管理Java堆一样管理这部分内存。永久代的内存回收主要针对常量池的回收和类的卸载,因此可回收的对象很少。

  常量被存储在运行时常量池(Runtime Constant Pool)中,是方法区的一部分。静态变量也属于方法去的一部分。在类信息(Class文件)中不但保存了类的版本、字段、方法接口等描述信息,还保存了常量信息。

  在即时编译后,代码的内容将在执行阶段(类加载完成后)被保存在方法区的运行常量池中。Java虚拟机堆Class文件的每一部分的格式都有明确的规定,只有符合JVM规范的Class文件才能通过虚拟机的检查,然后被装载、执行。

JVM的运行时内存

  JVM的运行时内存也叫做JVM堆,从GC的角度可以将JVM分为新生代、老年代和永久代。其中新生代默认占1/3堆内存空间,老年代默认占2/3堆内存空间,永久代占非常少的对内存空间。新生代又分为Eden区、SurvivorFrom区和SurvivorTo区, Eden区默认占8/10新生代空间,SurvivorFrom区和SurvivorTo区默认分别占1/10新生代空间,Eden区最小占3/5新生代空间,SurvivorFrom区和SurvivorTo区分别占1/5新生代空间,如下图所示:
JVM详解【三】JVM的内存结构_第4张图片

新生代:Eden区、SurvivorFrom区和SurvivorTo区

  JVM新创建的对象(除了大对象外)会被放在新生代,默认占1/3对内存空间。由于JVM会频繁创建对象,所以新生代会频繁触发MinorGC进行垃圾回收。新生代又分为Eden区、SurvivorFrom区和SurvivorTo区,如下所述:

  1. Eden区:Java新创建的对象首先会被存放在Eden区,如果新创建的对象属于大对象,则直接将其分配到老年代。大队相关的定义和具体的JVM版本、堆大小和垃圾回收策略有关,一般为2KB-128KB,可通过 XX:PretenureSizeThreshold设置其大小。在Eden区的内存空间不足时会触发MinorGC,对新生代进行一次垃圾回收。

  2. SurvivorFrom区:保留上一次MinorGC时的幸存者。

  3. SurvivorTo区:将上一次MinorGC时的幸存者作为这一次MinorGC的扫描者。

​ 新生代的GC过程叫做MinorGC,采用复制算法实现,具体过程如下:

  1. 把在Eden区和SurvivorFrom区中存活的对象复制到SurvivorTo区。如果某对象的年龄达到老年代的标准(对象晋升老年代的标准由 XX:MaxTenuringThreshold设置,默认为15),则将其复制到老年代,同时把这些对象的年龄加1;如果SuriviorTo区的内存空间不够,则也直接将其复制到老年代;如果对象属于大对象(大小为2KB-128KB的对象属于大对象,例如通过 XX:PretenureSizeThreshold=2097152 设置大对象为2MB,1024 × 1024 × 2Byte = 2MB),则也直接将其复制到老年代。
  2. 清空Eden区和SurvivorFrom区中的对象。
  3. 将SurvivoTo区和SurvivorFrom区互换,原来的SurvivorTo区成为下一次GC时的SurvivorFrom区。

老年代

  老年代主要存放长生命周期的对象和大对象。老年代的GC过程叫做MajorGC。在老年代对象比较稳定,MajorGC不会被频繁触发。在进行MajorGC前,JVM会进行一次MinorGC,在MinorGC过后仍然出现老年代且当老年代空间不足或无法找到足够大的连续内存空间分配给新创建的大对象时,会触发MajorGC进行垃圾回收,释放JVM的内存空间。

  MajorGC采用标记清除算法,该算法首先会扫描所有对象并标记存活的对象,然后回收未被标记的对象,并释放空间。

  因为要先扫描老年代的所有对象再回收,所以MajorGC的耗时较长。MajorGC的标记清除算法容易产生内存碎片。在老年代没有内存空间可分配时,会抛出Out Of Memory异常。

永久代

  永久代指内存的永久保存区域,主要存放Class和Meta(元数据)的信息。Class在类加载时被放入永久。永久代和老年代、新生代不同,GC不会在程序运行期间对永久代的内存进行清理,这也导致了永久代的内存会随着加载的Class文件的增加而增加,在加载Class文件过多时会抛出Out Of Memory异常,比如Tomcat引用Jar文件过多会导致JVM内存不足而无法启动。

  需要注意的是,在Java 8 中永久代已经被元数据区(也叫做元空间)取代。元数据区的作用和永久代类似,二者最大的区别在于:元数据区并没有使用虚拟机内存,而是直接使用操作系统的本地内存。因此,元空间的大小不受JVM内存的限制,之和操作系统的内存有关。

  在Java 8 中,JVM将类的元数据放入本地内存(Native Memory)中,将常量池和类的静态变量放入Java堆中,这样JVM能够加载多少元数据信息就不再由JVM的最大可用内存(MaxPermSize)空间决定,而由操作系统的实际可用的内存空间决定。

你可能感兴趣的:(JVM,java)