java内存机制

java内存模型把java分成:java栈,堆内存,方法区(常量池,静态池),程序计数,本地方法栈,执行引擎

java栈

      每启动一个线程,java虚拟机都会为它分配一个java栈,java栈中只会执行两种操作:压栈出栈
      只有通过return返回或者通过抛出异常而中止才会将当前帧释放掉。所以需要对异常进行处理,只有对异常进行处理才会按照正常的流程去执行。如果不对异常进行处理会强制释放掉当前栈帧。
      java栈中的数据,对于每个线程都是独立的。
      java栈保存了对象的引用,程序运行的中间结果、方法调用的栈帧等。
      实际上java栈就是一片内存片段



堆内存

      一个java虚拟机实例只存在一个堆空间,所有线程都共享这个堆。
      程序并不会主动去释放它,堆空间的管理都是由垃圾收集器来处理的。
      堆内存用来保存java中创建出来的对象。

堆空间分两种设计
       1、将堆分为两部分:一个句柄池,一个对象池。句柄池中的句柄每个条目分为两部分,一个指向对象实例,一个指向方法区中的类型数据指针。
      这种方式在访问的时候需要进行两次指针传递,但是可以更好的解决碎片,当移动对象池中的对象时,句柄部分只需要改变一下指向新的地址就可以了

      2、使对象直接指向一组数据,该数据包含对象实例和方法区类型指针,这样设计的缺点刚好和第一种相反,他可以减少访问次数,但是在进行碎片整理的时候,它必须更新运行时数据区中移动对象的引用。
      堆中的对象有一个独立的逻辑部分,用于保存多线程并发访问时的同步。



方法区

      方法区中保存了:
类信息类变量(静态变量)字段信息方法信息一个到Class类的引用一个到ClassLoader类的引用
      所有线程都共享方法区中的数据。在创建一个对象的时候,也是根据方法区中的类信息来大致确定一个对象所需要的内存大小的。
      方法区的大小不是固定的,虚拟机可以根据应用的需要动态调整,由于内存分配问题,方法区的存储空间可以不是连续的。
每个类都维护了一个自己的常量池。



本地方法栈

      本地方法栈是调用一些比较特殊的方法,这些方法可以直接访问运行时数据区,也可以直接使用操作系统的寄存器。一般本地方法是由其他编程语言编写的。例如:c语言。



程序计数器

      其中保存了一些PC寄存器。



执行引擎

支撑java程序的运行核心。



java虚拟机实例:
      一个java应用对应一个java虚拟机实例,一个java虚拟机实例只有一个方法区和一个堆。
      虚拟机实例与实例之间的方法区和堆互相独立。

一个对象的创建过程:
Person p=new Person();
      Person p这句声名就是加载class信息。通过new Person()进入构造方法,但是在执行构造方法前会先给静态代码或静态方法又或者静态变量分配空间;在给普通方法分配空间,但是并不会给内部的形参分配空间,形参只有在运行期间才会在栈中分配空间。然后给普通的类成员变量分配空间,按从上到下的方式分配。最后才执行构造方法。
      为什么形式参数会分配在栈中,就是为了加快访问速度。为什么栈中的速度比堆快,因为栈本身的数据存储是连续的,而堆中的存储不是连续的。
      对象在创建的时候,其实会给对象默认注入thissuper引用

基本数据类型的存储问题:
int a=3;
int b=3;
int c=6;
上面这些虽然是int,是java的基本数据类型。但是还是遵循引用原则。a,b,c都是int类型的引用。只不过基本数据类型的实际值存储在栈中,而对象类型存储在堆中。但是两者的使用方法都是一致的,都是使用引用的方法去进行数据的访问。这里的a实际上保存的是int的值的地址的引用。以上说的是在同一个线程中,线程访问是私有的。栈也可以抽象的理解成是私有的。,所以当你内存中有一个相同值的的对象或者基本数据类型的值存在的话,他会直接去引用你这个地址;而不是重新分配空间。这个时候a,b都引用int类型地址中的3,假设你修改了a这个引用的值,b的值不会发生改变。这是a修改值后,他会重新搜索栈,找有没有和他一致的值,如果有指向那个地址;如果没有找到一致的值,则重新分配空间。这也是基本数据类型引用和对象引用的唯一的区别,这有点类似于final,不可变对象,每次变化值都会产生新的值。对象引用如果两个引用变量指向一个地址,其中一个的引用变量修改这个对象地址的中的值,因为这两个引用变量都是指向同一个地址,所以两个引用变量的值都发生变化。当然,new关键字除外。new关键字产生的值不管你是否一致,都会强制分配空间。

