简介
JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。
JVM 内存结构
JVM 的内存空间分为 3 大部分:
- 堆内存:
- 方法区
- 栈内存
其中栈内存可以再细分为Java 虚拟机栈和本地方法栈,堆内存可以划分为新生代和老年代,新生代中还可以再次划分为 Eden 区、From Survivor 区和 To Survivor 区。
其中一部分是线程共享的,包括 Java 堆和方法区;另一部分是线程私有的,包括虚拟机栈和本地方法栈,以及程序计数器这一小部分内存。
堆内存(Heap)
Java 堆(Java Heap)是 Java 虚拟机所管理的内存中最大的一块。堆是被所有线程共享的区域,实在虚拟机启动时创建的。堆里面存放的都是对象的实例(new 出来的对象都存在堆中)。
此内存区域的唯一目的就是存放对象实例(new 的对象),几乎所有的对象实例都在这里分配内存。
堆内存分为两个部分:年轻代和老年代。我们平常所说的垃圾回收,主要回收的就是堆区。更细一点划分新生代又可划分为 Eden 区和 2 个 Survivor 区(From Survivor 和 To Survivor)。
下图中的 Perm 代表的是永久代,但是注意永久代并不属于堆内存中的一部分,同时 jdk1.8 之后永久代已经被移除,存放到了元空间里面。
新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定).
默认的,Eden : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
方法区(Method Area)
方法区也称”永久代“,它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。
在 JDK8 之前的 HotSpot JVM,存放这些”永久的”的区域叫做“永久代(permanent generation)”。永久代是一片连续的堆空间,在 JVM 启动之前通过在命令行设置参数-XX:MaxPermSize 来设定永久代最大可分配的内存空间,默认大小是 64M(64 位 JVM 默认是 85M)。
随着 JDK8 的到来,JVM 不再有 永久代(PermGen)。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)。
方法区或永生代相关设置:
- -XX:PermSize=64MB 最小尺寸,初始分配
- -XX:MaxPermSize=256MB 最大允许分配尺寸,按需分配
- XX: CMSClassUnloadingEnabled -XX: CMSPermGenSweepingEnabled 设置垃圾不回收
- 默认大小
- -server 选项下默认 MaxPermSize 为 64m
- -client 选项下默认 MaxPermSize 为 32m
虚拟机栈(JVM Stack)
Java 虚拟机栈是线程私有,生命周期与线程相同。创建线程的时候就会创建一个 Java 虚拟机栈。
虚拟机执行 Java 程序的时候,每个方法都会创建一个栈帧,栈帧存放在 Java 虚拟机栈中,通过压栈出栈的方式进行方法调用。
栈帧又分为一下几个区域:局部变量表、操作数栈、动态连接、方法出口等。
局部变量表
局部变量表是变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在 Java 编译成 class 文件的时候,就在方法的 Code 属性的 max_locals 数据项中确定该方法需要分配的最大局部变量表的容量。
局部变量表的容量以变量槽(Slot)为最小单位,32 位虚拟机中一个 Slot 可以存放 32 位(4 字节)以内的数据类型( boolean、byte、char、short、int、float、reference 和 returnAddress 八种)
对于 64 位长度的数据类型(long,double),虚拟机会以高位对齐方式为其分配两个连续的 Slot 空间,也就是相当于把一次 long 和 double 数据类型读写分割成为两次 32 位读写。
reference 类型虚拟机规范没有明确说明它的长度,但一般来说,虚拟机实现至少都应当能从此引用中直接或者间接地查找到对象在 Java 堆中的起始地址索引和方法区中的对象类型数据。
Slot 是可以重用的,当 Slot 中的变量超出了作用域,那么下一次分配 Slot 的时候,将会覆盖原来的数据。Slot 对对象的引用会影响 GC(要是被引用,将不会被回收)。 系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
系统不会为局部变量赋予初始值(实例变量和类变量都会被赋予初始值)。也就是说不存在类变量那样的准备阶段。
操作数栈
操作数栈和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。
操作数栈的每一个元素可用是任意的 Java 数据类型,包括 long 和 double。32 位数据类型所占的栈容量为 1,64 位数据类型占用的栈容量为 2。
当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈 / 入栈操作(例如:在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的)。
在概念模型里,栈帧之间是应该是相互独立的,不过大多数虚拟机都会做一些优化处理,使局部变量表和操作数栈之间有部分重叠,这样在进行方法调用的时候可以直接共用参数,而不需要做额外的参数复制等工作。重叠过程如图所示:
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,Class 文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中方法的符号引用为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用(静态方法,私有方法等),这种转化称为静态解析,另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。
方法返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:
- 执行引擎遇到任意一个方法返回的字节码指令:传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法来返回指令决定,这种退出的方法称为正常完成出口。
- 方法执行过程中遇到异常: 无论是 Java 虚拟机内部产生的异常还是代码中 thtrow 出的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出的方式称为异常完成出口,一个方法若使用该方式退出,是不会给上层调用者任何返回值的。无论使用那种方式退出方法,都要返回到方法被调用的位置,程序才能继续执行。方法返回时可能会在栈帧中保存一些信息,用来恢复上层方法的执行状态。一般方法正常退出的时候,调用者的 pc 计数器的值可以作为返回地址,帧栈中很有可能会保存这个计数器的值作为返回地址。方法退出的过程就是栈帧在虚拟机栈上的出栈过程,因此退出时的操作可能有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者的操作数栈每条整 pc 计数器的值指向调用该方法的后一条指令。
本地方法栈(Native Stack)
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。
程序计数器(PC Register)
程序计数器就是记录当前线程执行程序的位置,改变计数器的值来确定执行的下一条指令,比如循环、分支、方法跳转、异常处理,线程恢复都是依赖程序计数器来完成。 Java 虚拟机多线程是通过线程轮流切换并分配处理器执行时间的方式实现的。为了线程切换能恢复到正确的位置,每条线程都需要一个独立的程序计数器,所以它是线程私有的。
直接内存
直接内存并不是虚拟机内存的一部分,也不是 Java 虚拟机规范中定义的内存区域。jdk1.4 中新加入的 NIO,引入了通道与缓冲区的 IO 方式,它可以调用 Native 方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。
JVM 内存参数设置
- -Xms 设置堆的最小空间大小。
- -Xmx 设置堆的最大空间大小。
- -Xmn:设置年轻代大小
- -XX:NewSize 设置新生代最小空间大小。
- -XX:MaxNewSize 设置新生代最大空间大小。
- -XX:PermSize 设置永久代最小空间大小。
- -XX:MaxPermSize 设置永久代最大空间大小。
- -Xss 设置每个线程的堆栈大小
- -XX: UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
- -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
参考文档
https://zhuanlan.zhihu.com/p/58896619
https://juejin.im/post/5db84c79f265da4d2970f034