温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

java中JVM内存划分、栈区、堆区、方法区的区别

发布时间:2021-06-26 14:07:51 来源:亿速云 阅读:180 作者:chen 栏目:大数据

本篇内容介绍了“java中JVM内存划分、栈区、堆区、方法区的区别”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

现在用一张图来介绍每个区域存储的内容。

java中JVM内存划分、栈区、堆区、方法区的区别

运行时数据区怎么理解?
JVM运行时首先需要类加载器(classLoader)加载所需类的字节码文件。加载完毕交由执行引擎执行,在执行过程中需要一段空间来存储数据(类比CPU与主存)。这段内存空间的分配和释放过程正是我们需要关心的运行时数据区。

运行时数据区
运行时数据区都包括,程序计数器,方法区(包含常量池),虚拟机栈,本地方法栈,堆 。 JVM本身就是一台虚拟的计算机,目的是为了实现一次编译处处执行。

程序计数器
程序计数器是一块较小的内存空间。他可以看做是当前线程所执行的字节码行号指示器。字节码解释器工作就是通过改变这个计数器的值来选择下一条需要执行的字节码指令。分支,循环,跳转,异常,线程恢复等基础功能都需要依赖这个计数器来完成。
由于JVM虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。当切换到另外一条线程时,若不保存当前未执行完线程的执行位置,下次处理机再执行这条线程时,又要重新开始执行。这种情况显然是不能容忍的。因此,为了线程切换后能正确的恢复到执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为‘线程私有’内存,
如果线程正在执行的是一个java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址,如果正在执行的是native方法,这个计数器则为(undefined),此内存区域是为一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域

java虚拟机栈
与程序计数器一样java虚拟机也是线程私有的,他的生命周期与线程相同,虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame,可以这么理解栈帧,虚拟机栈包含N个栈帧每个栈帧包含局部变量表,操作数栈,动态链接,方法出口等信息)。每个方法从调用到执行完成这个过程,就对应这一个栈帧在虚拟机栈中的入栈到出栈的过程。
经常有人将java内存分为堆,栈内存,这其实是很粗糙的,java内存的划分远比这复杂。只能说明传统的程序员最关注的,与对象内存分配关系最密切的就是这两块内存堆,栈。栈就是现在说的虚拟机栈,或者说是虚拟机栈中栈帧的局部变量表部分。
局部变量表存放了编译期可知的各种基本数据类型(boolean,byte,char,short,int,float,long,double),对象引用(reference类型,他不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此相关的位置)和returnAddress类型(指向了一条字节码指令的地址)
其中64位长度的long和double类型会占用2个局部变量空间,其余的数据类型只会占用1个局部变量空间。局部变量表所需的内存空间在编译期间完成内存分配,当进入一个方法时,这个方法需要在帧中分配多大的内存空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
在java虚拟机规范中,对这个区域规定了两种异常状态:如果线程请求的栈的深度大于虚拟机允许的深度,将抛出StackOverFlowError异常(栈溢出),如果虚拟机栈可以动态扩展(现在大部分java虚拟机都可以动态扩展,只不过java虚拟机规范中也允许固定长度的java虚拟机栈),如果扩展时无法申请到足够的内存空间,就会抛出OutOfmMemoryError异常(没有足够的内存)

