Jvm内存模型 - 图2

Jvm内存区域

根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。

内存区域 主要作用 内存大小 线程隔离 生命周期 oom GC
程序计数器 标记执行位置 最小,128k,可通过-Xxs分配 线程 不会出现 执行
虚拟机栈 存储java方法信息 通过-Xss设置每个线程的栈内存 线程 无法为新线程分配内存 不执行
本地方法栈 存储本地方法信息;
基本类型的局部变量和引用类型对象的引用(指针)
线程 无法为新线程分配内存 执行
存储对象。
包含方法区,方法区包含常量池
最大,
通过-Xms -Xmx修改
虚拟机 无法为新创建的对象分配内存 执行
方法区 存储字节码,类信息,即时编译信息;静态变量 1.8以后,不局限于一个区了,一部分是元空间存储在本地内存中一部分是常量池,在堆中。 虚拟机 无法为新生成的类分配内存 执行
常量池 存储字节码的字面量。常量; jdk7之前包含在方法区中,1.8以后在堆中 虚拟机 包含在方法区中 执行
直接内存 使用NIO,通过DirectByteBuffer对象那个操作。 受服务器物理内存限制 申请分配 需要的直接内存与其他内存之和超过物理内存的限制 执行
元空间 原堆中的永久代

JVM内存参数设置

image.png
-Xms设置堆的最小空间大小。
-Xmx设置堆的最大空间大小。
-Xmn:设置年轻代大小
-XX:NewSize设置新生代最小空间大小。
-XX:MaxNewSize设置新生代最大空间大小。
-XX:PermSize设置永久代最小空间大小。64MB 最小尺寸,初始分配。
-XX:MaxPermSize设置永久代最大空间大小。256MB 最大允许分配尺寸,按需分配-server选项下默认MaxPermSize为64m;-client选项下默认MaxPermSize为32m
-Xss设置每个线程的堆栈大小
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled 设置垃圾不回收

典型JVM参数配置参考:
java-Xmx3550m-Xms3550m-Xmn2g-Xss128k
-XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC-XX:+UseParNewGC
-Xmx3550m:设置JVM最大可用内存为3550M。

-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,官方推荐配置为整个堆的3/8。

-Xss128k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大 小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000 左右。

一,程序计数器

程序计数器是一块非常小的内存,用于记录当前字节码文件执行的行号,其生命周期和线程相同

其主要作用是在多线程并发过程中,当cpu调度不同线程时,记录线程执行位置

因此程序计数器是线程隔离的,每个线程需要拥有自己的计数器。

  • 特点:
    1. 占用内存较小
    2. 线程隔离。
    3. 不会出现oom异常。因为他的功能只是记录需要执行的指令的地址。
    4. 在执行java方法时,会记录字节码指令的地址,执行native方法时为空。
      因为本地方法是通过本地方法接口(JNI)直接调用C/C++代码实现的,不由JVM控制

二,java虚拟机栈

虚拟机栈是用于描述java方法执行的内存模型。主要功能是将执行方法的信息存储在栈里生命周期和线程一致

每个java方法在执行时,会创建一个栈帧,其结构分为:局部变量表;操作数栈;动态链接;方法出口几部分。

方法执行时将栈帧压入虚拟机栈。

特点

  1. 线程隔离。在多线程中,同一个方法可能被多个线程执行,因此每个线程需要维护自己的虚拟机栈。
  2. 不存在垃圾回收问题,方法执行完成自动出栈。

    内存溢出错误

  • StackOverFlowError
    栈溢出错误。java.lang.StackOverflowError通过-Xss设置大小
    jvm为每个线程分配一定的内存大小,因此虚拟机栈允许容纳的栈帧数量是有限的,如果栈帧不断进栈而不出栈,导致该线程的内存耗尽,无法继续入栈的错误。
  • OutOfMemoryError
    内存溢出错误java.lang.OutMemoryError:unable to create new native thread
    Java虚拟机栈无法为新的线程分配内存而造成的错误。
  • OutOfMemoryError(见四,堆)
    堆内存溢出java.lang.OutOfMemoryError: Java heap space
    堆无法为新创建的对象分配内存,而造成的错误。
    在抛出异常之前,会先进行一次GC,如果仍然没有足够的空间分配内存,那么将抛出异常。
  • 方法区异常OutOfMemoryError
    方法区无法为新生成的类分配内存来存储类信息。
    java.lang.OutOfMemoryError: PermGen space
  • StackOverflow和OutOfMemoryError的区别
    StackOverflow是一个线程需要的虚拟机栈内存大于jvm给他分配的内存导致。在一个线程中,不断压入方法到栈中,导致该线程无法分配内存来存放新的方法。
    OutOfMemoryError是java虚拟栈无法申请到更多内存来分配给新的线程导致。
    一个(栈溢出错误)是在自身线程中,另一个(内存溢出错误)是由多个线程引起的java虚拟机栈内存不够。

三,本地方法栈

功能和虚拟机方法栈类似,区别是虚拟机方法栈服务于java方法。而本地方法栈为本地方法服务,不同的虚拟机可能存在不同的实现方法。HotSpot将本地方法栈和虚拟机方法栈进行了合并。

四,堆

内存中最大的区域,主要功能是存放对象,是GC的主要管理对象。