继承问题:
子类继承父类:子类会继承父类所共享出来的方法和属性,子类中可以直接调用父类中共享的方法和变量。为什么可以呢?因为在子类被加载之前会先去加载父类。然后再加载子类。因此你才能在子类中看到父类的的东西。这些东西都是父类所共享出来的。但是这里是单独创建了一个父类的对象,当你new子类的时候,子类的构造参数的第一行默认调用父类的构造方法。不管你子类有没有显示的调用,都会调用父类。也就相当于new了父类。

方法表
      为了提高方法区的访问效率,提供了方法表,方法表是负责记录所有类型的地址提高访问效率,就像操作系统的页表一样,
      java虚拟机规范没有明确一定要使用方法表。(牺牲时间换空间)

为什么要把栈和堆区分开来呢?栈不也可以存放数据吗?
1、从设计角度上看,栈代表了处理逻辑,而堆代表了数据。
2、栈与堆得分离,使得堆中的内容可以被多个栈共享,从而节约很多空间。
3、栈因为运行时的需要,一般保存了系统的上下文context。因为栈只能向上增长。如果存放内容的话,就会限制住存放的能力。而堆不同,因为堆本身的数据结构就是哪里有足够的空间就往哪里放,所以可以动态的增长,这样就实现了动态增长。
4、面向对象就是对堆和栈的完美结合。

如果是某个对象中的基本数据类型,只声名没有赋值不分配空间,而且因为你这些基本数据类型是存放在对象中的。所以分配的内存也是在堆中。

基本数据类型的变量如果是临时变量,只要定义了,就会分配内存空间,不管是否被赋值;如果是作为对象的属性,只要不实例化这个对象,就不会分配内存空间。

在堆中产生的数组或者对象,可以在栈中定义一个特殊的变量,然后让栈中的这个变量去指向堆中数组或者对象的首地址。栈中的这个变量就成了数组或者对象的引用变量。以后在程序中就可以使用栈中的引用变量来访问堆中的数组或者对象。引用变量就相当于给数组或者对象起了一个名称。
引用变量是普通的变量,定义时是在栈中分配空间。引用变量在使用完后占用的内存会被自动被释放。但是引用变量所指向的数组或者对象即使没有变量引用它,它会变为垃圾,不能再被使用,但是还占有内存空间。在随后的一个不确定的时间被垃圾回收器回收。这也是java比较占内存的原因。
实际上,栈中的引用变量指向堆内存中的变量,这就是java中的“指针”,虽然java没有指针。

比如说我们在main函数中实例化一个对象:
Oject obj=new Object()这个obj就是在被保存在栈内存中,(更加准确的说,栈内存中保存的是引用变量,保存的是堆内存中访问地址),而对象的具体的内容也就是new Object()会被保存在堆内存中,比如说这个对象的属性被保存在堆内存中。

堆内存空间必须使用new关键字才能开辟。

引用类型放在栈空间,也就是常说的声明定义。但这里指的是引用类型。

例如:
Person a=new Person();
Pserson b=a;
底层存储:变量a保存在栈空间中,new Person()部分被保存在堆空间中,栈空间中a的值保存的是堆空间中new Person()的首地址。变量b也是保存在栈空间中,因为a赋值给b,其实就是将栈空间中a所保存的new Person()的首地址赋值给b。实际上就是这两个变量a,b都指向new Person()的首地址。这两个变量都可以操作这个地址的值。

java中的引用传递:就是将一个堆内存的空间的使用权交给多个栈内存空间,每个栈内存空间都可以修改堆内存空间的内容。实际上就是栈空间保存了堆空间内的地址,然后通过地址去访问修改堆空间中地址中的数值。

