通俗易懂理解JVM结构
说明:本篇内容是结合网上各位大牛的关于JVM的文章,通过作者的理解,希望以一种比较易懂的方式,让各位朋友们理解JVM到底是怎么一回事儿,其中部分图片和内容引用来自于网络,如有雷同,请见谅~~
一、JVM内存区域模型是啥样?
这个是JVM大致的内存分布模型,看起来比较直观:
这个是更精细化的JVM内存模型,区别主要是方法区和堆是公共内存区,其他是私有的:
1.方法区:
也称"永久代” 、“非堆”, 它用于存储虚拟机加载的类信息、常量、静态变量、是各个线程共享的内存区域。可以说方法区就是公共存放常量等静态的常量池。
运行时常量池:是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种符号引用,这部分内容将在类加载后放到方法区的运行时常量池中。
方法区默认大小:16MB,最大值为64MB(补充:看到还有资料说是根据物理内存大小调整的,)
-XX:PermSize 设置方法区大小
-XX:MaxPermSize 设置方法区最大限制
------说明:方法区新人看的时候,容易看不明白,本人理解是方法区就是堆(heap)中的永久代,两个称呼都称呼同一种内存区
2.虚拟机栈
描述的是java方法执行的内存模型:每个方法被执行的时候 都会创建一个“栈帧”用于存储局部变量表(包括参数)、操作栈、方法出口等信息。每个方法被调用到执行完的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。声明周期与线程相同,是线程私有的。
局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、 double)、对象引用(引用指针,并非对象本身),其中64位长度的long和double类型的数据会占用2个局部变量的空间,其余数据类型只占1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量是完全确定的,在运行期间栈帧不会改变局部 变量表的大小空间。
栈的默认大小是1M
-Xss2m 这样设置成2M
异常:Fatal:Stack size too small
异常的引起一般是线程数目太多
3.本地方法栈
即为一些Native方法分配的stack
异常:java.lang.OutOfMemoryError: unable to create new native thread
一般也是由线程太多引起,增加栈空间,同上方法
与虚拟机栈基本类似,区别在于虚拟机栈为虚拟机执行的java方法服务,而本地方法栈则是为Native方法服务。
------说明:VM栈和native栈一般是不用调整的,使用默认即可
4.堆
也叫做java堆、GC堆是java虚拟机所管理的内存中最大的一块内存区域,也是被各个线程共享的内存区域,在JVM启动时创建。该内存区域存放了对象实例及数组(所有new的对象)。
堆将会作为下节重点讲解
------说明:堆则是整个JVM调优的重点
二、JVM调优重点区域:堆
如下图所示,为Java堆中的各代分布:
Young(年轻代)
新生代进一步划分为3个区域:一个相对大点的区域,称为”伊甸园区(Eden)”;两个相对小点的区域称为”From 幸存区(survivor)”和”To 幸存区(survivor)”。按照规定,新对象会首先分配在 Eden 中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden 中的对象会被移动到survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代。
上图演示GC过程,***表示死对象,绿色表示剩余空间,红色表示幸存对象
如果还没有明白,则看这个图
分代垃圾回收过程演示
当一个URL被访问时,内存申请过程如下:
A. JVM会试图为相关Java对象在Eden中初始化一块内存区域
B. 当Eden空间足够时,内存申请结束。否则到下一步
C. JVM试图释放在Eden中所有不活跃的对象(这属于1或更高级的垃圾回收), 释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区
D. Survivor区被用来作为Eden及OLD的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区
E. 当OLD区空间不够时,JVM会在OLD区进行完全的垃圾收集(0级)
F. 完全垃圾收集后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"out of memory错误"
总结一下,对象一般出生在Eden区,年轻代GC过程中,对象在2个幸存区之间移动,如果对象存活到适当的年龄,会被移动到老年代。当对象在老年代死亡时,就需要更高级别的GC,更重量级的GC算法(复制算法不适用于老年代,因为没有多余的空间用于复制)
现在应该能理解为什么新生代大小非常重要了(译者,有另外一种说法:新生代大小并不重要,影响GC的因素主要是幸存对象的数量),如果新生代过小,会导致新生对象很快就晋升到老年代中,在老年代中对象很难被回收。如果新生代过大,会发生过多的复制过程。我们需要找到一个合适大小,不幸的是,要想获得一个合适的大小,只能通过不断的测试调优。这就需要JVM参数了
-XX:NewSize and -XX:MaxNewSize
就像可以通过参数(-Xmsand -Xmx) 指定堆大小一样,可以通过参数指定新生代大小。设置XX:MaxNewSize 参数时,应该考虑到新生代只是整个堆的一部分,新生代设置的越大,老年代区域就会减少。一般不允许新生代比老年代还大,因为要考虑GC时最坏情况,所有对象都晋升到老年代。(译者:会发生OOM错误)-XX:MaxNewSize 最大可以设置为-Xmx/2.
考虑性能,一般会通过参数-XX:NewSize 设置新生代初始大小。如果知道新生代初始分配的对象大小(经过监控) ,这样设置会有帮助,可以节省新生代自动扩展的消耗。
-XX:NewRatio
可以设置新生代和老年代的相对大小。这种方式的优点是新生代大小会随着整个堆大小动态扩展。参数-XX:NewRatio 设置老年代与新生代的比例。例如-XX:NewRatio=3 指定老年代/新生代为3/1. 老年代占堆大小的3/4 ,新生代占1/4 .
Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。
Perm(持久代)----同时也叫方法区
用 于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等, 在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。
持久代一般固定大小为64m
下图是JVM在内存空间(堆空间)中申请新对象过程的活动图: