JVM内存模型和性能优化

JVM内存模型

  1. 模型图
    JVM内存模型和性能优化
  2. 各个内存块存放内容
    JVM内存模型和性能优化

JVM内存结构主要有三大块:堆(heap space)、方法区(method area)、本地区(native area)。
堆内存

  • 堆内存(heap space)存放所有线程共享的对象和数组。在虚拟机启动时创建,Java堆是垃圾收集器管理的主要区域,因此很多时候也被称做“GC堆”。GC回收的策略是按代回收。
  • 对象在实例化后的整个运行周期内,都被存放在堆内存中。堆内存又被划分成不同的部分:是JVM中最大的一块由年轻代(Young Generation Space)和老年代(Old Generation Space)组成。而年轻代内存又被分成三部分,伊甸区(Eden)、From Survivor空间、To Survivor空间(幸存者区域(Survivor Sapce)包括From Survivor空间、To Survivor空间)。默认情况下年轻代按照8:1:1的比例来分配。
  • JVM对堆内存要求:堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

不同堆区域的存放的对象拥有不同的生命周期:
新建(New)或者短期的对象存放在Eden区域;
幸存的或者中期的对象将会从Eden区域拷贝到Survivor区域;
始终存在或者长期的对象将会从Survivor拷贝到Old Generation;
内存的释放(通过销毁对象)通过2种不同的GC实现:Young GC、Full GC。为了检查所有的对象是否能够被销毁,Young GC会标记不能销毁的对象,经过多次标记后,对象将会被移动到老年代中。

方法区
方法区(method area)别名Non-Heap(非堆)。存储线程共享的类信息包括类名、属性(字段、方法)名、常量池、静态变量、方法和构造方法经编译器编译后的代码等数据,方法区有时被称为永久或持久代(PermGen) 因为HotSpot把GC分代收集扩展至方法区,。

  • JVM对堆内存要求:java虚拟机可以回收这个区也可以不回收这个区(类的卸载、常量池的回收)。方法区可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,可以扩展。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

本地区
本地区(native area)包含java虚拟机栈、本地方法栈、程序计数器等内存区域。本地区是线程私有的,存放线程运行时的指向堆内存中的对象引用和局部基本变量。

  • 栈帧(Stack 、线程栈、java栈) 用于存储局部变量表、操作栈、动态链接、方法出口等信息。以方法为单位,每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。 局部变量表存放了编译期可知的各种基本数据类型、对象引用。局部变量表所需的内存空间在编译期间完成分配。原始类型(primitive type)的本地变量以及引用都存放在线程栈中。
  • java虚拟机栈(JVM Stacks) 因为方法的执行都是伴随着线程的,线程执行每个方法的时候都会同时创建一个栈帧(Stack Frame)这个栈帧又叫线程栈或者java栈,是存放于虚拟机栈中的,先入后出。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。方法退出的过程实际上等同于把当前栈帧出栈。
  • 在Java虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度(栈帧的数量),将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。
  • 本地方法栈 本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError、OutOfMemoryError异常
  • 程序计数器(Program Counter Register/ PC)是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

控制参数
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。
-XX:MaxPermSize设置永久代最大空间大小。
-Xss设置每个线程的堆栈大小。没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制。老年代空间大小=堆空间大小-年轻代大空间大小

虚拟机栈的内存大小会直接影响线程创建的数量。假定内存中的堆内存的大小不变都是512M,每个线程的虚拟机栈的大小会直接影响可创建线程数量的大小虚拟机栈内存越大可创建的数量越小。即:JVM内存 = 堆内存 + 线程数量 * 栈内存。

在不同的操作系统版本中,最大的地址空间是不一样的。32位的Windows最大是2G内存,因此可以得到这样的计算公式: 线程数 =(最大内存地址 - Xmx - 系统保留内存)/ Xss
当然线程数量和系统的内核配置还是有很大的关系

什么是虚拟机栈

