JVM-内存结构和JMM-内存模型

JAVA的内存模型:JVM中的堆、栈、方法区(方法区是JVM规范的概念定义,在HotSpot虚拟机中,1.7版本对方法区的实现是永久代,1.8版本对方法区的实现是元空间,元空间使用本地内存Native Memory 实现的,也就是他的内存不在虚拟机内,理论上受限于物理机的内存)、程序计数器等等是Java虚拟机的内存结构,Java程序启动后,会初始化这些内存数据。如下图

内存模型就是另外一个东西。什么是内存模型?

计算机中,cpu与内存交互最频繁,相比内存,磁盘读写太慢,内存相当于高速缓冲区,但是内存读写速度也远远赶不上cpu,因此cpu厂商在每颗cpu上加上高速缓冲,用于缓解这种情况,cpu和内存的交互大致是cpu<=>高速缓存(一般为L1、L2、L3)<=>内存,高速缓存解决了处理器和内存的矛盾(一快一慢),但是也带来缓存一致性的问题。在多核cpu中,每个核心都有自己的高速缓存,而主内存只有一个(CPU要读取一个数据时,首从L1-L2-L3-主内存 依次查找,每个cpu有且只有一套自己的缓存)如何保证多个处理器运算涉及到同一个内存区域时,多线程场景下会存在缓存一致性问题,那么运行时保证数据一致性?由CPU来保证——各个处理器需遵循一致性协议保证(如MSI,MESI)。

内存屏障(Memory Barrier):

CPU中的高速缓存,提高了数据访问性能,避免每次都向内存索取,但是不能实时的和内存发生信息交换,多CPU执行的不同线程对同一个变量的缓存值不同。靠内存屏障来保证,硬件层的内存屏障分为两种:        Load Barrier(读屏障)、Store Barrier(写屏障)。内存屏障是硬件层面的。由于不同硬件对内存屏障的实现方式不一样,java屏蔽了这些差异,通过jvm生成内存屏障指令,对于读内存屏障:在指令前插入读屏障,使高速缓存中的数据失效,强制从主内存取。

内存屏障的作用:1.阻止屏障两侧指令重排;2.强制把写缓冲区、高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。(volatile关键字使用了内存屏障,被volatile修饰的变量在进行写操作时,会生成一个特殊的汇编指令,该指令会触发mesi协议,会存在一个总线嗅探机制的东西,简单来说就是这个cpu会不停检测总线中该变量的变化,如果该变量一旦变化了,由于这个嗅探机制,其它cpu会立马将该变量的cpu缓存数据清空掉,重新的去从主内存拿到这个数据)

JAVA内存区域

        以上都是以HotSpot虚拟机为基础的,java内存模型的设定符合上述的计算机中的规范,Java程序内存的分配是在JVM虚拟机内存分配机制下完成。Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。简要言之,jmm是jvm的一种规范,定义了jvm的内存模型。它屏蔽了各种硬件和操作系统的访问差异,不像c那样直接访问硬件内存,相对安全很多,它的主要目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。可以保证并发编程场景中的原子性、可见性和有序性。

        JAVA数据区域分为五大数据区域—堆、本地方法栈、虚拟机栈、程序计数器、方法区

程序计数器:

        程序计数器是一块很小的内存空间,主要用来记录各个线程执行的字节码的地址,例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。由于 Java 是多线程语言,当执行的线程数量超过 CPU 核数时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令。如果是遇到本地方法(native 方法),这个方法不是 JVM 来具体执行,所以程序计数器不需要记录了,这个是因为在操作系统层面也有一个程序计数器,这个会记录本地代码的执行的地址,所以在执行 native 方法时,JVM 中程序计数器的值为空(Undefined)。另外程序计数器也是 JVM 中唯一不会 OOM(OutOfMemory)的内存区域。

JAVA虚拟机栈:

数据结构:先进后出(FILO)的数据结构

作用:在 JVM 运行过程中存储当前线程运行方法所需的数据,指令、返回地址 

基于线程:以线程的方式运行的。在线程的生命周期中,参与计算的数据会频繁地入栈和出栈,栈的生命周期是和线程一样的。虚拟机栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k。

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

  1.     栈帧:是用来存储数据和部分过程结果的数据结构。
  2.     栈帧的位置:内存——> 运行时数据区 ——>某个线程对应得虚拟机栈——>栈帧
  3.     栈帧的大小及确定时间:编译期确定,不受运行数据影响

平时说的栈一般指局部变量表部分。局部变量表是一片连续的内存空间,存放方法参数、方法内定义的局部变量、编译期间已知的数据类型(八大基本类型和引用类型)、返回地址。

