目录
JVM 组成部分
Java虚拟机是Java程序运行的核心环境,它由多个部分组成,主要包括以下几个部分:
-
类加载器
-
运行时数据区:
- 方法区
- 堆(Heap)
- 程序计数器
- Java虚拟机栈
- 本地方法栈
-
执行引擎
- 解释器
- 及时编译器
-
本地方法接口
运行时数据区
Java虚拟机在运行Java程序过程中管理的内存区域,称之为运行时数据区。运行时数据区分为方法区、堆、程序计数器、Java虚拟机栈和本地方法栈五个区域。
程序计数器
程序计数器可以简单理解为一块小内存空间,它的作用类似于是一本“书签”,用来记录下一步将要执行的指令位置。当JVM执行一串指令时,程序计数器会指向当前执行指令的下一条指令的地址。每个线程都有自己的程序计数器,它们彼此独立,互不影响,当发生线程上下文切换时,通过程序计数器,能获取到需要执行的下一条指令的地址,保证了线程切换后能够从正确的位置继续执行。
Java虚拟机栈
Java虚拟机栈是每个线程私有的内存区域,一般就是我们所说的栈内存,栈内存用于存储方法调用的局部变量、操作数栈、方法出口等信息。Java虚拟机栈采用栈(stack)的数据结构。栈是一种后进先出(Last In First Out, LIFO)的数据结构,每个方法在执行时会创建一个栈帧,栈帧随着方法的调用和返回动态压栈和弹栈。
栈帧
每当线程调用一个方法时,虚拟机会在栈上分配一个称为栈帧(stack frame)的数据结构。栈帧包含了方法的局部变量表、操作数栈、动态链接、方法出口等信息,它们组成了方法执行的上下文环境。当方法执行结束时,对应的栈帧会从虚拟机栈中弹出并销毁,控制权回到调用该方法的方法栈帧中。
局部变量表
局部变量表的作用是在方法执行过程中存放所有的局部变量。源代码编译成字节码文件时就可以确定局部变量表的内容。栈帧中的局部变量表是一个数组,数组中每一个位置称之为槽(slot),每个槽可以存储4个字节的数据,因此,对于基本数据类型的变量而言,long和double类型的变量占用两个槽,其他类型占用一个槽。引用类型的局部变量会在局部变量表中存储一个指向对象的引用(通常是对象在堆中的地址)。这个引用本身占用 4 字节(在 32 位 JVM)或 8 字节(在 64 位 JVM)。
实例方法中的序号为0的位置存放的是this,指向的是当前调用方法的对象,运行时会在内存中存放实例对象的地址。方法参数也会保存在局部变量表中,其顺序与方法中参数定义的顺序一致。
为了节省空间,局部变量表中的槽是可以复用的,一旦某个局部变量不再生效,当前槽就可以再次被使用。
操作数栈
操作数栈是栈帧中虚拟机在执行指令过程中用来存放中间数据的一块区域。他是一种栈式的数据结构,如果一条指令将一个值压入操作数栈,则后面的指令可以弹出并使用该值。在编译期就可以确定操作数栈的最大深度,从而在执行时正确的分配内存大小。
帧数据
帧数据里面包含了动态链接、方法出口和异常表三个部分。
动态链接
当前类的字节码指令引用了其他类的属性或者方法时,需要将符号引用(编号)转换成对应的运行时常量池中的内存地址。动态链接就保存了编号到运行时常量池的内存地址的映射关系。以下是我个人的理解,不一定正确:
JVM中的动态链接,实际上就是方法运行时,需要访问别的类的方法或属性时,从被访问的类的运行时常量池中取得需要的类或方法的地址,然后保存到调用者的栈帧里面的动态链接中,将符号引用和获取到的地址关联起来,从而实现符号引用转换成实际引用。
方法出口
方法出口指的是方法在正确或者异常结束时,当前栈帧会被弹出,同时程序计数器应该指向上一个栈帧中的下一条指令的地址。所以在当前栈帧中,需要存储此方法出口的地址。
异常表
异常表存放的是代码中异常的处理信息,包含了异常捕获的生效范围以及异常发生后跳转到的字节码指令位置,先当与try catch代码块的catch部分。
栈内存溢出
虚拟机栈如果栈帧过多,占用内存超过栈内存可以分配的最大大小就会出现内存溢出,Java虚拟机栈内存溢出时会出StackOverflowError的错误,遇到栈内存溢出,有可能是递归操作时没有设置推出的条件,导致方法一直被调用,栈帧一直被创建。如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构:
如果需要设置栈内存的大小,可以使用以下虚拟机参数:
-Xss1048576
-Xss1024K
-Xss1m
-Xss1g
局部变量过多、操作数栈深度过大也会影响栈内存的大小。一般情况下,工作中即便使用了递归进行操作,栈的深度最多也只能到几百,不会出现栈的溢出。所以此参数可以手动指定为 Xss256k 节省内存。
本地方法栈
Java虚拟机栈存储了Java方法调用时的栈帧,而本地方法栈存储的是native本地方法的栈帧。在Hotspot虚拟机中,Java虚拟机栈和本地方法栈实现上使用了同一个栈空间。本地方法栈会在栈内存上生成一个栈帧,临时保存方法的参数同时方便出现异常时也把本地方法的栈信息打印出来。
堆区(Heap)
一般Java程序中堆内存是空间最大的一块内存区域。创建出来的对象都存在于堆上。栈上的局部变量表中,可以存放堆上对象的引用。静态变量也可以存放堆对象的引用,通过静态变量就可以实现对象在线程之间共享。
堆内存大小是有上限的,当对象一直向堆中放入对象达到上限之后,就会抛出OutOfMemory错误。
堆空间有三个需要关注的值,used、total、max。used指的是当前已使用的堆内存,total是java虚拟机已经分配的可用堆内存,max是java 虚拟机可以分配的最大堆内存。随着堆中的对象增多,当total可以使用的内存即将不足时,java虚拟机会继续分配内存给堆,如果堆内存不足,java虚拟机就会不断的分配内存,total值会变大。total最多只能与max相等。
如果不设置任何的虚拟机参数,max 默认是系统内存的1/4,total默认是系统内存的1/64 。在实际应用中一般都需要设置total和max的值。要修改堆的大小,可以使用虚拟机参数 Xmx(max 最大值)和 Xms(初始的total)。
-Xms6291456
-Xms6144k
-Xms6m
-Xmx83886080
-Xmx81920k
-Xmx80m
Java服务端程序开发时,建议将 Xmx 和 Xms 设置为相同的值,这样在程序启动之后可使用的总内存就是最大内存,而无需向java 虚拟机再次申请,减少了申请并分配内存时间上的开销,同时也不会出现内存过剩之后堆收缩的情况。
方法区
方法区是用来存储每个类的基本信息,如类的名称、父类信息、实现的接口、字段描述、方法描述和运行时常量池的引用等,这个元信息一般称之为InstanceKlass,InstanceKlass对象的创建在类的加载阶段完成。
方法区实现
方法区是《Java 虚拟机规范》 中设计的虚拟概念,每款Java 虚拟机在实现上都各不相同。Hotspot 设计如下:
-
JDK7及之前的版本将方法区存放在堆区域中的永久代空间,使用以下虚拟机参数可以控制永久代大小:
-XX:MaxPermSize
-
JDK8及之后的版本将方法区存放在元空间中,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配,使用以下参数可以限制元空间的大小:
-XX:MaxMetaspaceSize
运行时常量池
方法区除了存储类的元信息之外,还存放了运行时常量池。字节码文件中通过编号查表的方式找到常量,这种常量池称为静态常量池。当常量池加载到内存中之后,可以通过内存地址快速的定位到常量池中的内容,这种常量池称为运行时常量池。
常量不必非要使用 final 修饰才算是常量,只要JVM任务某个变量事实上不会被改变,那么也会将其视为常量。
字符串常量池
方法区中除了类的元信息、运行时常量池之外,还有一块区域叫字符串常量池 StringTable 。字符串常量池存储在代码中定义的常量字符串内容。比如“123” 这个123 就会被放入字符串常量池。
早期设计时,字符串常量池是属于运行时常量池的一部分,他们存储的位置也是一致的。后续做出了调整,将字符串常量池和运行时常量池做了拆分。
String.intern()方法是可以手动将字符串放入字符串常量池中。
静态变量的存储
JDK6及之前的版本中,静态变量是存放在方法区中的,也就是永久代。JDK7及之后的版本中,静态变量是存放在堆中的Class对象中,脱离了永久代。
直接内存
直接内存(Direct Memory)并不在《Java 虚拟机规范》中存在,所以并不属于Java运行时的内存区域。在JDK 1.4中引入了 NIO 机制,使用了直接内存,主要为了解决以下两个问题
- Java 堆中的对象如果不再使用要回收,回收时会影响对象的创建和使用。
- IO 操作比如读文件,需要先把文件读入直接内存(缓冲区)再把数据复制到Java 堆中。现在直接放入直接内存即可,同时Java堆上维护直接内存的引用,减少了数据复制的开销。写文件也是类似的思路。
直接内存的分配通常通过 ByteBuffer 方法来实现,释放则由Java虚拟机自动管理。
ByteBuffer.allocateDirect()
如果需要手动控制直接内存的大小,可以使用以下虚拟机参数:
-XX:MaxDirectMemorySize=<size>