JVM(一)内存区域

2024 年 6 月 20 日 星期四(已编辑)
/
47
这篇文章上次修改于 2024 年 7 月 5 日 星期五,可能部分内容已经不适用,如有疑问可询问作者。

JVM(一)内存区域

运行时数据区

运行时数据区是什么

虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干不同的数据区,这些区域有各自的用途、创建和销毁时间。

线程私有:程序计数器、Java虚拟机栈、本地方法栈

线程共享:Java堆、方法区

运行时数据区

运行时数据区

程序计数器

是什么:程序计数器是一块较小的内存空间,可以看作当前线程所执行字节码的行号指示器。

干什么:程序计数器主要有两个作用:

  • 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理(如果线程正在执行Java方法,计数器记录正在执行的虚拟机字节码指令地址。如果是本地方法,计数器值为Undefined)
  • 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了(为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,所以属于线程私有嘛)

⚠️ 注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡

Java虚拟机栈

Java虚拟机用于方法调用。

  • 干什么:Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过Java虚拟机栈来实现的
  • 怎么干:方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出
  • 分配:每当有新线程创建时就会分配一个栈空间,线程结束后栈空间被回收,栈与线程拥有相同的生命周期
  • 结构:栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址(方法出口)
    • 局部变量表:存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(this)
    • 操作数栈:主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中
    • 动态链接:主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用
    • 方法返回地址:当一个方法调用完成后,需要返回到调用该方法的位置继续执行。方法返回地址就是用来记录方法调用完成后应该返回到哪里继续执行

有两类异常:

  • 线程请求的栈深度大于虚拟机允许的深度抛出StackOverflowError
  • 如果JVM栈容量可以动态扩展,栈扩展无法申请足够内存抛出OutOfMemoryError(HotSpot不可动态扩展,不存在此问题)

本地方法栈

本地方法栈与虚拟机栈作用相似,不同的是虚拟机栈为虚拟机执行Java方法服务,本地方法栈为本地方法服务。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

虚拟机规范对本地方法栈中方法的语言与数据结构无强制规定,虚拟机可自由实现,例如HotSpot将虚拟机栈和本地方法栈合二为一。

是什么:堆是虚拟机所管理的内存中最大的一块,被所有线程共享的

干什么:堆用来存放对象实例,Java里几乎所有对象实例都在堆分配内存

创建:在虚拟机启动时创建

堆结构:

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)(Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代)
  2. 老生代(Old Generation)
  3. 永久代(Permanent Generation)

JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存。

堆内存结构

堆内存结构

特点:

  • 堆既可以被实现成固定大小,也可以是可扩展的
  • 堆可以处于物理上不连续的内存空间,逻辑上应该连续,但对于例如数组这样的大对象,多数虚拟机实现出于简单、存储高效的考虑会要求连续的内存空间。

异常:如果堆没有内存完成实例分配也无法扩展,抛出OutOfMemoryError

方法区

是什么:方法区属于是 JVM 运行时数据区域的一块逻辑区域,是各个线程共享的内存区域

干什么:当虚拟机要使用一个类时,它需要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已被虚拟机加载的 类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据

创建:

JDK8之前使用永久代实现方法区,容易内存溢出,因为永久代有-XX:MaxPermSize上限,即使不设置也有默认大小。

JDK7把放在永久代的字符串常量池、静态变量等移出方法区

JDK8中永久代完全废弃,改用在本地内存中实现的元空间代替,把JDK7中永久代剩余内容(主要是类型信息)全部移到元空间

特点:虚拟机规范对方法区的约束宽松,除和堆一样不需要连续内存和可扩展外,还可以不实现垃圾回收。垃圾回收在方法区出现较少,主要目标针对常量池和类型卸载

异常:如果方法区无法满足新的内存分配需求,将抛出OutOfMemoryError

运行时常量池

是什么:运行时常量池是方法区的一部分,它用于存放编译期生成的各种字面量和符号引用

