一、JVM内存模型图
二、概念
1. 堆
用来存储对象的详细数据的内存空间。每new 一个对象,就会在堆上分配一块内存用来存储对象信息。堆又分为新生代和老年代,新生代和老年代的内存空间占比默认为(1:3)
新生代用来存放被新创建的对象信息,新生代又分为Eden区、S0区、S1区。默认占比为(8:1:1)
老年代用来存放年龄超过阈值(默认15)的对象信息,或大对象信息。
2.线程栈
每一个正在运行的线程都会拥有一个线程栈,用来跟踪现在中方法的调用过程。每当线程调用一个方法时,就会向线程栈中压入一个栈帧;当一个方法被执行完成时,就会从线程栈中弹出该栈帧。
2.1.栈帧
栈帧用来存储方法的信息,每一个栈帧中都包含局部变量表、操作数栈、动态链接和方法出口。
2.1.1 局部变量表
局部变量表也称之为局部变量数组或者本地变量表。
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。
每一个栈帧中都包含局部变量表,局部变量表用来存储方法中的局部变量信息,基本数据类型的变量存储在局部变量表中,引用类型变量在局部变量表中存储的是对象的引用。
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。
2.1.2 操作数栈
每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(的 操作数栈,也可以称之为 表达式栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。.
2.1.3 动态链接
每个栈帧都包含一个对当前方法类型的运行时常量池的引用,以支持方法代码的动态链接。(是对常量池的引用)
当方法A入栈后,栈帧数据区中的动态链接会持有对当前方法所属类的常量池的引用,当A中调用了方法B(符号引用),就可以通过常量池查找到具体的直接引用(方法地址)。
2.1.4方法出口
当一个方法开始执行时,可能有两种方式退出该方法:
正常完成出口:是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时通过throw语句显示抛出的异常)。如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定。
异常完成出口:是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出。
无论是Java虚拟机抛出的异常还是代码中使用throw指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出。
无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。
方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压如调用者的操作数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。
一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息。
3.本地方法栈
用来执行本地的内存空间。本地方法:用navite修饰的方法。
4.方法区
用来存储常量、静态变量、类信息的的内存空间。
5.程序计数器
程序计数器是计算机处理器中的寄存器,它包含当前正在执行的指令的地址(位置)。当每个指令被获取,程序计数器的存储地址加一。在每个指令被获取之后,程序计数器指向顺序中的下一个指令。当计算机重启或复位时,程序计数器通常恢复到零。
三、GC(垃圾回收)
1. 概念
GC垃圾回收只在堆内存中进行。
2.垃圾回收算法
2.1. 标记清除算法
标记清除包含两个步骤。标记和清除。
2.1.1 标记和清除
标记:通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。
清除:将内存中标记的垃圾所占用的内存释放。
注意:此处的释放,并不是将内存中的字节清除,而只是将该内存的开始地址和结束地址记录在空闲内存地址列表中,当有新对象创建时,可以使用该地址。
2.1.2 优缺点
优点:速度快,因为标记清除算法,只需要将垃圾标记,并将垃圾所占用的内存地址释放即可,不需要做其他操作。
缺点:会产生碎片内存,浪费内存。因为垃圾在内存中可能是随机分布的,所以清理释放的内存也是随机分布的。若创建一个新对象,例如数组,需要一片连续的内存,总的空闲内存又可以容纳该新对象,但释放得到的每一个空闲内存都不足以容纳该新对象,就会照成内存溢出。
2.2 标记整理算法
标记整理也包含两个步骤。标记和整理。
2.2.1 标记和整理
标记:通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。
整理:在对内存中的垃圾进行清除时,同时也会对内存进行整理,将非垃圾的对象向前移动,使内存更加紧凑,使空闲内存连续。这样就避免了产生内存碎片的问题。
2.2.2 优缺点
优点:避免了内存碎片的产生,内存利用率更高。
缺点:由于整理牵扯到了对象的移动,效率会变得较低,对象在整理过程中要移动,移动的过程中如果有一些局部变量 引用了存活的对象,肯定需要改变引用的引用地址,对象在内存中的位置变了,地址变了,肯定涉及的工作就比较多一些,前者到内存区块的拷贝移动,还要把所有引用的地址加以改变。
2.3 复制算法
复制算法比较特殊,它包含标记、复制、清除、交换四个步骤。它需要将内存分为两块区域,分别未from区域和to区域,from区域用来存储对象,to区域只会在垃圾回收时作为可用对象的临时存放区,在对from垃圾清理后,to区域会变为from区域。
2.3.1 步骤
标记:对from区域的对象通过对枚举GCroot对象做引用可达性分析,即从GC root对象开始,向下搜索,形成的路径称之为 引用链。如果一个对象到GC roots对象没有任何引用,没有形成引用链,那么该对象等待GC回收。
复制:将未被标记的对象复制到to区域。
清除:将from区域中的垃圾对象所占用的内存释放。
交换:将from区域和to区域交换,即to区域变为from区域。
2.3.2 优缺点
优点:不会产生内存碎片。
缺点:会暂用双倍的内存空间,内存空间利用率低。
上面所讲的几种垃圾回收算法在JVM垃圾回收机制中会根据不同的情况被采用,并不是说只是用某一种。
2.4 分代垃圾回收算法
2.4.1 步骤
堆内存会被分为新生代和老年代,新生代和老年代默认内存占比为1:3。
新生代又会被分为Eden(伊甸园区)、S0(from)、S1(to)三部分,这三部分默认占比为8:1:1。
新创建的对象一般默认被存储在Eden区,大对象会被存储到老年代。
当Eden区剩余内存不够容纳新创建对象时,会触发Minor GC,存活对象会被复制到S0区,并且,年龄增加一,释放Eden区垃圾所占内存。
当Eden区和S0区剩余内存不够容纳新对象时,会触发Minor GC,Eden区和S0区存活的对象都会被复制到S1区,并且年龄增加1,清除Eden区和S0区垃圾,再交换from、to。
当Eden区和S1区剩余的内存不够容纳新对象时,会触发Minor GC,Eden区和S0区存活的对象都会被复制到S0区,并且年龄增加1,清除Eden区和S0区垃圾,再交换from、to。
当新生代中对象年龄超过阈值(默认15)时,会晋升至老年代。当老年代中内存不足时,会尝试进行一次Minor GC,Minor GC后,内存仍然不足时,会触发Full GC。
2.4.2 Minor GC 和 Full GC
Minor GC: 新生代进行的GC。速度快。
触发条件:新生代内存不够容纳新对象。
Full GC: 老年代进行的GC,速度较慢,通常一次Full GC至少会伴随着一次Minor GC。
触发条件:老年代内存不够容纳晋升对象或新对象。
四、JVM调优常用参数
参数 | 作用 |
---|---|
-Xms | 堆初始大小 |
-Xmx 或 -XX:MaxHeapSize=size | 堆最大大小 |
-Xmn 或 (-XX:NewSize=size + -XX:MaxNewSize=size ) | 新生代大小 |
-XX:InitialSurvivorRatio=ratio 和 -XX:+UseAdaptiveSizePolicy | 幸存区比例(动态) |
-XX:SurvivorRatio=ratio | 幸存区比例 |
-XX:MaxTenuringThreshold=threshold | 晋升阈值 |
-XX:+PrintTenuringDistribution | 晋升详情 |
-XX:+PrintGCDetails -verbose:gc | GC log打印 |
-XX:+ScavengeBeforeFullGC | FullGC 前 MinorGC |