当前位置:网站首页 > Java基础 > 正文

java基础语言和内存的区别



目录

一、JAVA内存结构

1.1 JVM启动流程:

1.2 JVM基本结构

1.2.1基本结构图

1.2.2 Java中的内存分配

二、Java内存模型

2.1 主内存和工作内存

2.2 内存间交互操作

2.3 java内存模型对并发提供的保障:原子性、可见性。有序性

2.4 先行发生原则

2.5 volatile型变量

三、Java对象模型

3.1 oop-klass model

3.2 Klass体系

3.3 InstanceClass

3.4 内存存储

四、总结


Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文我们要讨论的Java内存结构、Java内存模型和Java对象模型,这就是三个截然不同的概念,但是很多人容易弄混。

可以这样说,很多高级开发甚至都搞不不清楚JVM内存结构、Java内存模型和Java对象模型这三者的概念及其间的区别。甚至我见过有些面试官自己也搞的不是太清楚。不信的话,你去网上搜java基础语言和内存的区别索Java内存模型,还会有很多文章的内容其实介绍的是JVM内存结构。


一、JAVA内存结构

Java内存结构是每个java程序员必须掌握理解的,这是Java的核心基础。由于Java程序是交由JVM执行的,所以我们在谈Java内存区域划分的时候事实上是指JVM内存区域划分。

1.1 JVM启动流程:

java 内存架构 java的内存模型和内存结构_内存模型

JVM启动时,是由java命令/javaw命令来启动的。

1.2 JVM基本结构

1.2.1基本结构图

java 内存架构 java的内存模型和内存结构_内存结构_02

1、类加载子系统:负责从文件系统或者网络加载Class信息,加载的信息存放在一块称之方法区的内存空间。
2、方法区:就是存放类的信息、常量信息、常量池信息、包括字符串常量和数字常量等,还存放静态代码
3、Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到
Java堆中,还有定义的数组也存放在堆里面堆空间是所有线程共享
4、直接内存:JavaNio库允许Java程序直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。
5、Java栈:存放基本数据类型,局部变量每个虚拟机线程都有一个私有栈,一个线程的Java栈在线程创建的时候被创建,Java栈保存着局部变量、方法参数、同事Java的方法调用、
返回值等。
(解释:线程私有)
6、本地方法栈,(Java语言调用外部语言(C语言),方法使用native(native关键字在CAS底层代码中也有用到))最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通过使用C语言写)
8、PC(Program Couneter)寄存器也是每个线程私有的空间, Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法称为当前方法,如果当前方法不是本地方法,PC寄存器总会执行当前正在被执行的指令,
如果是本地方法,则PC寄存器值为Underfined,寄存器存放如果当前执行环境指针、程序技术器、操作栈指针、计算的变量指针等信息。
9、虚拟机核心的组件就是执行引擎,它负责执行虚拟机的字节码,一般户先进行编译成机器码后执行。

1.2.2 Java中的内存分配

java 内存架构 java的内存模型和内存结构_JVM_03

Java程序在运行时,需要在内存中的分配空间。为了提高运算效率,就对数据进行了不同空间的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

具体划分为如下5个内存空间:(非常重要)

  • :存放局部变量
  • :存放所有new出来的东西
  • 方法区:被虚拟机加载的类信息、常量、静态常量等。
  • 程序计数器(和系统相关)
  • 本地方法栈

1、程序计数器:

每个线程拥有一个PC寄存器

在线程创建时创建

指向下一条指令的地址

执行本地方法时,PC的值为undefined

2、方法区:

保存装载的类信息

  类型的常量池

  字段,方法信息

  方法字节码

通常和永久区(Perm)关联在一起

3、堆内存:

和程序开发密切相关

应用系统对象都保存在Java堆中

所有线程共享Java堆

对分代GC来说,堆也是分代的

GC管理的主要区域

