JVM内存基础知识总结
参考文章:
1.JVM的初布认识
java内存是一个比较重要的概念,大多数JVM将内存区域划分成Method
Area(Non-Heap)(方法区)、Heap(堆)、Program Counter Register(程序计数器),VM
Stack(虚拟机栈orjava方法栈),Native Method Stack(本地方法栈)。如下图所示:
GC作用区:方法区、堆
2、各区域详解
(1)java栈(java virtual Machine Stacks ):线程私有,生命周期同线程;
存放数据分为:基本类型的变量( java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、 部分的返回结果 以及 Stack Frame , 非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。
在Java虚拟机规范中,对这个区域规定了两种异常情况:
1、如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常。
2、如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
每个方法被执行的时候都会同时创建一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的 数据结构 。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。 栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的Code属性之中。 因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
补充说明: 局部变量表所需的内存空间在编译期间完成分配,即在Java程序被编译成Class文件时,就确定了所需分配的最大局部变量表的容量。当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
操作数栈又常被称为操作栈,操作数栈的最大深度也是在编译的时候就确定了
(2) 本地方法栈(Native Method Stacks)
类似于虚拟机栈,虚拟机栈为虚拟机执行Java方法服务;本地方法栈为使用到的本地操作系(Native)方法服务。此区域用于存储每个native方法调用的状态。
(3) Java堆(Heap) :线程共享,虚拟机管理的内存中最大一块,存储对象及数组值。
Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。由于 Java Heap是垃圾收集器管理的主要区域,也被称为“GC堆”。根据虚拟机规范规定,逻辑上连续,物理上可处于不连续的内存空间,无内存分配且堆无法扩展时,抛出OutOfMemoryError异常。
可参照下图 理解Heap结构:
对于堆,有几点需要注意:
😊线程共享,对象内存分配加锁,new对象开销大
😊Sun Hotspot JVM内存管理为提高分配效率,为创建的线程分配独立的空间TLAB(Threa Local
Allocation
Buffer),在此空间不需加锁。因此,JVM为线程对象分配内存时优先考虑此空间,效率与C一样高效,对象过大还是采用堆空间。不过,TLAB仅作用于新生代的Eden
Space,多个小的对象内存分配会比大对象更加高效。
(4) 方法区(Method Area):线程共享,
不需连续内存,对应永久(持久)代。
存储已经被虚拟机加载的类信息(名称、修饰符)、final类型常量、类中的静态变量、类中的方法Field信息、即时编译器编译后的代码等数据。 开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域。另外,运行时常量池空间从该区域分配,存放类中的固定的常量信息、方法、Field的引用信息。
该区域可以选择不实现垃圾回收。该区域的内存回收目标主要针是对废弃常量的和无用类的回收
(5) 程序计数器(Program Counter Register):
线程私有,无任何OOM(OutOfMemoryError)情况的区域
当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响
另外补充说明:
直接内存
不是虚拟机运行时数据区的一部分及Java虚拟机规范中定义的内存区域,直接从操作系统中分配,不受Java堆大小的限制,会受到本机总内存的大小及处理器寻址空间的限制,它也可能导致OutOfMemoryError异常出现。在JDK1.4中新引入了NIO机制,它是一种基于通道与缓冲区的新I/O方式, 它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。 即在堆外分配内存,这样能在一些场景中提高性能,因为避免了在Java堆和Native堆中来回复制数据
内存泄漏:
这里有一点要重点说明,在多线程情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。 操作系统为每个进程分配的内存是有限制的,虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值,如果是建立过多的线程导致的内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和每个线程的栈容量来换取更多的线程。
Java堆内也可能发生内存泄露(Memory Leak),简要说明一下内存泄露和内存溢出的区别:
内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露,内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
对象实例化分析
对内存分配情况分析最常见的示例便是对象实例化:
Object obj = new Object();
这段代码的执行会涉及java栈、Java堆、方法区三个最重要的内存区域。 假设该语句出现在方法体中,及时对JVM虚拟机不了解的Java使用这,应该也知道obj会作为引用类型(reference)的数据保存在Java栈的本地变量表中,而会在Java堆中保存该引用的实例化对象,但可能并不知道, Java堆中还必须包含能查找到此对象类型数据的地址信息(如对象类型、父类、实现的接口、方法等),这些类型数据则保存在方法区中。
另外,由于reference类型在Java虚拟机规范里面只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具***置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有两种:使用句柄池和直接使用指针。
通过句柄池访问的方式如下:
通过直接指针访问的方式如下:
这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存放的是稳定的句柄地址,在对象移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用直接指针访问方式的最大好处是速度快,它节省了一次指针定位的时间开销。目前Java默认使用的HotSpot虚拟机采用的便是是第二种方式进行对象访问的。