在 Java 虚拟机规范中,定义了五种运行时数据区,分别是 Java 堆、方法区、虚拟机栈、本地方法区、程序计数器
[hide reply_to_this="true"]
[/hide]
[collapse title="堆内存"]
堆是OOM故障最主要的发生区域,Java 堆是所有线程共享的,它在虚拟机启动时就会被创建
Java 堆是内存空间占据的最大一块区域了,Java 堆是用来存放对象实例及数组,也就是说我们代码中通过 new 关键字 new 出来的对象都存放在这里,存储着几乎所有的实例对象、数组。。所以这里也就成为了垃圾回收器的主要活动营地了,于是它就有了一个别名叫做 GC 堆,并且单个 JVM 进程有且仅有一个 Java 堆。根据垃圾回收器的规则,我们可以对 Java 堆进行进一步的划分,堆内存是JVM中最大的一块,实际上java堆是根据对象存活时间的不同,堆内存可以划分为新生代和老年代,而新生代又被分成三部分:Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配
Java堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
默认Eden:from :to = 8:1:1
- -Xms: 堆容量初始大小(堆包括新生代和老年代)。 例如:-Xms 20M
- -Xmx: 堆总共(最大)大小。 例如:-Xmx 30M 注意:建议将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后JVM重新分配内存!
- -Xmn: 新生代容量大小。例如:-Xmn 10M
- -XX: SurvivorRatio 设置参数Eden、form和to的比例 【比例参数Eden、form和to默认是8:1:1】例如:-XX: SurvivorRatio=8 代表比例8:1:1
注意:建议将 -Xms 和 -Xmx 设为相同值,避免每次垃圾回收完成后JVM重新分配内存!
虽然没有直接设置老年代的参数,但是可以设置堆空间大小和新生代空间大小两个参数来间接控制:
老年代空间大小 = 堆空间大小 - 年轻代大空间大小
当我们的 Java 堆内有足够的空间去完成实例分配时,并且堆也无法扩展,将会抛出我们常见的OutOfMemoryError 异常,也就是我们常说的OOM 异常
JVM 创建一个新对象的内存分配流程:
[/collapse]
[collapse title="方法区"]
方法区又被成为永久代(HotSpot虚拟机的设计团队选择把GC分代收集扩展至方法区),同样也是被所有的线程共享的。用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。
JDK1.7中,已经把放在永久代的字符串常量池移到堆中。JDK1.8撤销永久代,引入元空间Metaspace。
它存储了每个类的结构信息,例如运行时常量池、字段、方法数据、构造函数和普通方法的字节码内容,还包括一些在类、实例、接口初始化时用到的特殊方法。
方法区
的一部分,用于存放编译期生成的各种字面量
和符号引用
。这部分内容在类加载后进入方法区的运行时常量池
存放。运行时常量池
另一个重要特征就是具有动态性
。java语言并不要求常量
一定只有编译期
才能产生,运行期间也可以将新的常量放入池中,这种特性被开发人员利用的比较多的就是String类的intern()方法。
就以HotSpot 虚拟机来说,在 JDK1.8 之前,方法区也被称作为永久代,这个方法区会发生我们常见的 java.lang.OutOfMemoryError: PermGen space 异常,注意是永久代异常信息,我们也可以通过启动参数来控制方法区的大小:
-XX:PermSize 设置方法区最小空间
-XX:MaxPermSize 设置方法区最大空间
在JDK7之前的HotSpot虚拟机中,纳入字符串常量池的字符串被存储在永久代中,因此导致了一系列的性能问题和内存溢出错误。特别突出的例子就是String
的intern()方法
JDK1.8 之后的方法区
JDK8之后就没有永久代这一说法变成叫做元空间(meta space),而且将老年代与元空间剥离。元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了,也不会出现泄漏的数据移到交换区这样的事情。用户可以为元空间设置一个可用空间最大值,不设置默认根据类的元数据大小动态增加元空间的容量。对于一个 64 位的服务器端 JVM 来说,其默认的–XX:MetaspaceSize
值为 21MB。也就是说默认的元空间大小是21MB。
只要类加载器还存活,其加载的类的元数据也是存活的,不会被回收掉!也就是同生共死
[/collapse]
[collapse title="Java 虚拟机栈"]
1、 Java 虚拟机的每一条线程都有自己私有的 Java 虚拟机栈,这个 Java 虚拟机栈跟线程同时创建,所以它跟线程有相同的生命周期。
2、Java 虚拟机栈描述的是 Java 方法执行的内存模型:每一个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中的入栈到出栈的过程。
3、局部变量表存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型。
1、基本类型:八种基本类型
2、对象引用:reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置。
3、 returnAddress 类型:指向了一条字节码指令的地址。
其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间(Slot),其余的数据类型只占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
4、Java 虚拟机栈既允许被实现成固定的大小,也允许根据计算动态来扩展和收缩,如果采用固定大小的话,每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。在 Java 虚拟机栈中会发生两种异常,这个在虚拟机规范中有指出:
- 如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出
StackOverflowError
异常;也就是栈溢出错误!方法递归调用产生StackOverflowError
异常这种结果。 - 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存或者在创建新的线程时没有足够的内存去创建对应的 Java 虚拟机栈,那么虚拟机将会抛出
OutOfMemoryError
异常。也就是OOM内存溢出错误!(线程启动过多)
当然,可以通过参数 -Xss
去调整JVM栈的大小!
[/collapse]
[collapse title="本地方法栈(Native Method Stacks)"]
和虚拟栈相似,只不过它服务于Native方法,线程私有。当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也会使用到本地方法栈。如果 Java 虚拟机不支持 natvie 方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈。
与 Java 虚拟机栈一样,本地方法栈区域也会抛出 StackOverflowError
和 OutOfMemoryError
异常。
HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。
[/collapse]
[collapse title="程序计数器"]
当前线程所执行的字节码的行号指示器,用于记录正在执行的虚拟机字节指令地址,线程私有。
需要特别注意的是,程序计数器是唯一一个在Java虚拟机规范中没有规定任何
OutOfMemoryError
情况的区域。
程序计数器是用于标识当前线程执行的字节码文件的行号指示器。多线程情况下,每个线程都具有各自独立的程序计数器,所以该区域是非线程共享的内存区域。
当执行java方法时候,计数器中保存的是字节码文件的行号;当执行Native方法时,计数器的值为空。
[/collapse]
[collapse title="直接内存"]
OutOfMemoryError
异常。在JDK1.4 中新加入的NIO类,引入了一种基于通道(
Channel
)与缓存区(Buffer
)的I/O方式。它可以使用Native
函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中提高性能,因为避免了Java堆
和Native堆
中来回复制数据。小坑:本机的
直接内存
的分配不会受到Java堆大小的限制,但是会受到本机总内存的限制。可能导致各个内存区域总和大于物理内存的限制,从而导致动态扩展时出现OutOfMemoryError
。[/collapse]
文章评论