在逻辑上分为老年代和新生代

堆分为新生代,老年代和永久代三部分。永久代在1.8以后取消,改为元空间,使用的是物理内存。
其中新生代又分为Eden区、ServivorFrom、ServivorTo三个区
Eden区是新对象创建后所在的位置,如果新建对象的大小超过Eden区的大小,那么
ServivorFrom区,是保留了一次MinorGC过程中的幸存者。
ServivorTo区,是上次GC的幸存者,作为这一次GC的被扫描者。

特点:

  1. 其资源所有线程共享
  2. 根据不同的对象有不同的GC策略
  3. 生命周期随虚拟机的启动而创建虚拟机关闭生命周期结束
  4. 不要求空间联系,但要求逻辑连续。
  5. 设计为可扩展,通过-Xms -Xmx确定范围,扩展消耗性能严重。
  • OutOfMemoryError(见四,堆)
    堆内存溢出java.lang.OutOfMemoryError: Java heap space
    堆无法为新创建的对象分配内存,而造成的错误。
    在抛出异常之前,会先进行一次GC,如果仍然没有足够的空间分配内存,那么将抛出异常。

jdk1.8后取消永久代,取而代之的是元空间 永久代使用jvm堆内存,而元空间使用的是物理内存,受本机物理内存的限制。

五,方法区

方法区又称为非堆,是被线程共享的一个区域。主要存储类级别的数据编译后的字节码,class/field/method等数据,对象数据,常量,静态变量以及jit编译器的编译结果。

其中包含常量池

方法区通常被描述成永久代,但并不等同于永久代,只是使用永久代的方式实现。目的是为了方便GC能够如同管理堆一样管理方法区。但是更容易造成内存溢出问题。 垃圾回收机制GC在该区域比较少出现,但并不意味着数据在方法区会永久存在。

Jvm内存模型 - 图4

  • 方法区异常OutOfMemoryError
    方法区无法为新生成的类分配内存来存储类信息。
    java.lang.OutOfMemoryError: PermGen space

六,运行时常量池

常量池用于存放在字节码中使用到的字面量和符号引用。如字符串常量,int(1-127)..等。

该部分区域具备动态性,除了类加载是将常量写入其中,java运行期间也可以写入。
使用intern方法可以将其放到常量池。

  1. //使用intern方法
  2. //StringBuilder创建对象会在堆中保存,使用intern方法可以将其放到常量池。
  3. String str = new StringBuilder("ab");
  4. str.intern();
  5. //直接使用字符串字面量,其也会被放入常量池
  6. String str2 = "xy";

jdk7之后的版本已经将运行时常量池从方法区中移出来了,在堆中开辟了一块区域运行常量池。

Jvm内存模型 - 图5

七,直接内存

直接内存并不属于jvm的运行时内存,但是程序运行中却频繁使用,是因为NIO通过通道和缓冲区的IO方式,使用native函数库直接分配堆外内存,并通过DirctByteBuffer对象对堆外内存进行使用和操作

Jvm内存模型 - 图6

堆外内存并不受堆内存大小的限制,也不受jvm进程内存大小的限制,只和本机内存大小以及处理器寻址空间有关(32、64位cpu寻址空间限制不同)

  • 直接内存OutOfMemoryError
    java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method)
    需要分配的直接内存和其他内存加起来超过了服务器的最大物理内存限制。

    以程序为例理解各个区的作用

    ```shell public class HelloJVM { //在JVM运行的时候会通过反射的方式到Method区域找到入口方法main public static void main(String[] args) {//main方法也是放在Method方法区域中的
    1. /**
    2. * student(小写的)是放在主线程中的Stack区域中的
    3. * Student对象实例是放在所有线程共享的Heap区域中的
    4. */
    5. Student student = new Student("spark");
    6. /**
    7. * 首先会通过student指针(或句柄)(指针就直接指向堆中的对象,句柄表明有一个中间的,student指向句柄,句柄指向对象)
    8. * 找Student对象,当找到该对象后会通过对象内部指向方法区域中的指针来调用具体的方法去执行任务
    9. */
    10. student.sayHello();
    } }

class Student { // name本身作为成员是放在stack区域的但是name指向的String对象是放在Heap中 private String name; public Student(String name) { this.name = name; } //sayHello这个方法是放在方法区中的 public void sayHello() { System.out.println(“Hello, this is “ + this.name); } } ``` java程序在jvm中的流程(以以上代码为例)

  1. 程序启动,从类加载路径找到main方法入口类
  2. 读取入口类的class文件,将入口类的类信息放到方法区
  3. 计数器定位到main方法,将main方法入栈,开始执行指令
  4. 在堆中创建实例对象(student),将地址赋给放在java方法栈中的局部变量
    1. 首先到方法区寻找student类,如果没有就使用类加载器加载student类class文件
    2. 创建name对象,那么作为成员变量放在栈中,指向堆中(常量池)”spark”的实际存储地址。
    3. 创建student对象,student作为成员变量放在栈中,指向堆中实际存储地址。
  5. 在方法区中找到Student的类信息,调用sayHello方法,hello方法入栈
  6. 程序技术器执行打印方法。sayHello方法出栈。
  7. main方法出栈。