欲穷千里目之JVM(一)

352 阅读7分钟

  本文将介绍Java虚拟机内存的各个区域,很多知识来源于《深入理解Java虚拟机》这一本书。有兴趣的同学可以在网上或者是书店获得相应的资源,我看的这本书还是第二版,因此有些地方还没有更新,比如说元空间的出现,G1的出现等等,在后续文章将会提及(如果还有后续文章的话,哈哈哈)。

  Java虚拟机在执行Java程序的过程中会将所管理的内存划分为若干个区域,这些区域有各自的生命周期。Java虚拟机所管理的内存包含多个运行时数据区域。



程序计数器

  程序计数器是内存中较小的一个内存空间,可以被看作当前线程所执行的字节码行号指示器。由于Java虚拟机的多线程是通过线程轮流切换并分配cpu处理时间的方式来实现的,为了记录每一条线程的执行,需要给每条线程都设置一个独立的程序计数器。各线程之间的计数器互不影响,独立存储,是属于线程私有的。

  程序计数器记录的数据是不同的,当线程执行的是一个Java方法,记录的是正在执行的虚拟机字节码指令地址;当线程执行的是一个本地方法,记录的是Undefined。

  值得注意的是:程序计数器是内存区域唯一一个不会发生OutOfMemoryError的区域。

Java虚拟机栈

  虚拟机栈描述的是Java方法执行的内存模型:每个方法都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。Java虚拟机栈是虚拟机栈中的局部变量表部分,存储各种基本数据类型,对象引用类型和returnAddress类型(字节码指令地址)。 与程序计数器一样,Java虚拟机栈属于线程私有。

  注意:如果线程请求的栈深度大于虚拟机所运行的深度(换句话说,不断调用方法、方法庞大,栈空间不够用)将抛出StackOverflowEorror异常;如果虚拟机栈动态扩展空间时发现无法申请到足够的内存(栈默认大小为512k-1024k)将抛出OutOfMemoryError异常。

本地方法栈

   本地方法栈作用与虚拟机栈的作用是十分相似,区别在于虚拟机栈是为Java方法服务,本地方法栈是为native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowEorror异常和OutOfMemoryError异常。

方法区  方法区是用于存储已经被虚拟机加载的类信息、常量、静态变量等数据,是所有线程共享区域,不需要连续内存还可以选择不实现垃圾收集(并不意味不实现垃圾回收)。当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。运行时常量池运行时常量是方法区的一部分,存放编译期生成的各种字面量和符号引用。当常量池无法申请到内存分配需求时,将抛出OutOfMemoryError异常。

Java8 已经使用元空间代替方法区


元空间

  Java8 使用元空间代替永久代,绝大多数的类元数据的空间都从本地内存中分配。分配了多个虚拟内存空间给每个类加载器分配一个内存块的列表,块的大小取决于类加载器的类型; sun/反射/代理对应的类加载器的块会小一些 。当运行期间生成了大量的类并且这些类全部存活,没有被回收机制回收,导致方法区内存空间不足将抛出OutOfMemoryError异常。

永久代为什么被替换了 ,为什么使用元空间替换永久代?  

表面上看是为了避免OOM异常。因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。 当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制。 更深层的原因还是要合并HotSpot和JRockit的代码,JRockit从来没有所谓的永久代,也不需要开发运维人员设置永久代的大小,但是运行良好。同时也不用担心运行性能问题了,在覆盖到的测试中, 程序启动和运行速度降低不超过1%,但是这点性能损失换来了更大的安全保障。

  堆是内存管理中最大的一块,被所有的线程共享,在虚拟机启动时就创建,存放所有的对象实例以及数组。Java堆可以处于物理上不连续的内存空间中,只要逻辑上时连续即可。如果堆没有内存完成实例分配,并且堆无法在扩展时(换句话说就是堆内存不够用,不断new实例对象,或者分配一个超大空间给对象)抛出OutOfMemoryError异常。(Java堆、内存回收,会在后文给出)

直接内存

  直接内存并不是虚拟机运行时数据区的 一部分,也不是虚拟定义的内存区域,但是这部分频繁使用,当各内存区域总和大于物理内存限制,动态扩展将会抛出OutOfMemoryError异常。直接内村分配不会受到Java堆大小的限制,但是受本机总内存大小以及处理器寻址空间的限制。

OutOfMemoryError异常

Java堆内存溢出 OutOfMemoryError:Java heap space

Java堆是用于存放对象实例,只要不断New对象或者New一个超大的对象,同时保证这些对象都是活的(不会被垃圾回收机制清理)那么对象所占空间超过堆最大容量限制就会产生内存溢出异常。

当出现 OutOfMemoryError:Java heap space时,首先要分析出到底是内存泄漏还是内存溢出:如果是内存泄漏,查看泄漏对象到GC Roots的引用链,可以定位到泄漏代码的位置;如果是内存溢出,确定虚拟机堆是否可以扩展,机器物理内存是否可以扩展,对象生命周期是否过长,持有状态时间是否过长。

VM Args: -Xms 堆最小值 -Xmx 堆最大值

虚拟机栈和本地方法栈溢出 StackOverflowEorror

虚拟机栈和本地方法栈是用于存放方法,如果线程请求的栈深度大于虚拟机所允许的最大深度,抛出StackOverflowEorror异常;如果虚拟机在扩展栈无法申请到足够大小的内存空间,抛出OutOfMemoryError异常。

当出现StackOverflowEorror时,先要考虑过多线程导致导致栈溢出,还是栈深度太深;要么减少线程,要么减少最大堆和栈容量。

VM Args: -Xss 栈容量

方法区和运行时常量池溢出 OutOfMemoryError:PermGen space(持久代已经被元空间取代,不会出现这个异常) 

元空间溢出  OutOfMemoryError:MetaSpace

元空间是用于存放虚拟机加载的类信息、字符串常量代替了方法区中的持久代,如果大量的类生成,且不被回收,造成空间不足,抛出OutOfMemoryError异常。

当出现OutOfMemoryError:MetaSpace,考虑是否设置的元空间初始大小,确定元空间是否可以扩展,类生命周期是否过长,持有状态时间是否过长。

VM Args: -XX:MaxMetaspaceSize 元空间的最大值 -XX:MetaspaceSize 元空间的初始大小

本地直接内存溢出 OutOfMemoryError:Direct buffer memory

当出现OutOfMemoryError:Direct buffer memory ,是否直接或间接使用了 nio,MaxDirectMemorySize初始值大小, JVM 参数里面有 -XX:+DisableExplicitGC 。

VM Args -XX:MaxDirectMemorySize最大本地直接内存

关于OutOfMemoryError异常的问题,在日后开发中遇到新的问题,将会再次把相关知识,产生原因,解决方法详细写出。有关于Java虚拟机内存管理机制已经初步阐述完毕,如果有错误,不全或者观点不正确,请及时告知,一起探讨。

特别注意:本文部分摘抄文字版权属于原作者!!!