需要注意的是,局部变量表所需要的内存空间在编译期完成分配,当进入一个方法时,这个方法在栈中需要分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表大小。Java虚拟机栈可能出现的两种类型的异常:

  1. StackOverflowError:线程请求的栈深度大于虚拟机允许的栈深度会抛出该异常
  2. OutOfMemoryError:虚拟机栈空间可以动态扩展,当动态扩展是无法申请到足够的空间时会抛出该异常

本地方法栈:

        本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的(比如 Object.hashcode 方法)。本地方法栈是和虚拟机栈非常相似的一个区域,它服务的对象是 native 方法。你甚至可以认为虚拟机栈和本地方法栈是同一个区域。虚拟机规范无强制规定,各版本虚拟机自由实现 ,HotSpot 直接把本地方法栈和虚拟机栈合二为一 。

堆:

        堆是 JVM 上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的。我们常说的垃圾回收,操作的对象就是堆。堆空间一般是程序启动时,就申请了,但是并不一定会全部使用。堆一般设置成可伸缩的。随着对象的频繁创建,堆空间占用的越来越多,就需要不定期的对不再使用的对象进行回收。这个在 Java 中,就叫作 GC(Garbage Collection)。那一个对象创建的时候,到底是在堆上分配,还是在栈上分配呢?这和两个方面有关:对象的类型和在 Java 类中存在的位置。Java 的对象可以分为基本数据类型和普通对象。对于普通对象来说,JVM 会首先在堆上创建对象,然后在其他地方使用的其实是它的引用。比如,把这个引用保存在虚拟机栈的局部变量表中。对于基本数据类型来说(byte、short、int、long、float、double、char),有两种情况。当你在方法体内声明了基本数据类型的对象,它就会在栈上直接分配。其他情况,都是在堆上分配。

堆大小参数:
-Xms:堆的最小值;
-Xmx:堆的最大值;
-Xmn:新生代的大小;
-XX:NewSize;新生代最小值;
-XX:MaxNewSize:新生代最大值;
例如- Xmx256m

方法区:

        方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。

运行时常量池 是方法区的一部分,class文件除了有类的字段、接口、方法等描述信息之外,还有常量池用于存放编译期间生成的各种字面量和符号引用。老版jdk,方法区也被称为永久代,因为没有强制要求方法区必须实现垃圾回收,HotSpot虚拟机以永久代来实现方法区,JVM的垃圾收集器可以像管理堆区一样管理这部分区域,从而不需要专门为这部分设计垃圾回收机制。从JDK7之后,Hotspot虚拟机便将运行时常量池从永久代移除了。jdk8真正开始废弃永久代,而使用元空间(Metaspace),java虚拟机对方法区比较宽松,除了跟堆一样可以不存在连续的内存空间,定义空间和可扩展空间,还可以选择不实现垃圾收集。

Java8 为什么使用元空间替代永久代,这样做有什么好处呢?
        官方给出的解释是:移除永久代是为了融合 HotSpot JVM 与 JRockit VM 而做出的努力,因为 JRockit 没有永久代,所以不需要配置永久代。永久代内存经常不够用或发生内存溢出,抛出异常java.lang.OutOfMemoryError: PermGen(永久代)。这是因为在 JDK1.7 版本中,指定的 PermGen 区大小为8M,由于 PermGen 中类的元数据信息在每次 FullGC 的时候都可能被收集,回收率都偏低,成绩很难令人满意;还有为 PermGen 分配多大的空间很难确定,PermSize 的大小依赖于很多因素,比如,JVM 加载的 class 总数、常量池的大小和方法的大小等。

JDK1.8常量池解析:

  1. 类常量池:诞生时间 :编译时; 所处区域:堆(类常量池存放在Class文件中,一个Class文件对应一个常量池) ;存储内容:符号引用和字面量
  2. 字符串常量池:诞生时间:编译时;所处区域:堆;存储内容:堆内的字符串对象的引用和字符串常量
  3. 运行时常量池:诞生时间:当类加载到内存中后;所处区域:本地内存(每个class都加载后常量池的数据被汇总到运行时常量池,运行时常量池存在元空间中);储存内容:class文件元信息描述,编译后的代码                           数据,引用类型数据(类经过解析后会把符号引用替换为直接引用,解析的过程会去查询字符串常量池,以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的)。

直接内存(堆外内存):

直接内存有一种更加科学的叫法,堆外内存。JVM 在运行时,会从操作系统申请大块的堆内存,进行数据的存储;同时还有虚拟机栈、本地方法栈和程序计数器,这块称之为栈区。操作系统剩余的内存也就是堆外内存。它不是虚拟机运行时数据区的一部分,也不是 java 虚拟机规范中定义的内存区域;如果使用了 NIO,这块区域会被频繁使用,在 java 堆内可以用directByteBuffer 对象直接引用并操作;这块内存不受 java 堆大小限制,但受本机总内存的限制,可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样),所以也会出现 OOM 异常。