现在的GC基本都采用分代收集算法,如果是分代的,那么堆也是分代的。如果堆是分代的,那堆空间应该是下面这个样子:

java 内存架构 java的内存模型和内存结构_内存结构_04

上图是堆的基本结构,在之后的文章中再进行详解。

4、栈内存:

  • 线程私有,生命周期和线程相同
  • 栈由一系列帧组成(因此Java栈也叫做帧栈)
  • 帧保存一个方法的局部变量、操作数栈、常量池指针
  • 每一次方法调用创建一个帧,并压栈

解释:

Java虚拟机栈描述的是Java方法执行的内存模型:每个方法被调用的时候都会创建一个栈帧,用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程就对应着一个栈帧在虚拟机中从入栈到出栈的过程。

在Java虚拟机规范中,对这个区域规定了两种异常情况:

  • 如果线程请求的栈深度太深,超出了虚拟机所允许的深度,就会出现StackOverFlowError(比如无限递归。因为每一层栈帧都占用一定空间,而 Xss 规定了栈的最大空间,超出这个值就会报错)
  • 虚拟机栈可以动态扩展,如果扩展到无法申请足够的内存空间,会出现OOM

Java栈之局部变量表:包含参数和局部变量

    局部变量表存放了基本数据类型、对象引用和returnAddress类型(指向一条字节码指令的地址)。其中64位长度的long和double类型的数据会占用2个局部变量空间(slot),其余数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配。

例如,我写出下面这段代码:

 

上方代码中,静态方法有6个形参,实例方法有3个形参。其对应的局部变量表如下:

java 内存架构 java的内存模型和内存结构_对象模型_05

上方表格中,静态方法和实例方法对应的局部变量表基本类似。但有以下区别:实例方法的表中,第一个位置存放的是当前对象的引用。

Java栈之函数调用组成栈帧

方法每次被调用的时候都会创建一个栈帧,例如下面这个方法:

 

当它每次被调用的时候,都会创建一个帧,方法调用结束后,帧出栈。如下图所示:

java 内存架构 java的内存模型和内存结构_JVM_06

Java栈之操作数栈

Java没有寄存器,所有参数传递都是使用操作数栈

例如下面这段代码:

 

压栈的步骤如下:

  0:   iconst_0 // 0压栈

  1:   istore_2 // 弹出int,存放于局部变量2

  2:   iload_0 // 把局部变量0压栈

  3:   iload_1 // 局部变量1压栈

  4:   iadd      //弹出2个变量,求和,结果压栈

  5:   istore_2 //弹出结果,放于局部变量2

  6:   iload_2 //局部变量2压栈

  7:   ireturn   //返回

如果计算100+98的值,那么操作数栈的变化如下图所示:

java 内存架构 java的内存模型和内存结构_对象模型_07

Java栈之栈上分配:

小对象(一般几十个bytes),在没有逃逸的情况下,可以直接分配在栈上

直接分配在栈上,可以自动回收,减轻GC压力

大对象或者逃逸对象无法栈上分配

栈、堆、方法区交互:

java 内存架构 java的内存模型和内存结构_内存模型_08

记住了:jvm内存结构 = java内存结构

二、Java内存模型

JMM目标:定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节(这里变量指代的是实例字段、静态字段和构成数组对象的元素)

学习JMM可以和内存模型(处理器、高速缓存器、主内存空间)对等起来:内存模型是为了解决处理器和内存之间的速度差异问题,JMM是为了解决线程之间的信息共享,数据一致性问题

java 内存架构 java的内存模型和内存结构_java 内存架构_09

2.1 主内存和工作内存

(1)所有变量均存储在主内存(虚拟机内存的一部分)

(2)每个线程都对应着一个工作线程,主内存中的变量都会复制一份到每个线程的自己的工作空间,线程对变量的操作都在自己的工作内存中,操作完成后再将变量更新至主内存;

