概述
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,也就是Java虚拟机运行时的数据区域,包含程序计数器、虚拟机栈、本地方法栈、Java堆、方法区、直接内存等,不同的版本会有所差异。----写文不易,点个关注呗!
在JVM的管理下,Java程序员不需要管理内存的分配和释放,这和在C和C++中是完全不一样的。所以,在JVM的帮助下,Java程序员很少会关注内存泄露和内存溢出的问题。但是,一旦JVM发生这些情况的时候,如果你不清楚JVM内存的内存管理机制是很难定位与解决问题的。
线程私有区域-生命周期与线程相同
线程私有区域有程序计数器、虚拟机栈、本地方法栈。线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 HotspotVM 内, 每个线程都与操作系统的本地线程直接映射(上篇文章说过线程映射这块,大家可以去看看),因此这部分内存区域的存否跟随本地线程的生死对应)。
线程共享区域-生命周期与相对应的虚拟机实例相同
线程共享区域有Java堆,方法区。生命周期与相对应的虚拟机实例相同,随相应的虚拟机实例的启动/关闭而创建/销毁。
程序计数器( 线程私有)
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。Java代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。多线程是通过CPU时间片轮转来实现的。也就是说,某个线程在执行过程中可能会因为时间片耗尽而被挂起,而另一个线程获取到时间片开始执行。当被挂起的线程重新获取到时间片的时候,它要想从被挂起的地方继续执行,就必须知道它上次执行到哪个位置,在JVM中,通过程序计数器来记录某个线程的字节码执行位置。因此,程序计数器是具备线程隔离的特性。这个内存区域是唯一一个在虚拟机中没有规定任何 OutOfMemoryError 情况的区域。需要注意的是在执native本地方法时,程序计数器的值为空,因为native方法是java通过JNI直接调用本地C/C++库,无法产生相应的字节码。
虚拟机栈( 线程私有)
描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( DispatchException)栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正结束成还是异常结束(抛出了在方法内未被捕获的异常)都算作方法结束。栈里面存放着各种基本数据类型和对象的引用(-Xss),-Xss参数是用来调整JAVA虚拟机栈的。一个线程调用多个方法,只会有一个栈。栈的缺省大小为1M。
局部变量表:是一组变量值存储空间,存储方法定义的局部变量、方法的参数。实例方法中有个隐含参数“this”,所以实例方法可以访问该类的实例变量和其他实例方法。局部变量表中变量的存放顺序:this(如果是实例方法)=>参数(如果有)=> 定义的局部变量(如果有)。
操作数栈:用来存放操作数,局部变量表中的变量是不可直接使用的,如需使用必须通过相关指令将其加载至操作数栈中作为操作数使用。
动态链接:在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。
方法出口:一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
Java堆(heap线程共享)
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域。由于现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 年轻代和老年代。年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。堆内存存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
为什么要分代?主要为了解决碎片化。如果内存碎片化严重,也就是如果对象占用不连续的内存,已有的连续内存不够新对象存放,就会触发GC。
方法区(永久代,线程共享)
即我们常说的永久代(Permanent Generation),存储程序运行时长期存活的对象,比如类的元数据、方法、常量、静态变量、属性、即时编译器编译后的代码等数据. HotSpot VM把GC分代收集扩展至方法区, 即使用Java堆的永久代来实现方法区, 这样 HotSpot 的垃圾收集器就可以像管理 Java 堆一样管理这部分内存,而不必为方法区开发专门的内存管理器(永久带的内存回收的主要目标是针对常量池的回收和类型的卸载, 因此收益一般很小)。在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间与永久代上类似,都是方法区的实现,他们最大区别是:元空间并不在JVM中,而是使用本地内存。
运行时常量池(Runtime Constant Pool)是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量池。(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
为什么移除永久代?为融合HotSpot JVM与JRockit VM(新JVM技术)而做出的改变,因为JRockit没有永久代。有了元空间就不再会出现永久代OOM问题了
直接内存
直接内存并不是 JVM 运行时数据区的一部分, 但也会被频繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 与 Buffer 的 IO 方式, 它可以使用 Native 函数库直接分配堆外内存, 然后使DirectByteBuffer 对象作为这块内存的引用进行操作, 这样就避免了在 Java堆和 Native 堆中来回复制数据, 因此在一些场景中可以显著提高性能。
最后
都看到这里了,顺便点个关注吧!