干什么:运行时常量池的主要作用是存放编译期生成的各种字面量和符号引用,为程序的运行提供支持。

  • Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表,用于存放编译器生成的各种字面量与符号引用,这部分内容在类加载后存放到运行时常量池。
  • 一般除了保存Class文件中描述的符号引用外,还会把符号引用翻译的直接引用也存储在运行时常量池(存符号引用和直接引用)

创建:运行时常量池随着类的加载而加载

特点:运行时常量池相对于Class文件常量池的一个重要特征是动态性,Java不要求常量只有编译期才能产生,运行期间也可以将新的常量放入池中,这种特性利用较多的是String的intern方法(jdk7后放字符串常量池)

异常:运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutofMemoryError.

字符串常量池

是什么:字符串常量池是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建

直接内存

是什么:直接内存是一种特殊的内存缓冲区,并不在 Java 堆或方法区中分配的,而是通过 JNI (Java 本地接口)的方式在本地内存上分配的。

干什么:JDK1.4中新加入了NIO这种基于通道与缓冲区的IO,它可以使用Native函数库直接分配堆外内存,通过一个堆里的DirectByteBuffer对象作为内存的引用进行操作,避免了在Java堆和Native堆来回复制数据。

特点:

  • 直接内存的分配不受Java堆大小的限制,但还是会受到本机总内存及处理器寻址空间限制,一般配置虚拟机参数时会根据实际内存设置-Xmx等参数信息,但经常忽略直接内存,使内存区域总和大于物理内存限制,导致动态扩展时出现OOM
  • 由直接内存导致的内存谥出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现内存溢出后产生的Dump文件很小,而程序中又直接或间接使用了直接内存(典型的间接使用就是NIO),那么就可以考虑检查直接内存方面的原因

内存溢出和内存泄漏的区别?

内存溢出OutOfMemory,指程序在申请内存时,没有足够的内存空间供其使用。

内存泄露Memory Leaak,指程序在申请内存后,无法释放已申清的内存空间,内存泄漏最终将导致内存溢出。

栈溢出的原因?

由于HotSpot不区分虚拟机和本地方法栈,设置本地方法栈大小的参数没有意义,栈容量只能由-Xss参数来设定,存在两种异常:

  • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError,例如一个递归方法不断调用自己。该异常有明确错误堆栈可供分析,容易定位到问题所在。
  • OutOfMemoryError:如果JVM栈可以动态扩展,当扩展无法申请到足够内存时会抛出OutOfMemoryError。.
    • HotSpot不支持虚拟机栈扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现OOM,否则在线程运行时是不会因为扩展而导致溢出的。

运行时常量池溢出的原因?

String的intern方法是一个本地方法,作用是如果字符串常量池中已包含一个等于此String对象的字符串,则返回池中这个字符串的String对象的引用,否则将此String对象包含的字符串添加到常量池并返回此String对象的引用。

在JDK6及之前常量池分配在永久代,因此可以通过-XX:PermSize和-XX:MaxPermSize限制永久代大小间接限制常量池。在while死循环中调用intern方法导致运行时常量池溢出。在JDK7后不会出现该问题,因为存放在永久代的字符串常量池已经被移至堆中(会堆溢出)。

方法区溢出的原因?

方法区主要存放类型信息,如类名、访问修饰符、常量池、字段描述、方法描述等。只要不断在运行时产生大量类,方法区就会溢出。例如使用JDK反射或CGLib直接操作字节码在运行时生成大量的类(动态代理等)。很多框架如Spring、Hibernate等对类增强时都会使用CGLib这类字节码技术,增强的类越多就需要越大的方法区保证动态生成的新类型可以载入内存,也就更容易导致方法区溢出。

JDK8使用元空间取代永久代,HotSpot提供了一些参数作为元空间防御惜施,例如-XX:MetaspaceSize指定元空间初始大小,达到该值会触发GC进行类型卸载,同时收集器会对该值进行调整,如果释放大量空间就适当降低该值,如果释放很少空间就适当提高。

不同JDK版本之间运行时数据区区别

JDK6

JDK7

JDK8

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...