(3)其他线程再通过主内存来获取更新后的变量信息,即线程之间的交流通过主内存来传递

Note:JMM的空间划分和JVM的内存划分不一样,非要对应的话,关系如下:

(1)JMM的主内存对应JVM中的堆内存对象实例数据部分

(2)JMM的工作内存对应JVM中栈中部分区域

每一个线程有一个工作内存。工作内存和主存独立。工作内存存放主存中变量的值的拷贝。

java 内存架构 java的内存模型和内存结构_对象模型_10

当数据从主内存复制到工作存储时,必须出现两个动作:第一,由主内存执行的读(read)操作;第二,由工作内存执行的相应的load操作;

当数据从工作内存拷贝到主内存时,也出现两个操作:第一个,由工作内存执行的存储(store)操作;第二,由主内存执行的相应的写(write)操作。

每一个操作都是原子的,即执行期间不会被中断

对于普通变量,一个线程中更新的值,不能马上反应在其他变量中。如果需要在其他线程中立即可见,需要使用volatile关键字作为标识。

java 内存架构 java的内存模型和内存结构_java 内存架构_11

指令重排:

java 内存架构 java的内存模型和内存结构_java 内存架构_12

指令重排:破坏了线程间的有序性:

java 内存架构 java的内存模型和内存结构_java 内存架构_13

指令重排:保证有序性的方法:

java 内存架构 java的内存模型和内存结构_java 内存架构_14

指令重排的基本原则:

程序顺序原则:一个线程内保证语义的串行性

volatile规则:volatile变量的写,先发生于读

锁规则:解锁(unlock)必然发生在随后的加锁(lock)前

传递性:A先于B,B先于C 那么A必然先于C

线程的start方法先于它的每一个动作

线程的所有操作先于线程的终结(Thread.join())

线程的中断(interrupt())先于被中断线程的代码

对象的构造函数执行结束先于finalize()方法

2.2 内存间交互操作

JMM定义了8种操作(原子操作),虚拟实现时保证这8中操作均为原子操作,以下为8中操作的介绍以及执行顺序:

(1)lock(锁定):作用于主内存的变量,把一个变量标志为一个线程占有状态(锁定)

(2)unlock(解锁):作用于主内存的变量,把一个变量从一个线程的锁定状态解除,以便其他线程锁定

(3)read(读取):作用于主内存的变量,将变量从主内存读取到线程的工作空间,以便后续load操作使用

(4)load(载入):作用于工作空间的变量,将load操作从主内存得到的变量放入工作内存变量副本中

(5)use(使用):作用于工作空间的变量,将工作空间中的变量值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将执行这个操作。

(6)assign(赋值):作用于工作内存的变量,把一个从执行引擎接收的值赋给工作空间的变量

(7)store(存储):作用于工作内存的变量,把工作空间的一个变量传到主内存,以便后续write操作使用

java 内存架构 java的内存模型和内存结构_内存结构_15

Note:这些操作之间是存在一些冲突的,需保持顺序执行

有关操作的一些规定:

(1)不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况。

(2)不允许一个线程丢弃它的最近assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。

(3)不允许一个线程无原因的(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。即不能对变量没做任何操作却无原因的同步回主内存

(4)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,就是对一个变量执行use和store之前必须先执行过了load和assign操作

(5)一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值

(7)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store write)

Note:以上可以完全确定Java程序中哪些内存访问操作在并发下是安全的。

2.3 java内存模型对并发提供的保障:原子性、可见性。有序性

(1)原子性:

Java内存模型直接保证得原子性操作包括read、load、use、assign、store和write这六个。可以认为基本数据类型(long和double除外,64字节在32位上需要两部操作)的变量操作是具有原子性的,而lock和unlock对应在高层次的字节码指令monitorenter和monitorexit来隐式的使用底层的这两个操作,高层次的这两个字节码指令在Java中就是同步块,Synchronized关键字,因此在synchronized块之间的操作也具备原子性。

