JAVA内存区域

内存区域(简)

背景:java运行时,java虚拟机即jvm,会拿到操作系统给他分配的内存区域。jvm把这个内存分为5个区域,每个区域都有自己的用途,这5个区域存储不同的数据,五个区域的数据结构不同并且创建时间和销毁时间也不一样,执行过程不一样,这样再不影响数据功能的前提下,提高代码的运行效率。jvm把内存区域的分为五个区域,他们分别是栈区(虚拟机栈)、堆区、本地方法栈、程序计数器、方法区(元空间)

程序员能控制的区域就只有堆区,栈区、方法区。程序计数器和本地方法栈都是自动控制的,程序员控制不了。堆和元空间是线程共享的;虚拟机栈、本地方法栈、程序计数器是线程共享的。

内存区域的结构图

在这里插入图片描述

或者这么画(从左到右依次是,方法区、堆、栈、程序计数器、本地方法栈。其中栈里面是有多个线程的,每个线程有自己一个小区域,这个小区域下有栈帧(方法生成的,一个方法一个栈帧)还有一些其他的不管了,程序计数器给每一个线程都记下线程切换前运行的位置)

在这里插入图片描述

本地方法栈

本地方法栈是存储C++/C的native方法运行的栈区,就是java底层涉及的代码是涉及到C/C++的,所以,java运行到底层C/C++代码就会自动放在这里面来执行(因为java的底层是C++和C,所以不要感觉疑惑)。这部分主要与虚拟机用到的 Native 方法相关,一般情况下, Java 应用程序员并不需要关心这部分的内容,因为这个区域是我们程序员控制不了的。

程序计数器

程序计数器是指向程序当前运行的位置的,程序计数器也叫PC 寄存器。JVM支持多个线程同时运行,每个线程都有自己的程序计数器。这个是线程私有的。这样的话,要是多线程运行,再切回原来线程就可以靠程序计数器知道原来运行到哪一行,就知道继续执行这个线程是从哪里开始执行的。这个是我们程序员无法控制的,所以只要知道它是干嘛的就行了。

堆区

堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。几乎所有的对象实例(包括类的成员变量。不是局部变量哦,局部变量在栈里面)和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。JVM规范中规定堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。

值得注意的是:基本所有对象都在堆上被创建,而对象的声明是在栈中,它存着堆上的引用。例如 Object object = new Object();object为对象的声明,存在虚拟机栈里面,新建的Object对象存在于堆上面,包括类的成员变量。

Java堆分为年轻代(Young Generation)和老年代(Old Generation);年轻代又分为伊甸园(Eden)和幸存区(Survivor区);幸存区又分为From Survivor空间和 To Survivor空间。

JDK8后(包括JDK8),静态成员变量、运行时常量池和字符串常量池等也在堆中;与C不同,动态申请的内存不需要程序员回收,Java有自动垃圾回收机制。JDK1.7之后运行时常量池和字符串常量池就放在堆中了。

栈区

虚拟机栈是线程私有的,每个线程都会分配一个栈的空间,一个线程栈的生命周期与线程相同(随线程而生,随线程而灭)。栈里面存放局部变量对象的引用(如String s=new String(“aa”);中的s就是放在栈区的)、基本数据类型的值(比如有局部变量int i=1;那么这个i和1都是保存在栈中的。要是这个int i=1;是成员变量的话,这个i和1都是放在堆区的,i和他的值是放在一起的,他有一个变量表,一列存变量一列存值。但是只是说基础类型,引用类型的对象是放在别的地方的)。

扩展:

  • 栈帧是栈的元素(是用于支持虚拟机进行方法调用和方法执行的数据结构)。每个方法在执行时都会创建一个栈帧。栈帧中存储了局部变量表、操作数栈、动态连接和方法出口等信息。每个方法从调用到运行结束的过程,就对应着一个栈帧在栈中压栈到出栈的过程。只有栈顶的方法是正在执行的方法。一个线程有一个栈区,不同线程有它自己的栈区。

    一个线程分配的栈的结构如图(一个栈帧相当于一个方法):

在这里插入图片描述

栈帧中的局部变量

  • 局部变量表就是用来存储方法中的局部变量(包括在方法中生命的非静态变量以及函数形参),对于基本数据类型,直接存值;对于引用类型的变量,存储指向该对象的引用。由于==它只存放基本数据类型的变量、引用类型的地址和返回值的地址,这些类型所需空间大小已知且固定,==所以当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全可以确定的,在方法运行期间也不会改变局部变量表的大小。

栈帧中的动态连接

栈帧中的返回地址

  • 方法返回地址:当一个方法执行结束后,要返回到之前调用它的地方,因此在栈帧中需要保存一个方法返回地址。

栈帧中的操作数

  • 主要用于保存计算过程到中间结果,同时作为计算过程中变量临时的存储空间,比如int i=1;i=i++;他是怎么执行的呢,答:局部变量表生成标识为i的变量,操作数栈将1压栈,=右边运算已结束,操作数栈将1弹栈,通过=赋值给局部变量表的i变量。对于i=i++;i是局部变量存在的,他会去拿那个局部变量表里的那个i的值,i++意思是操作数栈将局部变量表i变量的值压栈,操作数栈栈顶元素是1;之后对局部变量表i变量的值自增变为2;(=右边运算已结束)最后操作数栈将栈顶元素1弹栈,通过=赋值给局部变量表的i变量,此时局部变量表变量i的值为1。

方法区

一个new HelloWorld();语句执行的过程就会先把HelloWorld的字节码文件(即HelloWorld.class文件)就是装载到方法区,并记录那个类的一些信息。然后再区堆区创建一个对象。方法区主要存储的数据为类装载的一些信息。

