深入理解java虚拟机之内存模型

java虚拟机管理的内存包括几个运行时数据内存:方法区、虚拟机栈、堆、本地方法栈、程序计数器(PCR Program Counter Register)。其中方法区和堆是线程共享的数据区,其他几个是线程隔离的数据区。
在这里插入图片描述
程序计数器
如果是线程正在执行java方法,PCR记录正在执行的虚拟机字节码指令的地址,可以看成是当前线程所执行的行号指示器,其实就是通过改变该计数器的值来选取下一条要执行的字节码的指令,以保证其下次可以正确执行。

一般依赖PCR的基础功能有:分支、循环、跳转、异常处理、线程恢复等。
每一个线程都有为自己提供服务的程序计数器,独立存储,互不影响。
如果正在执行的是native 方法PCR值为空。native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。JNI是Java本机接口(Java Native Interface),JNI允许Java代码使用以其他语言编写的代码和代码库。
PCR耗费的内存非常小,所以是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。
虚拟机栈 VM Stack

给每一个线程提供运行空间,保存方法的执行顺序、方法的内部局部变量,提供方法在运算时的内存。
栈中的数据都是以栈帧(Stack frame)的格式存在,栈帧是一哥内存区块,是一个有关方法和运行期数据的数据集,
每一个方法从调用直至执行完成的过程,就对应着一个栈帧在java虚拟机中入栈和出栈的过程。
在这里插入图片描述
VM stack可以抛出的异常:
Stack OverflowError:请求的栈深度超过最大值;OutOfMemoryError 栈进行动态扩展的时候无法申请足够内存。
虚拟机栈的内部结构:
VM stack中包括:局部变量表、操作数栈、动态连接、返回地址和附加信息等。
1.局部变量表:
保存方法内部定义的局部变量的内存区域。其实我们平时所说的栈,就是VMstack中的栈帧中的局部变量表。

2.动态连接Dynamic Linking:
虚拟机在执行方法时有两种形式被用来确定执行指令对应的方法。
(1.)静态解析:类加载的时候,直接确定要执行的方法(静态方法、私有方法和final方法等)
(2.)动态连接:在真正运行的时候,根据对象的真实引用判断当前要执行的方法。
字节码文件中都有一个常量池,在常量池中保存有大量的符号引用(是每一个方法的间接引用),运行阶段需要调用真实的执行方法的地址(直接引用)。
动态连接就是为了保证在运行阶段可以调用到正确的方法,每个栈帧根据自己在运行时常量池中所对应的真实地址记录的位置。
在栈帧中的动态连接和查找符号引用为真实引用中的动态连接,是两个概念。前者表示的是一个区域,后者表示的是一种查找方式。

查找方式类型的动态链接: 在动态链接程序运行时,除了可执行文件本身,将还有动态链接文件与动态链接器将被映射到进程的地址空间中,动态链接器被当做普通的共享对象来进行映射,在系统运行可执行文件之前,会将控制权交给动态链接器,由它完成所有的动态链接工作以后再把控制权交给可执行文件。

3.返回地址:
退出当前的方法的方式:正确的遇到返回指令;遇到没有捕获的异常。
不管怎样,方法退出之后栈帧的顶端都应该是当前退出方法的上层方法,上层方法的状态会随着本次方法的结果而改变,而“返回地址”这块区域就是为了用来帮助栈帧去恢复上层方法的状态。
4.附加信息:
对于虚拟机规范中没有申明的,拥有指定存放位置的信息可以由各个虚拟机自己决定,放置到这个区域中。
5.操作数栈
为虚拟机用来数值计算的内存区域。一次完整的计算之后,栈中的数据会已经出栈,所以操作数栈的空间在一个方法内部可以反复使用。所以为了减少内存的消耗,VM一般都是只分配当前方法中单次计算所需要的最大内存空间给当前的栈帧。
同时为了增加运行效率,减少数据的不断复制,在大部分虚拟机的实现中,将当前方法的局部变量表和上层方法的操作数栈的内存形成部分重叠,从而减少参数的不断复制而引起的性能消费。
机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如,压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是,有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。
操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。

Native Stack本地方法栈 Native Stack
Native方法就是用关键字native修饰的方法。在VMstack中,会为每一个线程独立的开辟一个专门运行字节码(java语言)的方法,我们还需要一个区域保存线程的调用本地方法的状态,该区域就是Native Stack。
在虚拟机规范中,对于本地方法栈中的结构、方法的语言、方式,都没有强制规定,各个虚拟机可以自由的实现它。