一、 虚拟机栈:与程序计数器、本地方法栈都是属于线程私有的JVM内存区域。虚拟机栈的生命周期是和线程相同的,是在JVM运行时创建的,在线程中,方法在执行的过程中会创建一个栈帧(Stack Frame)。主要用于存放局部变量表、操作栈、动态链接、方法出口等信息。一般将栈帧内存的大小称为宽度,而栈帧的数量被称为虚拟机栈的深度。虚拟机栈的大小可以通过参数-xss配置。因此在同等大小的虚拟机栈下,如果局部变量表等占用的内存越小,虚拟机栈的深度越大

二、虚拟机栈的内部结构
虚拟机栈是一个后入先出的栈。栈帧是保存在虚拟机栈中的,栈帧是用来存储数据和存储部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、方法返回值和异常分派(Dispatch Exception)。线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。
三、 栈帧

  1. 局部变量表

局部变量表是一组局部变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在Java文件编译为Class文件时,就在方法表的Code属性的max_locals数据项中确定了该方法需要分配的最大局部变量表的容量。

  1. 操作数栈

操作数栈也常被称为操作栈,它是一个后入先出栈。JVM底层字节码指令集是基于栈类型的,所有的操作码都是对操作数栈上的数据进行操作,对于每一个方法的调用,JVM会建立一个操作数栈,以供计算使用。和局部变量一样。操作数栈的最大深度也是编译的时候写入到方法表的code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型,包括long、double(64位)32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2栈容量的单位为**“字宽”,对于32位虚拟机**来说,一个“字宽”占4个字节64位虚拟机来说,一个“字宽”占8个字节。当一个方法刚刚执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指向操作数栈中写入和提取值,也就是入栈与出栈操作。例如,在做算术运算的时候就是通过操作数栈来进行的,又或者调用其它方法的时候是通过操作数栈来行参数传递的。
另外,在概念模型中,两个栈帧作为虚拟机栈的元素,相互之间是完全独立的,但是大多数虚拟机的实现里都会作一些优化处理,令两个栈帧出现一部分重叠。让下栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用返回时就可以共用一部分数据,而无须进行额外的参数复制传递了。

  1. 动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属性方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期期间转化为直接引用,这部分称为动态连接

  1. 方法返回地址

当一个方法被执行后,有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的的方法称为调用者),是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法方式称为正常完成出口(Normal Method Invocation Completion)。另外一种退出方式是,在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方式称为异常完成出口(Abrupt Method Invocation Completion)。一个方法使用异常完成出口的方式退出,是不会给它的调用都产生任何返回值的。 无论采用何种方式退出,在方法退出之前,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者PC计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用都栈帧的操作数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。

  1. 附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。

JVM性能优化

如果JVM频繁发生FULL GC,那么将会是致命的危险,不仅仅会造成网站响应迟钝,更严重的时候会导致系统崩溃,这对用户体验来讲,都是我们不愿意看到的。
JVM内存模型和性能优化

  • Eden区域是用来存放使用new或者newInstance等方式创建的对象,默认都是存放在Eden区,除非这个对象太大,或者超过了设定的阈值-XX:PretenureSizeThresold,这样的对象会被直接分配到Old区域。

(2)2个Survivor(幸存)区,一般称S0,S1,理论上他们是一样大的,解释一下,他们是如何工作的:
在不断创建对象的过程中,Eden区会满,这时候会开始做Young G也叫Minor GC,而Young空间的第一次GC就是找出Eden区中,幸存活着的对象,然后将这些对象,放到S0,或S1区中的其中一个, 假设第一次选择了S0,它会逐步将活着的对象拷贝到S0区域,但是如果S0区域满了,剩下活着的对象只能放old区域了,接下来要做的是,将Eden区域 清空,此时时候S1区域也是空的。

当第二次Eden区域满的时候,就将Eden区域中活着的对象+S0区域中活着的对象,迁移到S1中,如果S1放不下,就会将剩下的部门,放到Old区域中,只是这次对象来源区域增加了S0,最后会将Eden区+S0区域,清空

第三次和第四次依次类推,始终保证S0和S1有一个是空的,用来存储临时对象,用于交换空间的目的,反反复复多次没有被淘汰的对象,将会放入old区域中,默认是15次。具体的交换过程就和上图中的信息相似。