本地方法栈
本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地Native方法服务,在虚拟机规范中对本地方法栈中的使用方法,语言,与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(例如Sun HotSpot虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样本地方法栈也会抛出StackOverFlowError和OutOfmMemoryError异常

java堆
对于大多数应用来说,java堆(java Heap)是java虚拟机管理内存中的最大一块。java堆是所有线程共享的一块内存管理区域,在虚拟机启动时创建。此内存区域唯一目的就是存放对象的实例。几乎所有对象实例都在堆中分配内存。这一点在java虚拟机规范中的描述是:所有对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展与逃逸技术逐渐成熟,栈上分配,标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也不是变的那么“绝对”了。
java堆是垃圾回收器管理的主要区域,因此很多时候也被称为GC堆(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以java堆中还可以细分为:新生代和年老代:在细致一点的划分可以分为:Eden空间,From Survivor空间,To Survivor空间等。从内存分配的角度来看,线程共享的java堆中可能划分出多个线程私有的分配缓冲区 ,不过无论如何划分,都与存放内容无关,无论哪个区域存放的都是对象实例。进一步划分的目的是为了更好的回收内存,或者更快的分配内存。
根据java虚拟机规范的规定,java堆可以处在物理上不连续的内存空间,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现上既可以实现成固定大小,也可以是可扩展的大小,不过当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)。如果在堆中没有内存实例完成分配,并且堆也无法在扩展时将会抛出OutOfMemoryError异常。

方法区
方法区和java堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。虽然java虚拟机规范把方法区描述为堆的一部分,但是他还有个别名叫做Non-heap(非堆),目的应该是与java堆区分开来。
java虚拟机规范对方法区的限制非常宽松,除了和java堆一样不需要连续的内存和可以选择固定大小或者可扩展外,还可以选择不实现垃圾收集。相对而言,垃圾收集在这个区域是比较少出现的,但并非数据进入了方法区就如永久代的名字一样永久存在了。这区域的内存回收目标重要是针对常量池的回收和类型的卸载,一般来说这个内存区域的回收‘成绩’比较难以令人满意。尤其是类型的卸载条件非常苛刻,但是这部分的回收确实是必要的。在sun公司的bug列表中,曾出现过的若干个严重的bug就是由于低版本的HotSpot虚拟机对此区域未完成回收导致的内存溢出。

这里有三个概念需要清楚
1 常量池(Constant Pool): 常量池数据编译器被确定,是class文件中的一部分,存储了类,方法,接口等中的常量,当然也包括字符串常量。
常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目资源关联最多的数据类型
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。
字面量:文本字符串、声明为final的常量值等;
符号引用:类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
2 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存储了编译器产生的字符串类型数据

3 运行时常量池(Runtime Constant Pool): 方法区的一部分,所有线程共享。虚拟机加载class文件后把常量池中的数据存放到运行时常量池中
这里需要注重提一下在JDK1.6之前字符串常量池是存在于方法区之中,在JDK1.7和以上字符串常量池存在了堆之中。
这是官网翻译后的中文说明:在JDK 7中,在Java堆的永久生成中不再分配interned字符串,而是在Java堆的主要部分(称为young和old generation)中分配,以及应用程序创建的其他对象。此更改将导致更多的数据驻留在主Java堆中,而在永久生成中数据更少,因此可能需要调整堆大小。由于这种变化,大多数应用程序在堆使用上只会看到相对较小的差异,但是更大的应用程序加载了许多类,或者大量使用了string . intern()方法将看到更显著的差异。如果还有疑问可进行测试,测试参考http://blog.csdn.net/u014039577/article/details/50377805

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

=============================================================================================

案例演示:

demo1

数据准备

main类

//运行时, jvm 把AppMain的信息都放入方法区 
public class AppMain {
    //main方法本身放入方法区
    public static void main(String[] args) {
        Sample test1 = new Sample("测试1");
        test1.printName();
    }
}

Sample 类

public class Sample {
    private String name;

    //new Sample实例后,引用放入栈区,  对象放入堆
    public  Sample(String name){
        this.name = name;
    }

    //printName方法本身放入 方法区
    public void printName(){
        System.out.println(name);
    }
}

在JVM中的栈,堆,方法区的交互

这里写图片描述

=============================================================================================

demo2

public class baseTest {
    public static void main(String[] args) {
        //创建Car类实例,开始通过JVM向内存加载
        Car car1 = new Car(4, "red");
        car1.show();

        Car car2 = new Car(2, "black");
        car2.show();
    }
}

class Car{
    private int wheelNum;//成员变量(堆)
    private String color;//成员变量(堆)

    public Car() {

    }

    public Car(int w, String c) {//形参w,c为局部变量(栈)
        this.wheelNum = w;
        this.color = c;
    }

    public void show(){//方法名show放到方法区
        System.out.println("car:wheelNum-"+wheelNum+"color-"+color);
    }
}

=============================================================================================

接下来使用一个小例子来对堆栈进行更深一步的了解

String str = “a”;
String strr = “bc”;
String str1 = "abc";     //定义字符串变量str1
        String str2 = "abc";      //定义字符串变量str2
        String str3 = new String("abc"); //以new的方式定义字符串变量str3
        String str4 = new String("abc");//以new的方式定义字符串变量str4

String str5 = str + strr;
String str6 = “a” + “bc”;
       结果:
         * str1 ==str2    true;     ①
         * str2 ==str3    false;     ②
         * str3 ==str4    false;    ③
         *str1 == str5  false;       ④
         *str1.equals(str5)   true;   ⑤
         *str1==str6        true;   ⑥
讲解:

‘==’比较的是地址
     Str1 和 str2显然指向的是String中常量池中的一个地址。(何灵鸿上次发的String的两种创建方式)

equals比较的是内容

①  由于地址相同,所以true
②  由于str3 使用的new ,相当于在String类堆中创建了一个堆地址。而str2的地址则是在常量池中。地址对不上,因此false
③  和②的原理是一样的,相当于new了两个对象,也就是各自拥有了自己的地址。因此false。
④  str5使用的是字符串变量的相加。当看String的源码的时候,你会发现str.append()方法。本质上是先new 一个Stringbuilder对象,然后使用这个对象进行append,最后Builder对象toStirng回到String类型。也就是地址发生了变化。

结果:
⑤  比较的是内容,所以为true
⑥  也许看到这个会感到迷茫,一开始我也迷茫。后来一想,这种情况str6则是在常量池中进行的拼接(若存在,则直接指向;若不存在,拼接创建)

“java中JVM内存划分、栈区、堆区、方法区的区别”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI