JVM内存浅述

JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。Java虚拟机内存包括程序计数器、虚拟机栈、本地方法栈、堆、方法区等几个运行时数据区域。

PC Register(PC寄存器/程序计数器)

PC寄存器是一块很小的内存区域,主要作用是记录当前线程所执行的字节码的行号。任何分支,循环,方法调用,判断,异常处理,线程等待以及恢复线程,递归等等都是通过这个计数器来完成的。

由于Java多线程是通过交替线程轮流切换并分配处理器时间的方式来实现的,在任何一个确定的时间里,在处理器的一个内核只会执行一条线程中的指令。每条线程都会有一个独立的程序计数器来记录当前指令的行号。计数器之间相互独立互不影响,我们称这块内存为“线程私有”的内存。

如果正在执行的是Native方法,PC寄存器中不存储任何信息,计数器值则为空(Undefined),此内存是唯一一个没有任何OutOfMemoryError的情况。

JVM Stack(虚拟机栈)

线程私有,生命周期与线程相同。为执行Java方法(字节码)服务;每个方法执行的时候会创建一个栈帧,用于存储局部基本类型变量、操作数栈、动态链接、方法出口,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。因此Java中基本类型的变量是值传递,而非基本类型的变量是引用传递。每个方法被调用到执行完成的过程,就对应这一个栈帧在虚拟机栈中入栈到出栈的过程。

JVM栈的空间是在物理内存上分配的,而不是从堆上分配。如果线程请求的栈深度大于虚拟机所允许,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。当线程运行完毕后,这些内存也就被自动回收。可以通过-Xss来指定栈的大小。

Native Method Stacks(本地方法栈)

作用与虚拟机栈类似,但为Native方法服务。

Heap(堆)

所有线程共享,用于存放对象实例,几乎所有的对象都会在这里分配内存,是GC管理的主要区域。可以处于物理上不连续的内存空间中,但是逻辑上要求连续。当堆中没有内存完成实例分配,并且堆也无法扩展时,将抛出OutOfMemoryError异常。其大小通过-Xms(最小值)和-Xmx(最大值)参数设置。-Xms为JVM启动时申请的最小内存,默认为操作系统物理内存的1/64但小于1G,-Xmx为JVM可申请的最大内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap到-Xmx指定的大小,可通过-XX:MinHeapFreeRation=来指定这个比列;当空余堆内存大于70%时,JVM会减小heap的大小到-Xms指定的大小,可通过XX:MaxHeapFreeRation=来指定这个比列,为避免在运行时频繁调整Heap的大小,通常-Xms与-Xmx的值设成一样。

堆被划分为Old Generation(老年代)和Young Generation(新生代)。新生代主要存储新创建的对象和尚未进入老年代的对象。老年代存储经过多次新生代GC(Minor GC)任然存活的对象。



这里插播一下Java GC机制
一般采用引用计数法(简单高效,但无法处理循环引用)和可达性分析法(通过一系列的称为“GC Roots”的对象作为起始点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。不可达的对象要成为可回收对象至少经历两次标记过程)来判断一个对象是否可被回收。Python采用前者,Java、C#是采用后者。

GC算法常见为:
1.Mark-Sweep,标记-清除法
标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。算法很简单,但容易产生内存碎片。
2.Copying,复制法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。该算法对内存空间的使用做出了高昂的代价,而且如果存活对象很多,那么Copying算法的效率将会大大降低。
3.Mark-Compact,标记-整理法
该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动聚拢,然后清理掉端边界以外的内存。
4.Generational Collection,分代收集法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。
新生代都采取Copying算法,每次使用Eden空间和其中的一块Survivor(From Space)空间,当进行回收时,将Eden和Survivor中还存活的对象复制到另一块Survivor空间中,然后清理掉Eden和刚才使用过的Survivor空间。
由于老年代每次都只回收少量对象,一般使用的是Mark-Compact算法。

新生代

程序新创建的对象都是从新生代分配内存,新生代由Eden Space和两块相同大小的Survivor Space(通常又称S0和S1或From和To)构成,可通过-Xmn参数来指定新生代的大小,Eden Space的大小和两块Survivor Space的大小比例默认为8,这个比例可通过-XX:SurvivorRatio来指定。 针对新生代的垃圾回收即Young GC。

老年代

用于存放经过多次新生代GC任然存活的对象,例如缓存对象,新建的对象也有可能直接进入老年代,主要有两种情况:1.大对象,可通过启动参数设置-XX:PretenureSizeThreshold=1024(单位为字节,默认为0)来代表超过多大时就不在新生代分配,而是直接在老年代分配。2.大的数组对象,且数组中无引用外部对象。 老年代所占的内存大小为-Xmx对应的值减去-Xmn对应的值。 针对年老代的垃圾回收即Full GC。

Method Area(方法区)

在JDK中这块区域对应的为Permanent Generation,又称为持久代、永久代。所有线程共享,存储被VM加载的类信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域。当方法区无法满足内存分配需求的时候,将抛出OutOfMemoryError异常。默认大小为64M。可以通过-XX:PermSize和-XX:MaxPermSize来指定最小值和最大值。

Runtime Constant Pool(运行时常量池)

是方法区的一部分,其空间从方法区域中分配,存放编译器生成的各种符号引用。

直接内存

直接内存并不是虚拟机内存的一部分,也不是Java虚拟机规范中定义的内存区域。JDK1.4中新加入的NIO,引入了通道与缓冲区的IO方式,它可以调用Native方法直接分配堆外内存,这个堆外内存就是本机内存,不会影响到堆内存的大小。

参数调优说明