(2)可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了volatile外,Synchronized和final也提供了可见性,

Synchronized的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得

Final的可见性是指被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把this的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了半”的对象),那在其他线程就能看见final字段的值

(3)有序性

Java程序在本线程所有操作是有序的(线程表现为串行的),在一个线程中看另一个线程是无序的(指令重排和工作内存与主内存同步延迟现象),可以用Synchronized和volatile来保证线程操作的有序性

Volatile本身就包含禁止指令重排序的语义

Synchronized是因为:一个变量在同一时刻只能被一个线程lock,即串行化操作保证有序

2.4 先行发生原则

先行发生原则是java内存模型中定义的两项操作之间的偏序关系,这个原则作为依据来判断是否存在线程安全和竞争问题,如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序。以下为8个具体原则:

(1)程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

(2)传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

(3)对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

(4)管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

(5)volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

(6)线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作

(7)线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行

(8)线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

2.5 volatile型变量

Volatile是java虚拟机提供的最轻量级的同步机制,它具有可见性和有序性,但不保证原子性,在大多数场景下,volatile总开销仍然比锁要低

java 内存架构 java的内存模型和内存结构_对象模型_16

volatile保证了变量的新值能立即同步到主内存,以及每次使用之前立即从主内存刷新。因此可以说volatile保证了多线程操作时变量的可见性,而普通变量不能保证这一点

(1)volatile可以保证变量对所有线程的可见性,即一条线程修改了变量的值,新值对于其他线程来说是可以立即得知的。volatile变量在各个线程的工作内存中不存在一致性问题(即时存在,由于每次使用之前都得刷新,执行引擎看不到不一致的情况,所以认为是 不存在一致性问题)volatile实际上就使用到了内存屏障技术来保证其变量的修改对其他CPU立即可见。

(2)volatile禁止指令重排,保证有序性

*Volatile不能保证的原子性操作有以下两条:

(1)对变量的写入操作不依赖于该变量的当前值(比如a=0;a=a+1的操作,整个流程为a初始化为0,将a的值在0的基础之上加1,然后赋值给a本身,很明显依赖了当前值),或者确保只有单一线程修改变量。

(2)该变量不会与其他状态变量纳入不变性条件中,(当变量本身是不可变时,volatile能保证安全访问,比如双重判断的单例模式。但一旦其他状态变量参杂进来的时候,并发情况就无法预知,正确性也无法保障)

三、Java对象模型

在内存中,一个Java对象包含三部分:对象头、实例数据和对齐填充。而对象头中又包含锁状态标志、线程持有的锁等标志。

3.1 oop-klass model

OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

oop体系:

 

如上面代码所示, oops模块包含多个子模块, 每个子模块对应一个类型, 每一个类型的oop都代表一个在JVM内部使用的特定对象的类型。其中有一个变量oop的类型oopDesc是oops模块的共同基类型。而oopDesc类型又包含instanceOopDesc (类实例)、arrayOopDesc (数组)等子类类型。其中instanceOopDesc 中主要包含以下几部分数据:markOop _mark和union _metadata 以及一些不同类型的 field。

在java程序运行过程中, 每创建一个新的java对象, 在JVM内部就会相应的创建一个对应类型的oop对象来表示该java对象。而在HotSpot虚拟机中, 对象在内存中包含三块区域: 对象头、实例数据和对齐填充。其中对象头包含两部分内容:_mark和_metadata,而实例数据则保存在oopDesc中定义的各种field中。

_mark:

_mark这一部分用于存储对象自身的运行时数据, 如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等, 这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit, 官方称它为 "Mark Word"。对象需要存储的运行时数据很多, 其实已经超出了32位和64位Bitmap结构所能记录的限度, 但是对象头信息是与对象自身定义的数据无关的额外存储成本, 考虑到虚拟机的空间效率, Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息, 它会根据对象的状态复用自己的存储空间。  

