Java进阶之理解JVM内存区域——运行时数据区

前言

最近在学习 JVM(Java Virtual Machine)的内存区域相关知识,为巩固所学知识整理做个记录,个人学习总结的知识体系架构图如下:
Java进阶之理解JVM内存区域——运行时数据区_第1张图片

一、JVM与操作系统的关系

Java进阶之理解JVM内存区域——运行时数据区_第2张图片
Java 程序编译后生成的 Java 字节码( .class 文件或者 .jar 文件)需要通过 JVM 翻译才能被操作系统识别(0/1机器码),目前可实现跨平台甚至跨语言(如 kotlin)。

Java SE 体系架构中 JDK 提供工具,JRE 提供基础类库, JVM 则只负责翻译,其关系如下图:
Java进阶之理解JVM内存区域——运行时数据区_第3张图片

二、JVM运行过程

JVM 运行过程是本次学习记录的重点,其运行流程图如下:
Java进阶之理解JVM内存区域——运行时数据区_第4张图片
本次主要分析记录“运行时数据区”模块的工作流程,后续会有专门的文章记录“类加载器 classLoader” 模块的内部流程。

三、JVM运行时数据区

Java 虚拟机在执行 Java 程序过程中会将所管理的内存划分为若干个不同的数据区域:程序计数器、虚拟机栈、本地方法栈、方法区和堆。
Java进阶之理解JVM内存区域——运行时数据区_第5张图片
其中,左侧灰色背景的 方法区 和 堆 属于线程共享区域
右侧白色背景的 虚拟机栈、本地方法栈和程序计数器 属于线程私有区域

因为线程共享区域相对较简单,故先总结这块的知识,后续专门分析线程私有区域知识。

静态变量+常量+类信息+运行时常量池存在方法区
数组和几乎所有的对象实例存在 Java 堆内存中,堆是 gc 主要的回收区,一个 JVM 实例只存在一个堆内存,堆内存的大小是可以调节的,可分为三个部分:

A 新生区:伊甸区(Eden space)和2个幸存者区(Survivor pace)

  • 当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园进行垃圾回收(MinorGC),将伊甸园中的剩余对象移动到幸存0区。
  • 若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。
  • 那如果1去也满了呢?再移动到养老区。
  • 若养老区也满了,那么这个时候将产生Major GC(FullGCC),进行养老区的内存清理。
  • 若养老区执行Full GC 之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

重点
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:

  • Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

B 养老区
养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。

C 永久区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。

重点:
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有二:

  • 程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
  • 大量动态反射生成的类不断被加载,最终导致Perm区被占满。

四、JVM线程私有区域

4.1 程序计数器:指向当前线程正在执行的字节码指令的地址

  • JVM规范中规定,如果线程执行的是非native方法,则程序计数器中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则程序计数器中的值是undefined。
  • 由于程序计数器中存储的数据所占空间的大小不会随程序的执行而发生改变,因此,对于程序计数器是不会发生内存溢出现象(OutOfMemory)的-----JVM内存区中唯一不会OOM的内存区域,因只要记录对应地址

4.2 虚拟机栈(Java栈):Java栈是Java方法执行的内存模型,后进先出
每个线程私有的区域,它的生命周期与线程相同,一个线程对应一个java栈,每执行一个方法就会往栈中压入一个元素,这个元素叫“栈帧”,而栈帧中包括了方法中的局部变量、用于存放中间状态值的操作栈动态连接完成出口(返回地址)
如果java栈空间不足了,程序会抛出StackOverflowError异常,所以递归最容易产生这样的问题,若递归如果深度很深,就会执行大量的方法,方法越多java栈的占用空间越大。

4.3 本地方法栈:保存的是native方法的信息

  • 本地方法栈角色和java栈类似,只不过它是用来表示执行本地方法的,本地方法栈存放的方法调用本地方法接口,最终调用本地方法库,实现与操作系统、硬件交互的目的。
  • HotSpot直接把本地方法栈和虚拟机栈合二为一(通过HDBS直接呈现)

五、直接内存

Java进阶之理解JVM内存区域——运行时数据区_第6张图片

  • 不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域;
  • 若使用NIO,此区域会被频繁使用,Java堆内可用directByteBuffer对象直接引用操作;
  • 此块内存不受Java堆大小限制,但受本机总内存限制,可通过MaxDirectMemorySize设置(默认与堆内存最大值一样),故也会出现OOM异常。

六、从底层深入理解运行时数据区

6.1 流程
申请内存–》类加载(class进入方法区)–》常量 静态变量进方法区 --》虚拟机栈入栈桢 --》栈帧的方法执行。

6.2 内存可视化工具HSDB:监控JVM内存运行情况
执行以下命令后会弹出HSDB窗口:
E:\jdk\lib>java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB

然后再另开窗口在对应执行的类的目录后执行 jps 命令则会显示当前正在运行的进程,将进程对应ID输入HSDB中则可查看其中运行的线程,点击 Stack Memory 则可看到对应虚拟机栈以及各个栈帧: F:\androidproject\PracticeDemo\app\src\main\java\com\example\practicedemo\jvm\ex1>jps
Java进阶之理解JVM内存区域——运行时数据区_第7张图片

七、辨析堆和栈

1 功能

  • 栈是以栈帧的方式存储方法调用的过程,并存储方法调用过程中基本数据类型的变量(int、short、long、byte、float、double、boolean、char等)以
    及对象的引用变量,其内存分配在栈上,变量出了作用域就会自动释放;
  • 堆内存用来存储Java中的对象。无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
    2 线程独享还是共享
  • 栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存。
  • 堆内存中的对象对所有线程可见。堆内存中的对象可以被所有线程访问

3 空间大小
栈的内存要远远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError问题

八、内存溢出及解决方案

1 堆溢出
JVM在启动的时候会自动设置JVM Heap的值, 可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap的大小是Young Generation 和Tenured Generaion 之和。
在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息。
分清 内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用。不足则通过 调大 -Xms,-Xmx参数。
解决方法
手动设置JVM Heap(堆)的大小。

2 栈溢出:程序所要求的栈深度过大导致。
解决方法
1:修改程序。2:通过 -Xss: 来设置每个线程的Stack大小即可。

3 方法区溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象
解决方法
通过-XX:PermSize和-XX:MaxPermSize设置永久代大小即可。

4 本机直接溢出
出现这种情况的原因基本下面2点:

  • 程序创建的线程数超过操作系统的限制。
  • JVM占用的内存太多,导致创建线程的内存空间太小。
    解决方法
    1:增大进程所占用的总内存。
    2:减少-Xmx或者-Xss来达到创建更多线程的目的。

九、虚拟机优化技术

1 编译优化技术:方法内联
2 栈的优化技术:栈帧之间数据共享

你可能感兴趣的:(Java基础知识及进阶)