对象的内存布局:

在HotSpot中,对象在内存存储布局分为:1.对象头;2.示例数据;3.对齐填充

//TODO

gc简介

GC(Garbage Collection):垃圾回收,主要用来回收、释放垃圾占用的空间,JAVA GC泛指Java的垃圾回收。

哪些区域的垃圾需要回收?什么时候回收?如何回收?

哪些内存需要回收? java内存模型中的5大区域已经了解了,程序计数器、虚拟机栈、本地方法栈,由线程而生,随线程而灭,其中栈中的栈帧随着方法的进入顺序的执行的入栈和出栈的操作,一个栈帧需要分配多少内存取决于具体的虚拟机实现并且在编译期间即确定下来【忽略JIT编译器做的优化,基本当成编译期间可知】,当方法或线程执行完毕后,内存就随着回收,因此无需关心。而Java堆、方法区则不一样。方法区存放着类加载信息,但是一个接口中多个实现类需要的内存可能不太一样,一个方法中多个分支需要的内存也可能不一样【只有在运行期间才可知道这个方法创建了哪些对象没需要多少内存】,这部分内存的分配和回收都是动态的,gc关注的也正是这部分的内存。

堆的回收区域?  

  1. 新生代(Young Generation)NewSize和MaxNewSize分别可以控制年轻代的初始大小和最大的大小
  2. 老年代(Old Generation)
  3. 永久代(Permanent Generation)【1.8以后采用元空间,就不在堆中了】

判断对象是否存活的算法?

1.引用计数算法
早期判断对象是否存活大多都是以这种算法,这种算法判断很简单,简单来说就是给对象添加一个引用计数器,每当对象被引用一次就加1,引用失效时就减1。当为0的时候就判断对象不会再被引用。
优点:实现简单效率高,被广泛使用与如python何游戏脚本语言上。
缺点:难以解决循环引用的问题,就是假如两个对象互相引用已经不会再被其它其它引用,导致一直不会为0就无法进行回收。