_metadata:

_metadata这一部分是类型指针, 即对象指向它的类元数据的指针, 虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针, 换句话说查找对象的元数据信息并不一定要经过对象本身, 其取决于虚拟机实现的对象访问方式。目前主流的访问方式有使用句柄和直接指针两种, 两者方式的不同这里先暂不做介绍。另外, 如果对象是一个Java数组, 那么在对象头中还必须有一块用于记录数组长度的数据, 因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小, 但是从数组的元数据中却无法确定数组的大小。  

3.2 Klass体系

 

和oopDesc是其他oop类型的父类一样,Klass类是其他klass类型的父类。

Klass向JVM提供两个功能:

  • 实现语言层面的Java类(在Klass基类中已经实现)
  • 实现Java对象的分发功能(由Klass的子类提供虚函数实现)

HotSpot JVM的设计者因为不想让每一个对象中都含有一个虚函数表, 所以设计了oop-klass模型, 将对象一分为二, 分为klass和oop。其中oop主要用于表示对象的实例数据, 所以不含有任何虚函数。而klass为了实现虚函数多态, 所以提供了虚函数表。所以,关于Java的多态,其实也有c++虚函数的影子在。

3.3 InstanceClass

VM在运行时,需要一种用来标识Java内部类型的机制。在HotSpot中的解决方案是:为每一个已加载的Java类创建一个InstanceClass对象,用来在JVM层表示Java类。

InstanceClass内部结构:

 

在JVM中,对象在内存中的基本存在形式就是oop。那么,对象所属的类,在JVM中也是一种对象,因此它们实际上也会被组织成一种oop,即klassOop。同样的,对于klassOop,也有对应的一个klass来描述,它就是klassKlass,也是klass的一个子类。klassKlass作为oop的klass链的端点, 它的klass就是它自身。

3.4 内存存储

我们首先来看看下面这段代码的存储结构。

 

存储结构如下:

java 内存架构 java的内存模型和内存结构_java 内存架构_17

由此我们能得出结论: 对象的实例(instantOopDesc)保存在堆上,对象的元数据(instantKlass)保存在方法区,对象的引用保存在栈上。

在JVM加载java类的时候, JVM会给这个类创建一个instanceKlass并保存在方法区, 用来在JVM层表示该java类。当我们使用new关键字创建一个对象时, JVM会创建一个instanceOopDesc对象, 这个对象包含了对象头和元数据两部分信息。对象头中有一些运行时数据, 其中就包括和多线程有关的锁的信息。而元数据维护的则是指向对象所属的类的InstanceKlass的指针。

四、总结

我们再来区分下JVM内存结构、 Java内存模型 以及 Java对象模型 三个概念。
JVM内存结构,和Java虚拟机的运行时区域有关。
Java内存模型,和Java的并发编程有关。
Java对象模型,和Java对象在虚拟机中的表现形式有关。

关于这三部分内容,本文并未分别展开,因为涉及到的知识点实在太多,如果读者感兴趣,可以自行学习。

最后,这三个概念非常重要,一定要严格区分开,千万不要在面试中出现答非所为的情况。

  • 上一篇: 多线程java基础
  • 下一篇: java 基础开源类库
  • 版权声明


    相关文章:

  • 多线程java基础2024-10-20 21:34:04
  • 优秀的Java基础入门教材推荐2024-10-20 21:34:04
  • java数据基础及程序结构2024-10-20 21:34:04
  • O基础java程序员2024-10-20 21:34:04
  • java基础逻辑运算符2024-10-20 21:34:04
  • java 基础开源类库2024-10-20 21:34:04
  • 零基础学java薪资2024-10-20 21:34:04
  • 我的世界指令基础教学java2024-10-20 21:34:04
  • java基础类型和字节2024-10-20 21:34:04
  • java基础数据类型默认值2024-10-20 21:34:04