类装载的信息包括:

  • 类型信息:类的全限定名(即类的"包名.类名"这个就是全限定名)、这个类父类的全限定名、这个类是类还是接口、这个类的访问修饰符、这个类型直接接口的一个有序列表

  • 域(Field)信息,即成员变量信息:包括域名称、域类型、域修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)(成员变量的默认值应该也在方法区比如有些成员变量的类的声明里是带值的,如类下有一个成员变量int i=12。那它装载的时候在方法区应该也有值)

    JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

    注意:这个域,就是指字段或叫成员变量

  • 方法信息:方法名称;方法的返回类型(或void);方法参数的数量和类型(按顺序);方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集);方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外);异常表( abstract和native方法除外)每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

  • 常量池和静态池(可以放静态变量)。但是这两个池在JDK1.8之后被移到了堆区。

下面看一下java类怎么装载到方法区的,java类源代码是.java文件,然后编译后变为.class文件,他们通过线程的运行到相应的语句,比如运行到某方法中的这句语句,Person person=new Person();它就会去装载,把Person.class这个字节码文件先装到方法区里去。因为是通过方法运行才装载的嘛,方法是在栈里面的,所以这个箭头是这样画的。(注意一点,在JDK1.8之后这里的常量池是应该在堆区的)

在这里插入图片描述

方法区的变化在JDK不同版本的变化比较大

下面看一下变化:
在这里插入图片描述

JDK8之前,静态成员变量存放在方法区;但JDK8之后就取消了“永久代”,取而代之的是“元空间”(元空间和永久代都是方法区的一种实现方式,方法区是JVM规定的一个方法区的概念和作用(就像是定义了一个接口,没有写具体的实现,只写了作用干什么)(永久代和方法区相当于是具体实现的代码。相当于Map这个接口定义了一个put方法是做什么的,然后下面的HashMap、TreeMap用它们自己的方式去实现这个这个功能)),因为永久代的消失,永久代中的数据也进行了迁移,静态成员变量、常量池都迁移到了堆中

总之相当于:java1.8把java1.7之前的实现方法区的永久代给瓜分了,相当于把永久代里面的静态变量和常量池等并入堆中,然后出现一个叫元空间的区域,它来实现方法区,元空间把永久代存放的类和类加载器的元数据信息的这个功能抢走了,然后永久代就彻底消失了。

JDK8之前,由永久代实现,主要存放类的信息、常量池、方法数据、方法代码等;JDK8之后,取消了永久代,提出了元空间,并且常量池、静态成员变量等迁移到了堆中;元空间不在虚拟机内存中,而是放在本地内存中。

常量池

字节码文件中即那个.class文件中,就包含了常量池。常量池就相当于,一个表格,然后表格下一个符合对应一个字符串,所以你看那个字节码文件有很多都是很多符号,.class文件中有很多那个符号,.class文件中还有一个表格叫常量池,就是因为有常量池的原因,你编译为字节码文件就很小,你想,你一个程序肯定会用到很多public这样的关键字还有你定义的i变量这个标识符,会很多,要是你把public用一个字符来表示,文件大小不是小很多了嘛,这就是常量池存在的好处。字节码文件中的常量池加载到内存中的方法区以后,对应的结构就叫做运行时常量池

在.class文件里就自带有一个常量池,为了就是减小文件的大小。在一起常量池在方法区,后来被移到了堆区了,但是作用还是一样的,只是位置变了。

注意点

  1. 这是jvm把二维数组存到内存里的模式,你看,引用类型的arr变量是在栈里面的,且他的值(0x0011这个地址值)是在栈里面的,相当于栈里面有一张表,即变量对应值的那种两列多行的表格。然后堆里面存数组,数组里面的值也是存在与堆里面的,不要去想字面量不是常量吗,不应该放在常量池里面吗,不要这么想,你现在就当没有常量池吧,等学好JVM就懂了。

在这里插入图片描述

  1. 局部变量不是方法的嘛,方法要进栈,局部变量依赖于方法,所以局部变量也是在栈里面的并且那些int i=1;这样的1这个值虽然是常量,但是也是在栈里面的,不是在常量池里面的。对象是在堆里面的,成员变量依赖于对象,所以成员变量是在堆里面的。这些说的都是运行时候,类加载的时候,那些类的信息,什么成员变量,成员变量的值什么的都在方法区,然后运行时开始复制到各个内存部分了。

  2. 运行时,局部变量的如果时基本数据类型,那么局部变量和它的值都是在栈里面的;要是局部变量是引用类型,那么栈区只存放这个引用变量和引用对象的地址,致于引用对象就是在堆区了。要是引用对象里面有一个成员变量,那成员变量也是在堆区。又要是,那个成员变量是字符串引用类型的,那么那个堆里面那个对象里面的成员变量就存一个成员变量名和字符串对应的地址,而字符串这个对象的实体放在堆里面的常量池里面,要是这个成员变量是基本数据类型那她的值就是在堆的这个对象空间里。

  3. 分析一句语句来理解一下,比如有一个方法里面有一句语句Person person = new Person();这个Person是一个类,这个Person person = new Person();是在栈里面的一个线程的一个帧栈里面执行的(因为它是方法),然后运行到这个,Person就会去先把这个Person.class这个字节码装载到方法区里。这样方法区里就知道这个类的一些信息,所以这个Person这个类信息是在方法区的(只是类的信息在方法区哦),因为这个这个Persion person是定义了一个变量且是在方法里的,所以这个person这个变量在栈区的栈帧里面存放着,且这个变量的值是一个地址。new Peron()是在堆区生成了一个对象内存。但是生成是怎么知道是怎样的呢?就是这个堆要去方法区找那个类的信息,然后才生成一个对象,所以这个堆有一个箭头指向方法区,表示去那里寻找模板数据。

在这里插入图片描述

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值