Java虚拟机 之 内存区域

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/yichen97/article/details/90416492

引言

上一篇介绍了JVM的发展史,那只是简单的看了JVM,只是知道有这么一个东西,是干什么用的;从这一篇到未来,将一点点慢慢了解它,为的是为以后的学习打下基础和浇灭面试官的狂气 :)。

 

简介

下面是我画的一个草图,可以表示出大致的结构图,但不是详细的。

那我们平时所说的堆内存和栈内存到底是什么呢?

堆内存:就是指的Java堆;

栈内存:虚拟机栈中的局部变量表。

线程独占区与线程共享区

首先得知道,每一个线程都是一个顺序的执行单元。

线程独占区:就是对每一个线程都有这样一块区域(比如说有自己的程序计数器、有自己的本地方法栈和虚拟机栈)。

线程共享区:多个线程共享这同一块区域。

在一个程序中,会存在多个线程;

在每一个线程中,存在着自己的线程独占区,这里就有他们的程序计数器、本地方法栈……;

而方法区和堆处在线程共享区,被所有线程所共享。

各区域的介绍

知道有这些东西,那他们都是干什么的呢?先简单说一下,然后挨个区域再详细说。

共享:

方法区:存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

Java堆:存储实例对象。

独占:

虚拟机栈:存储方法运行时所需的数据,成为栈帧,虚拟机栈描述的是方法的内存模型。

本地方法栈:为JVM所调用的Native,即本地方法服务。

程序计数器:记录当前线程所执行到的字节码行号。

程序计数器

1. 程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

字节码是什么呢?字节码(Byte-code)是一种包含执行程序,由一序列 op 代码/数据对组成的二进制文件,是一种中间码。

2. 程序计数器处于线程独占区,每一个线程都会有它的程序计数器。

3. 如果线程执行的是Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Native方法,这个计数器的值为undefined。

4. 此区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域,我们不需要操作程序计数器,这个计数器是JVM内部维护的。

5. Java中保留字"goto",作用是跳到某一行。goto目前java中不使用,也不允许开发者使用,为的就是可能在以后的JDK版本中增加这样一个关键字。

Java虚拟机栈

虚拟机栈描述的是Java方法执行的动态内存模型。

栈帧:每个方法执行,都会创建一个栈帧,伴随着方法从创建到执行完成;栈帧是用来存储方法所执行的局部变量表、操作数栈、动态链接、以及方法出口等;遵守"先进后出,后进先出"的原则。

首先,每一个方法的执行都会创建一个栈帧。

当我门创建好了栈帧,就开始进栈了,进栈也就开始执行这个方法。

如果,在执行的过程中,它调用了另外的一个方法,而另一个方法也需要创建一个栈帧。

接着,它所调用的方法进栈,这个方法就开始执行了。

当那个方法执行到方法出口之后,这个方法就执行完毕了,相应的栈帧也就出栈了,出栈之后,栈帧随之销毁了。

然后接着执行第一个方法,等方法执行结束后,同样出栈。一个Java方法的执行过程就结束了。

局部变量表:

1. 存放编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double);对象引用(不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置);returnAddress类型(指向一条字节码指令的地址)。

2. 局部变量表所需的内存空间在编译期间分配完成,当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

3. 假如说有一个对象user,而在user里面有一个String类型的属性name。那么它在运行期间可能会给他指定值,它的长度一定是不固定的,为什么说他的内存区域是不会变化的呢?因为我们在局部变量表中所引用的只是对象的引用,对象的创建会创建到堆内存中,而局部变量表他所存储的就是这个对象的引用,这个对象的引用的大小他是不会改变的,所以,在方法运行期间局部变量表的大小是不会改变的。

Java虚拟机栈的大小与抛出的异常:

1. StackOverFlowError(栈内存溢出)。由于方法的不停进栈,从而导致错误抛出。

2. OutOfMemoryError(内存溢出)。而如果这个栈的内存大于虚拟机的内存,或者说已经超出了机器本身的内存,并且已经申请不下内存来的时候。

本地方法栈

本地方法栈与虚拟机栈相似,栈帧、栈大小异常抛出、局部变量表等都完全相同,以至于很多虚拟机把两者合二为一。

两者唯一的区别就是 虚拟机栈为虚拟机执行Java方法服务;本地方法栈为所使用的Native方法服务。

Java堆

1. 唯一目的:存放对象实例。

2. 一般来说,Java堆是JVM所管理的最大的一块内存区域。因为在应用运行过程中,应用会不停地创建对象,只要是对象的存储,都会放到堆中。

3. Java堆是垃圾收集器所管理的主要区域,也是回收效率最高的。

4. 从回收角度看,大致可分为新生代和老年代;从分配角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区。无论如何划分,存储的都是对象实例,目的是为了更好地回收内存或更快地分配内存。

5. Java 堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

6. 如果堆中没有内存完成实例的分配了,则会抛出OutOfMemoryError。

7. 修改堆大小的两个参数:-Xmx 和 -Xms。

方法区

1. 前面说的,方法区是存储运行时常量池、已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

那什么是类信息?

类信息就是指类的版本、字段、方法、接口等。

2. 在Java虚拟机规范中,方法区和永久代并不是等价的。只是在HotSpot虚拟机中,为了减少内存管理代码的工作,而省去了专门对方法区的编码。

3. 垃圾收集器主要在Java堆回收,而方法区是堆的一个逻辑部分,但垃圾收集器在方法区出现较少,因为它回收效率较低。这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。

4. 当申请内存区域失败,仍会抛出OutOfMemoryError。

运行时常量池

1. 运行常量池属于方法区的一部分。

2. 它用来存储编译期生成的各种字面量和符号引用。

3. 现有s1,s2:

String s1 = "abc";

String s2 = "abc";

问:(s1 == s2)结果如何?

答:“==”是表示地址是否相同。

随着方法的运行,我们会对每一个方法创建一个栈帧,栈帧里有一个局部变量表,所有的基本数据类型和抽象数据类型的引用都会放到局部变量表中。

由于HashSet是无序、不可重复的。“abc”存入到运行时常量池后,到第二个“abc”也只会存入一个,因为只创建了一个实例,那么,s1和s2的地址显然是相同的。

故,“s1 == s2”为true。

4. 现有s2,s3:

String s2 = "abc";

String s3 = new String("abc");

问:(s2 == s3)结果如何?

答:如果使用new去创建对象的话,它一定是在堆内存,它不再去考虑常量池。所以,直接在堆中创建了一个s3。

s3在堆中,s2在常量池中,显然不是一块内存区域,内存地址也肯定是不相同的。

故false。

5. 现有s1,s3:

String s1 = “abc”;

String s3 = new String("abc");

问:“s1 == s3.intern()”

答:intern()这个方法,它会把堆中的区域搬到运行时常量池中去,产生一个运行时常量。

故为true。

直接内存

它是一种基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java 堆中的DirectByBuffer对象作为这块内存的引用进行操作。

好处就是避免在Java堆和Native堆中来回复制数据,提高性能;

但在配置虚拟机参数时容易被忽略掉,让各内存区域总和大于实际内存,从而抛出OutOfMemoryError异常。

猜你喜欢

转载自blog.csdn.net/yichen97/article/details/90416492
今日推荐