2.可达性分析算法
目前主流的商用语言[如java、c#]采用的是可达性分析算法判断对象是否存活。这个算法有效解决了循环利用的弊端。它的基本思路是通过一个称为“GC Roots”的对象为起始点,搜索所经过的路径称为引用链,当一个对象到GC Roots没有任何引用跟它连接则证明对象是不可用的。作为GC Roots 的对象有四种

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
  2. 方法区中的类静态属性引用的对象,一般指被static修饰引用的对象,加载类的时候就加载到内存中。
  3. 方法区中的常量引用的对象,
  4. 本地方法栈中JNI(native方法)引用的对象

垃圾回收算法

  • 标记/清除算法【最基础】
  • 复制算法
  • 标记/整理算法

jvm采用`分代收集算法`对不同区域采用不同的回收算法。

新生代采用复制算法:新生代中因为对象都是"朝生夕死的",98%死亡率,适用于复制算法【复制算法比较适合用于存活率低的内存区域】。它优化了标记/清除算法的效率和内存碎片问题,且JVM不以5:5分配内存【由于存活率低,不需要复制保留那么大的区域造成空间上的浪费,因此不需要按1:1【原有区域:保留空间】划分内存区域,而是将内存分为一块Eden空间和From Survivor、To Survivor【保留空间】,三者默认比例为8:1:1,优先使用Eden区,若Eden区满,则将对象复制到第二块内存区上。但是不能保证每次回收都只有不多于10%的对象存货,所以Survivor区不够的话,则会依赖老年代年存进行分配】。新生代的回收流程为:    在GC前 【To Survivor】 保持清空,对象保存在 Eden 和【From  Survivor】中,GC运行时,Eden中的幸存对象被复制到 【To Survivor】。针对【From  Survivor】中的幸存对象,会考虑对象年龄,如果年龄没达到阀值(tenuring threshold,默认15),对象会被复制到【To Survivor】。如果达到阀值对象被复制到老年代。复制阶段完成后,Eden 和【From  Survivor】中只保存死对象,可以视为清空。如果在复制过程中【To Survivor】被填满了,剩余的对象会被复制到老年代中。最后【From  Survivor】和 【To Survivor】会调换下名字,在下次GC时,【To Survivor】会成为【From  Survivor】。

老年代采用【标记清除】、【标记整理】由于老年代存活率高,没有额外空间给他做担保,必须使用这两种算法。

垃圾收集器:

如果说垃圾回收算法是内存回收的方法论,那么垃圾收集器就是具体实现。jvm会结合针对不同的场景及用户的配置使用不同的收集器。

  • 年轻代收集器:Serial 、ParNew 、 Parallel Scavenge
  • 老年代收集器:Serial Old 、Parallel Old 、CMS
  • 特殊收集器:G1收集器 (不在年轻、年老范畴内)

新生代收集器:

Serial:最基本、发展最久的收集器,在jdk3以前是gc收集器的唯一选择,Serial是单线程收集器,只能使用一条线程进行收集工作,在收集的时候必须得停掉其它线程,等待收集工作完成其它线程才可以继续工作。

ParNew: Serial升级版,支持多线程(GC线程),工作的时候与Serial一样Stop the word,HotSpot第一个真正意义实现并发的收集器,默认开启线程数与cpu数相同(Server模式下新生代首选收集器,因为新生代这几                 个收集器只有它和Serial能配合CMS一起使用)

Parallel Scavenge:采用复制算法,支持多线程,重点关心吞吐量(吞吐量=代码运行时间 / (代码运行时间+垃圾回收时间)),如代码运行99min,垃圾收集1min,则吞吐量=99% ,适合停顿时间短的场景

老年代收集器:

Serial Old :单线程,Serial的老年代版本,不过他采用标记-整理算法,也需要STW

Parallel Old :支持多线程、Parallel Scavenge的老年代版本,jdk1.6出现,标记-整理算法,老年代收集器大多采用这种算法

CMS:(Concurrent Mark Sweep)是一种获取最短回收停顿时间为目标的收集器(重视响应,被sun称为并发低停顿收集器),标记-清除算法,支持并发。其回收过程如下

  1. 初始标记:标记一下GC Roots能直接关联到的对象,单线程标记,速度快,STW
  2. 并发标记:GC Roots Tarcing过程,即可达性分析
  3. 重新标记:为了修正因并发标记期间用户程序运作而产生变动的那一部分对象的标记记录,会有些许停顿,时间上一般 初始标记 < 重新标记 < 并发标记,STW
  4. 并发清除

CMS量大问题:

  • 内存碎片严重,一旦老年代产生很多碎片,年轻代过来的对象找不到空间,就会使用Serial Old做标记整理
  • 无法处理浮动垃圾,浮动垃圾是CMS回收垃圾时,有新的垃圾产生,这时CMS无法处理这些垃圾,需要下次才能处理

G1 收集器:(garbage first——尽可能多的收集垃圾避免 Full GC),当前最前沿收集器之一,1.7后,关注低延时,替代cms功能,解决了cms产生空间碎片等一系列问题。(摘自甲骨文:适用于 Java HotSpot VM 的低暂停、服务器风格的分代式垃圾回收器。G1                         GC 使用并发和并行阶段实现其目标暂停时间,并保持良好的吞吐量。当 G1 GC 确定有必要进行垃圾回收时,它会先收集存活数据最少的区域(垃圾优先)g1的特别之处在于它强化了分区,弱化了分代的概念,是区域化、增量式的收集器,它不属于                         新生代也不属于老年代收集器。用到的算法为标记-清理、复制算法)

JDK1.7-1.8默认关闭,开启选项为 -XX:+UseG1GC

g1是区域化的,它将java堆内存划分为若干个大小相同的区域【region】,jvm可以设置每个region的大小(1-32m,大小得看堆内存大小,必须是2的幂),它会根据当前的堆内存分配合理的region大小。g1通过并发(并行)标记阶段查找老年代存活对象,通过并行复制压缩存活对象【这样可以省出连续空间供大对象使用】。g1将一组或多组区域中存活对象以增量并行的方式复制到不同区域进行压缩,从而减少堆碎片,目标是尽可能多回收堆空间【垃圾优先】,且尽可能不超出暂停目标以达到低延迟的目的。g1提供三种垃圾回收模式 young gc、mixed gc 和 full gc,不像其它的收集器,根据区域而不是分代,新生代老年代的对象它都能回收。

Minor GC:在年轻代(包括Eden 区和Suriver区)中的垃圾回收称之为Minor GC,只清理年轻代

Major GC:清理老年代(old GC),但是通常也可以指和Full GC是等价,因为收集老年代的时候往往也会伴随着升级年轻代,收集整个Java堆。

Full GC:是对新生代、老年代、永久代【jdk1.7】、元空间【jdk1.8】统一的回收。

mixed GC:G1特有,混合GC,收集整个 young gen 以及部分old gen 的GC。只有G1有这个模式

 

猜你喜欢

转载自blog.csdn.net/csdnbeyoung/article/details/113144174