JVM整体结构及内存模型

JAVA体系

JDK(Java Development Kit)又称J2SDK(Java2 Software Development Kit),是Java开发工具包,包含了JAVA运行环境(JRE)

JRE(Java Runtime Enviroment)是Java的运行环境。它包括Java虚拟机、Java平台核心类和支持文件。它不包含开发工具(编译器、调试器等)。

在这里插入图片描述

Java语言的跨平台特性

JAVA语言能够跨平台,是因为JVM帮我们处理了不同操作系统上的底层指令语言。
在这里插入图片描述

JVM整体结构

在这里插入图片描述

各结构中存放的数据

1、程序计数器(线程私有):指向当前线程正在执行的字节码指令。

2、虚拟机栈(线程私有):虚拟机栈是Java执行方法的内存模型。每个方法被执行的时候,都会创建一个栈帧,把栈帧压人栈,当方法正常返回或者抛出未捕获的异常时,栈帧就会出栈。
   栈帧:栈帧存储方法的相关信息,包含局部变量数表、返回值、操作数栈、动态链接
a、局部变量表:包含了方法执行过程中的所有变量。局部变量数组所需要的空间在编译期间完成分配,在方法运行期间不会改变局部变量数组的大小。
b、返回值:如果有返回值的话,压入调用者栈帧中的操作数栈中,并且把PC的值指向 方法调用指令 后面的一条指令地址。
c、操作数栈:操作变量的内存模型。操作数栈的最大深度在编译的时候已经确定(写入方法区code属性的max_stacks项中)。操作数栈的的元素可以是任意Java类型,包括longdouble32位数据占用栈空间为164位数据占用2。方法刚开始执行的时候,栈是空的,当方法执行过程中,各种字节码指令往栈中存取数据。
d、动态链接:每个栈帧都持有在运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。

3、本地方法栈(线程私有):调用本地native的内存模型

4、方法区(线程共享):用于存储已被虚拟机加载的【类信息、常量、静态变量】、即时编译后的代码等数据
还包含运行时常量池:
     A、是方法区的一部分
     B、存放编译期生成的各种字面量和符号引用
     CClass文件中除了存有类的版本、字段、方法、接口等描述信息,还有一项是常量池,存有这个类的 编译期生成的各种字面量(如字符串、常量)和符号引用(编译后字节码层面的符合),这部分内容将在类加载后,存放到方法区的运行时常量池中。
     符号引用主要包括了以下三类常量: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符

5、堆(Heap):Java对象存储的地方
(1Java堆是虚拟机管理的内存中最大的一块
(2Java堆是所有线程共享的区域
(3)在虚拟机启动时创建
(4)此内存区域的唯一目的就是存放对象实例,几乎所有对象实例都在这里分配内存。存放new生成的对象和数组
(5Java堆是垃圾收集器管理的内存区域,因此很多时候称为“GC堆”

如图中Math类加载后,Math类信息Math.classMath中的常量、静态变量放在方法区,Math对象放在堆中。
当调用Math中的方法时,会把方法放入栈中。

堆区

在这里插入图片描述

Java1.8 版本堆的内存划分如图所示,使用分代收集算法,分别为年轻代、Old Memory(老年代)。
从Jdk1.8中,堆中永久代被移除,使用元空间(MetaSpace)即方法区,直接使用物理内存。
1、年轻代:
(1)分为EdenSurvivor FromSurvivor To,比例默认为8112)年轻代每次GC都要回收大部分对象。新生代里面分成一份较大的Eden空间和两份较小的Survivor空间。
每次只使用Eden和其中一块Survivor空间,然后垃圾回收的时候,把存活对象复制到到未使用的Survivor(划分出from、to)空间中,
清空Eden和刚才使用过的Survivor空间。
(3)内存不足时发生Minor(young) GC
2、老年代:
(1)存放的是长期存活的对象,正常情况下当年轻代分代年龄大于15后(对象头分代年龄占4bit最多表示15),下次young GC就会把对象从年轻代移到到老年代。
     其他进入老年代的原因:如大对象直接进入老年代等看下放补充说明
3MetaSpace(元空间):元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。

JMM Java内存模型

Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段 和构成数组对象的元素)的访问方式。
JVM运行程序的实体是线程,而每个线程创建时 JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问, 但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自 己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作 主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。
在这里插入图片描述

1Java线程之间的通信由内存模型JMMJava Memory Model)控制。
(1JMM决定一个线程对变量的写入何时对另一个线程可见。
(2)线程之间共享变量存储在主内存中
(3)每个线程有一个私有的本地内存,里面存储了读/写共享变量的副本。
(4JMM通过控制每个线程的本地内存之间的交互,来为程序员提供内存可见性保证。
2、可见性、有序性:
(1)当一个共享变量在多个本地内存中有副本时,如果一个本地内存修改了该变量的副本,其他变量应该能够看到修改后的值,此为可见性。
(2)保证线程的有序执行,这个为有序性。(保证线程安全)

内存间交互操作

在这里插入图片描述

数据同步八大原子操作 
(1lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态 
(2unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,
释放后的变量才可以被其他线程锁定 
(3read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存 中,
以便随后的load动作使用 
(4load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中 
(5use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎 
(6assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内 存的变量
(7store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存 中,
以便随后的write的操作 
(8write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值 传送到主内存的变量中 
如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操 作,
如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但
Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

同步规则分
1)不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内 存中
2)一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化 (load或者assign)的变量。
即就是对一个变量实施use和store操作之前,必须先自行 assign和load操作。 
3)一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重 复执行多次,多次执行
lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock 和unlock必须成对出现。 
4)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个 变量之前需要重新执行
load或assign操作初始化变量的值。 
5)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作,
也不允许去unlock一个被其他线程锁定的变量。 
6)对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write 操作)

JMM解决原子性、可见性、有序性问题

【原子性问题 】
除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronizedLock实现原子性。因为
synchronizedLock能够保证任一时刻只有一个线程访问该代码块。
【可见性问题】
volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即 被其他的线程看到,
即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中 读取新值。
synchronizedLock也可以保证可见性,因为它们可以保证任一时刻只有一个 线程能访问共享资源,
并在其释放锁之前将修改的变量刷新到内存中。 
【有序性问题】
在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲 述volatile关键字)。
另外可以通过synchronizedLock来保证有序性,很显然, 
synchronizedLock保证每个时刻是有一个线程执行同步代码,
相当于是让线程顺序执行 同步代码,自然就保证了有序性。

指令重排序

java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果 与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的 重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处 理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的 发挥机器性能。 下图为从源码到最终执行的指令序列示意图:
在这里插入图片描述

JVM和CPU缓存结构

在这里插入图片描述
参考:https://segmentfault.com/a/1190000014395186
https://blog.csdn.net/witsmakemen/article/details/28600127/

补充对象直接进入老年代情况

大对象直接进入老年代

大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下 有效。比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一 个程序会发现大对象直接进了老年代
这样设置可以避免为大对象分配内存时的复制操作而降低效率。

长期存活的对象将进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在 老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度 (默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代 的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。

对象动态年龄判断

当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的 50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了, 例如Survivor区域里现在有一批对象,年龄1+年龄2+年龄n的多个年龄对象总和超过了Survivor区域的50%,此时就会 把年龄n(含)以上的对象都放入老年代。这个规则其实是希望那些可能是长期存活的对象,尽早进入老年代。对象动态年龄判断机制一般是在minor gc之后触发的。

老年代空间分配担保机制

年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间 如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。 如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾, 如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full
gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值