一个栈内存只能指向一个堆内存空间,如果想要再指向其他的堆内存空间,则必须先断开以有的指向才能能分配新的指向。

在函数中定义的基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当这个函数运行完后,这个函数中的变量会被自动销毁。java会自动释放掉该变量的内存空间,该内存空间可以立即被另作它用。

如果在eclipse中使用debug调试,如果一个变量有id值,则说明这个变量使用的内存空间是堆中的内存空间。

string中如果直接使用一个变量赋值,不使用new关键字,如果这个变量的值相同,则之后的变量不会重新分配空间,会直接指向与之相同值的的变量地址。如果值不一样,则会重新分配空间。如果使用new关键字,不管值是否一样,都会产生新的内存地址。

static所修饰的变量在被加载后,就一直存在内存当中,直到程序结束,才被释放。static所修饰的变量在类被加载的同时,就存在内存中了,所以才被称之为类变量。因为类的属性是在创建对象时,通过构造器初始化,所以才被称为实例变量。

静态代码块中不允许有可变的数据结构,因为静态代码块在编译时就要给他分配空间,所以这个必须要事先确定好存储空间的大小。

在栈中分配的空间一般在编译的时候就需要确定,因为存储在栈中的空间是联系的。不像堆空间,堆空间是不连续的一块区域,所以可以随便存储。

那为什么说栈符合程序的运行流程呢?因为程序在运行的时候,是从上致下运行。上面的代码先写出,上面的代码先执行;下面的代码后写出,下面的代码后执行。这正好符合栈的特点,先进先出,后进后出的特点。

那为什么说堆空间符合对象的存储呢?
一个对象一般由一系列的属性,其他对象引用,static代码块组成。而且每个对象都是一个独立的存在,虽然有可能他们内部有对其他类的引用。但是相对来说,每一个类依旧是独立的。如果这一个个放在栈中存储,因为栈的特点,假设我在2这个空间存储了一个对象A,我想在5这个空间引用2空间的对象A,我就又要回到2空间,这样就造出了死循环。而且假设我就这样访问,设置了跳出点。跳出了。如果对象多了,那么这个栈空间就会变得相当的大,由于栈空间又是连续的空间。就容易造成很多不连续的空间没有利用到,又造成了空间上的浪费。再者栈这个空间是需要提前知道自己需要多少空间大小,对象的中又很容易使用变体类型,这种不确定空间大小的数据结构。这不符合栈的数据结构。

栈以帧为单位保存线程的状态。jvm对栈只进行两种操作:以帧为单位的压栈和出栈操作。线程使用的方法叫做当前方法,当前方法使用的帧叫做当前帧。在此方法执行期间,这个帧将用来保存参数,局部变量,中间计算过程和其他数据。类似于ps中的历史记录。

jvm的组成
在这里插入图片描述

线程的组成
在这里插入图片描述
每个线程的底层组成:
一个线程通常由:程序计数器,虚拟机栈,本地方法栈组成。
每个线程的这些组成部分都是独立的。
方法区和堆内存再线程之间是共享的。

监控内存工具

jmap:可以保存程序执行的堆内存快照文件。

Jstack:可以用来保存线程的执行快照。

然后就可以使用jvisualvm可视化工具取分析这些文件了。

      如果一个变量只声名不使用,不会开辟空间。汇编可证明,因为假设int b;反汇编显示过去,根本没有汇编的语句,那就证明在二进制码中压根没有去开辟这个内存的空间。但是在java中因为局部变量在对class字节码进行校验的时候会赋值一个初始值。

注意:
1、在程序中,分为主存和工作内存,在java中可以理解成主存就是堆内存,工作内存就是java栈,也就是线程的局部内存。在读取或者写数据的时候,先将主存的数据读入到工作内存中,然后在工作内存中进行编辑,然后在从工作内存中写出到主存中,这也是为什么会造成数据污染的一个原因。
2、工作内存中永远不会保存原始数据,保存的都是原始数据的拷贝副本。
3、每一个class文件都维护了一个自己的常量池。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值