一、Java虚拟机内存模型
· JVM内存区域模型
如上图所示,JVM内存区域分为程序计数器、虚拟机栈、本地方法栈、堆、方法区这5个区域,来保证程序正常运行。
· 程序计数器
程序计数器在JVM内存中是一块很小的空间,他是当前线程所需要执行的字节码的行号指示器。字节码可通过改变程序计数器中的值来选择跳转、分支、循环、异常处理等指令。因此每个线程都有自己的一个程序计数器,并且多个线程之间的程序计数器互相不干扰,是线程私有的并且生命周期与线程相同。
如果当前线程执行的是java方法那么程序计数器记录的是当前正在执行的java字节码地址。如果当前线程执行的是native方法,那么程序计数器就不会记录,并且当前程序计数器为空值。
· 虚拟机栈
Java虚拟机栈已在之前发布的文章中提到过,所以不做具体阐述。他是线程私有的内存空间,并且生命周期也与线程相同。虚拟机栈的核心是栈帧,栈帧中存放着当前线程方法的局部变量表、操作数栈、动态链接、返回地址等信息。一个线程中的方法被调用就会创建一个栈帧(入栈),方法执行的结束意味着这个栈帧也会被销毁(出栈)。
栈帧结构如下图:
· 本地方法栈
本地方法栈负责管理的是虚拟机本地方法(native方法)的调用。他的功能与虚拟机栈是类似的(虚拟机栈负责管理的是java方法的调用)。
· 堆
堆的空间在虚拟机中拥有分配最大的空间。因为所有线程中对象实例的创建都会在堆空间中并且堆的空间是所有线程共享的,因此在堆中会发生很频繁的gc操作。
堆空间中分为新生代和老年代两块区域,新生代中主要存放的是刚刚产生和一些被gc回收后仍幸存的对象实例,老生代中主要存放的是一直没被gc回收的对象实例。因此当对象刚被new出来时会被放入新生代中,经过多次gc回收该对象仍未被回收,则会被移入到老年代中。
堆空间中的新生代中又被分为eden区、幸存区(有些jvm若选择了标记-复制回收算法,则会存在2个幸存区from space,to space)。
堆空间结构如下图:
设置JVM参数模拟gc回收过程:-XX:+PrintGCDetails -Xmx6M -Xms6M -Xmn3M。表示最大堆内存为6M,新生代内存空间为3M。
运行上述图中代码,gc结果如下图:
由于修改jvm参数仍调试不出minor gc的过程,所以配图只有full gc的过程。可以发现在经历full gc后,新生代空间被清空。b1、b2对象为被清空,进入老生代。
· 方法区
方法区内存空间也是被线程所共享的,他主要存储被虚拟机加载的类信息、常量池、静态变量。上图中方法区的空间为metaSpace。方法区中存储的数据大部分来自于java的class文件,也是java应用程序运行的重要数据。
当方法区中空间达到饱和状态时,也会唤起full gc来进行回收。如果不能及时回收也会像堆空间一样抛出OutOfMemoryError异常。
使用之前文章中String中提到过的intern()方法来将字符串直接加入方法区中的常量池来模拟方法区中的gc。
设置JVM参数-XX:+PrintGCDetails -XX:PermSize=2M -XX:MaxPermSize=4M。设置方法区初始值为2M,最大的方法区空间为4M。
运行上述代码,gc结果如下:
[Perm: 4096K->528K(4096K)] 4096K->536K(4096K), 0.0139378 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Perm: 4096K->512K(71680K)] 4096K->520K(4096K), 0.0246657 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
[Perm: 4096K->848K(71680K)] 4096K->864K(159232K), 0.0830849 secs] [Times: user=0.08 sys=0.01, real=0.09 secs]
二、VM Options参数设置
· 设置最大堆内存-Xmx
-Xmx可以用来设置堆的最大空间。-Xmx参数设置的不同,将直接决定程序何时会抛出OutOfMemory异常。
设置-Xmx1M后运行上图程序,运行结果:
第1次分配空间
第2次分配空间
第3次分配空间
第4次分配空间
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
发现,第5次分配空间时堆内存空间就超过了设置的最大值1M,导致抛出内存溢出异常。程序中将bytes放入list集合中目的是为了给bytest增加强引用,保证他不被垃圾回收掉。
设置-Xmx2M后运行上图程序,运行结果:
第12次分配空间
第12次分配空间
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
· 设置最小堆空间-Xms
-Xms可以用来设置最小堆空间,程序在运行时,被分配空间为当前设置的最小堆内存,所以jvm会维护该最小堆内存,通过gc来保证程序处于最小堆内存中。当最小堆内存不能满足于当前程序所需要的空间时,才会使用比最小堆内存更大的空间,但是堆空间仍不能超过-Xmx设置的最大空间,否则也会抛出OutOfMemoryError。
设置JVM参数-Xms4M -Xms10M,运行结果如下:
[GC 7900K->6876K(9728K), 0.0006741 secs]
[GC 7900K->6876K(9728K), 0.0008844 secs]
[GC 7900K->7900K(9728K), 0.0026272 secs]
[Full GC 7900K->1756K(9728K), 0.0058599 secs]
[GC 7900K->6876K(9728K), 0.0007725 secs]
[GC 7900K->6876K(9728K), 0.0004481 secs]
[GC 7900K->7900K(9728K), 0.0005057 secs]
[Full GC 7900K->1756K(9728K), 0.0045034 secs]
为了保证程序在-Xms设定的范围内运行,会频繁的增加minor gc和full gc的次数,对性能产生一定影响。
提高为-Xms10M,运行结果:
[GC 7900K->4276K(9728K), 0.0001241 secs]
[GC 7900K->5816K(9728K), 0.0005144 secs]
[GC 7900K->6100K(9728K), 0.0009122 secs]
· 设置新生代-Xmn
参数-Xmn用于设置堆中新生代的大小。设置一个较大的新生代会减少老年代的大小,如果新生代的大小设置太小,会导致新生代空间不足造成频繁进行minor gc。
当设置-Xmn时,表示-XX:NewSize(新生代初始大小)和-XX:MaxNewSize(新生代的最大值)设置了相同的大小。
在新生代的设置中,不建议设置不同的-XX:NewSize和-XX:MaxNewSize,会导致频繁minor gc增加不必要的系统开销。只设置-Xmn即可。
· 设置堆中新生代的比例分配-XX:SurvivorRatio
参数-XX:SurvivorRatio用来设置堆中新生代的eden区和幸存区的比例。由于大部分gc算法都采用了分代回收算法,因此新生代一般会存在eden、from s1,to s2三块空间。
示例:-XX:SurvivorRatio=8 -Xmn10M 计算新生代三块空间占用大小。
-Xmn上述已经提到表示为新生代大小,所以这里新生代大小为10M。-XX:SurvivorRatio=8说明eden:from s1:to s2 = 8:1:1。所以eden区的大小为8M,from s1的大小为1M,to s2的大小为1M。
· 设置堆的比例分配-XX:NewRatio
参数-XX:NewRatio用来设置堆中老年代和新生代的比例。
示例:-XX:NewRatio=2 -Xmx30M -Xms 30M,计算新生代和老年代空间大小。
-Xmx30M和-Xms30M说明该堆空间为30M,-XX:NewRatio=2老年代:新生代为2:1,因此老年代大小为20M,新生代大小为10M。
· 设置方法区-XX:PermSize -XX:MaxPermSize
-XX:permSize表示方法区的初始大小,-XX:MaxPermSize表示方法区的最大值,若方法区空间超过了最大值,也会抛出OutOfMemoryError异常。
一般来说MaxPermSize设置为64MB或128MB已经可以满足大部分程序正常工作。
三、堆内存gc总结
堆被分为新生代和老生代,新生代被分为eden区和幸存区。当对象刚被new出来时,会被分配到新生代中的eden区,eden区的gc很频繁。随后经过gc多次回收,如果还存在,则会被移到幸存区,幸存区的gc相比eden区少了很多。经过gc多次回收后,若还存在,则会被移到老生代,老生代中不会很频繁的发生gc,如果老生代中的对象被回收那么很有可能造成系统的卡顿甚至崩溃。