java Heap
java堆:被所有线程共享。在虚拟机启动时创建. 此内存区域的唯一目的就是存放对象实例以及数组分配空间, 几乎所有的对象实例都在这里分配内存.。
又被称为GC堆:(Garbage Collection heap)。现在的收集器一般都是分代收集算法(新生代和老年代)
从内存分配的角度看,线程共享的Java对中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB). 不过, 无论如何划分, 都与存放内容无关, 无论哪个区域, 存储的都仍然是对象实例, 进一步划分的目的是为了更好的回收内存, 或者更快的分配内存. 在这里插入图片描述
java堆 可以处于物理上不连续的内存空间中, 只要逻辑上是连续的即可。当前主流的虚拟机都是按照可拓展来实现的( 通过-Xms 初始化堆, -Xmx 最大堆空间), 可以通过 -Xmx 和 -Xms 来控制动态扩展内存大小。如果在堆中没有内存完成实例分配, 并且堆也无法在拓展时, 将会抛出OutOfMemoryError异常.

类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分:
Permanent Space 永久存储区
永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class,Interface的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。
Young Generation Space 新生区
新生区是类的诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace),所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。
Tenure generation space养老区
养老区用于保存从新生区筛选出来的JAVA对象,一般池对象都在这个区域活跃。 三个区的示意图如下:
在这里插入图片描述
方法区Method Area
保存类信息、静态变量、常量以及即时编译后的代码等数据,也是被各个线程共享的内存区域。又称 永久代。但是该区域中的数据仍可能被回收(主要是回收常量池或者类型的卸载)
虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分, 但是它却有一个别名叫做Non-Heap非堆, 目的应该是与Java Heap 区分开来.

运行时常量池 Runtime Constant Pool
属于方法区的一块子区域。Class文件中除了有类的版本, 字段,方法, 接口等描述信息外, 还有一项信息是常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用(字面量可以理解为java语言中的常量//符号引用:类和接口的全限定名称、字段和方法的名称和描述符), 这部分内容将在类加载后存放到方法区的运行时常量池中。
(java语言在编译成class文件之后,并没有关于方法和字段在内存中的地址信息,所以VM使用这些变量和方法时,需要先从变量池中找到这些数据对应的符号引用,然后去方法的栈帧中的动态连接区域中找到符号引用对应的内存真实地址)
Runtime Constant Pool相对于Class文件常量池的另一个重要特征就是有动态性,java语言并不要求常量一定只能在编译期产生,也就是说并非只有预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中。利用最多的便是:String类的intern()方法。

直接内存 Direct Memory
是本机直接分配的内存(受到本机总内存的大小及处理器寻址空间的限制)。这部分区域不属于java虚拟机规范中定义的内存区域,也不是虚拟机运行时数据区的一部分,但是该区域被频繁使用,也可能导致OutOfMemoryError的异常。
在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
服务器管理员配置虚拟机参数时, 一般会根据实际内存-Xmx等参数信息, 但经常会忽略到直接内存, 使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制), 从而导致动态扩展时出现OutOfMemoryError异常.

两种常见的内存溢出的错误:
OutOfMemoryError:区域申请的空间大于VM设定的给该区域的最大值。
Stack OverflowError:内存中的栈结构不断地入栈导致栈的深度超过了虚拟机所允许的栈深度。

其他几个必须知道的专有名词:
常量池(constant pool):按照顺序存放程序中的常量,并且进行索引编号的区域。比如int i =100,这个100就放在常量池中。
安全管理器(Security Manager):提供Java运行期的安全控制,防止恶意攻击,比如指定读取文件,写入文件权限,网络访问,创建进程等等,Class Loader在Security Manager认证通过后才能加载class文件的。
方法索引表(Methods table),记录的是每个method的地址信息,Stack和Heap中的地址指针其实是指向Methods table地址。

注意:不建议在程序中显式的生命System.gc(),该方法是停止JVM中所有的活动。

Java的方法(函数)到底是传值还是传址?
答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地址。对于原始数据类型,JVM的处理方法是从Method Area或Heap中拷贝到Stack,然后运行frame中的方法,运行完毕后再把变量指拷贝回去。

OutOfMemory错误分几种?
答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen space”,两种都是内存溢出,heap size是说申请不到新的内存了,这个很常见,检查应用或调整堆内存大小。
“PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是因为每次发布应用系统都不重启,久而久之永久存储区中的死对象太多导致新对象无法申请内存,一般重新启动一下即可。

部分转载自:https://blog.csdn.net/xiaoye142034/article/details/78120225

猜你喜欢

转载自blog.csdn.net/mulinsen77/article/details/84337120