JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池

JVM内存模型

对于 Java 程序员来说,在虚拟机自动内存管理机制下,不再需要像 C/C++ 程序开发程序员这样为每一个操作去写对应的 delete / free 操作,不容易出现内存泄漏和内存溢出问题。正是因为 Java 程序把内new存控制权利交给JVM虚拟机。一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会是一个非常艰巨的任务。

JVM 虚拟机在执行 java 程序的过程中,会把它管理的内存划分成若干个不同的区域,每个区域有各自的不同的用途、创建方式及管理方式。有些区域随着虚拟机的启动一直存在,有些区域则随着用户线程的启动和结束而建立和销毁,这些共同组成了 Java 虚拟机的运行时数据区域,也被称为 JVM 内存模型

运行时数据区域划分

JVM虚拟机在执行JAVA程序的过程中会把它管理的内存划分成若干个不同的数据区域,由方法区,堆区,虚拟机栈,本地方法栈,程序计数器五部分组成

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第1张图片

版本的差异

  1. JDK 1.8之前分为:线程共享(Heap堆区、Method Area 方法区)、线程私有(虚拟机栈、本地方法栈、程序计数器)
  2. JDK 1.8之后分为:线程共享(Heap堆区、MetaSpace员工间)、线程私有(虚拟机栈、本地方法栈、程序计数器)

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第2张图片

其中虚拟机栈、本地方法栈、程序计数器是线程私有的区域,所以随着线程消亡而结束。而线程共享的 Heap 堆区、MetaSpace 元空间会随着虚拟机的启动,一直存在。

程序计数器

Program Counter Register‘

程序计数器是一块较小的内存空间,是当前线程所执行的字节码的行号指示器

字节码解释器在解释执行字节码文件工作时,每当需要执行一条字节码指令时,就通过改变程序计数器的值来完成.程序中的分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
程序执行过程中,会不断的切换当前执行线程,切换后,为了能让当前线程恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,并且各线程之间计数器互不影响,独立存储

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第3张图片

程序计数器主要作用

  1. 字节码解释器通过改变程序计数器来一次读取命令,从而实现代码的流程控制,如:顺序执行,选择,循环。异常处理
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候,能够知道当前现成的运行位置,恢复当前线程的执行

程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它随着现成的创建而创建,随着线程的结束而死亡

Java虚拟机栈

VM Stack

虚拟机栈是线程执行Java程序时,处理Java方法中内容的区域,虚拟机栈也是线程私有的区域,每个Java方法被调用的时候,都会在虚拟机栈中创建一个栈顶,而每个栈帧又由局部变量,操作数栈,动态链接和方法返回四部分组成,有些虚拟机的栈帧还包括了一些附加的信息

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第4张图片

​ JMM内存区域可以粗略的氛围堆内存(Heqp)和栈内存(Stack)其中栈就是VM Stack虚拟机栈,或者说是虚拟机栈中局部变量表部分

​ 局部变量表主要存放了编译器可知的各种基本数据类型变量值(boolean、byte、char、short、int、float、long、double),对象应用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第5张图片

虚拟机栈运行原理

​ 每一次方法调用都会有一个对应的栈帧被压入VM Stack 虚拟机栈,每一个方法调用结束后,代表该方法的栈帧会从VM Stack虚拟机栈中弹出

​ 虚拟机栈是内存的私有区域,并且栈帧不允许被其他线程访问,不存在线程安全问题,栈帧弹出后内存就会被系统回收,所以也不存在垃圾回收问题

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第6张图片

​ 在活动线程中,只有位于栈顶的栈才是有效的,成为当前活动栈帧,代表正在执行的当前方法

​ 在JVM执行引擎运行时,所有指令都只能对当前活动栈帧进行操作。虚拟机栈通过pop和push的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第7张图片

活动栈帧被弹出的方式:

java方法有两种返回方式,不管哪种返回方式都会导致当前活动栈帧被弹出

  • return 语句
  • 抛出异常

虚拟机栈可能产生的错误

java虚拟机栈会出现两种错误:StackOverFlowError和OutOfMemoryError

  1. StackOverFlowError:当线程请求栈的深度超过JVM虚拟机栈的最大深度的时候,就抛出StackOverFlowError错误
  2. OutOfMemoryError:JVM的内存大小可以动态扩展,如果虚拟机的动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

虚拟机栈的大小

虚拟机栈的大小可以通过-Xss参数设置,默认单位是byte,也可以使用k,m,g作为单位(不区分大小写)。例如:-Xss 1m

在不同的操作系统下-Xss的默认值不同

  • Linux:1024K
  • MacOs:1024K
  • Windows:默认值依赖于虚拟机的内存

本地方法栈

Native Method Stack

native关键字修饰的本地方法被执行的时候,在本地方法栈中会创建一个栈帧,用于存放该native本地方法的局部变量表、操作数栈、动态链接、方法出口信息。方法执行完毕后,相应的栈帧也会出栈且释放内存空间。也会出现StackOverFlowErrorOutOfMemoryError两种错误

★★堆(Heap)★★

​ Heap 堆区,用于存放对象实例和数组的内存区域

​ Heap 堆区,是 JVM 所管理的内存中最大的一块区域,被所有线程共享的一块内存区域。堆区中存放对象实例,“几乎”所有的对象实例以及数组都在这里分配内存。


每一个 JVM 进程只存在一个堆区,它在 JVM 启动时被创建, JVM 规范中规定堆区可以是物理上不连续的内存,但必须是逻辑上连续的内存。每一个 JVM 进程只存在一个堆区,它在 JVM 启动时被创建, JVM 规范中规定堆区可以是物理上不连续的内存,但必须是逻辑上连续的内存。

  1. 每一个 JVM 进程只存在一个堆区,它在 JVM 启动时被创建, JVM 规范中规定堆区可以是物理上
    不连续的内存,但必须是逻辑上连续的内存。

  2. JVM 规范中描述,所有的对象实例及数组都应该在运行时分配在堆上。而他们的引用会被保存在
    虚拟机栈中,当方法结束,这些实例不会被立即清除,而是等待 GC 垃圾回收。

  3. 由于堆占用内存大,所以是 GC 垃圾回收的重点区域,因此堆区也被称作 GC堆

    (Garbage Collected Heap)


对象逃逸分析

​ Java 世界中“几乎”所有的对象都在堆中分配,但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了

​ 从 JDK 1.7 开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用 (也就是未逃逸出去),那么对象可以直接在栈上分配内存

堆的组成:新生代+老年代

​ 从垃圾回收的角度,由于现在收集器基本都采用粉黛垃圾收集算法,所以JVM中的堆区往往进行粉黛划分,例如新生代和老年代目的是为了更好地回收内存,或者更快地分配内存


​ 堆区的组成分为新生代(Young Generation)老年代(Old Generation)。

  • 新生代被分为伊甸区(Eden)和幸存者区(from + to),幸存区又被分为

    Survivor 0(from)和Survivor 1(to)

  • 新生代和老年代的比例为1:2,伊甸区和S0、S1比例为8:1:1,不用区域存放对象的用途和方式不同

    1. 伊甸区(Eden):存放大部分新创建对象
    2. 幸存区(Survivor):存放Minor GC 之后,Eden区和幸存区(Survivor)本身没有被回收的对象
    3. 老年代:存放Minor GC之后且年龄计数器达到15(此信息存在与对象头中)依然存活的对象,Major GC和Full GC之后仍然存活的对象

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第8张图片

对空间大小设置

堆区的内存大小是可以修改的,默认轻快下,初始堆内为物理内存的1/64,最大的物理内存的1/4

  • -Xms:设置初始化堆内存,例如:-Xms64m
  • -Xmx:设置最大堆内存,例如:-Xmx64m
  • -Xmn:设置年轻代内存,例如-Xmx32m

Heap堆区中的新生代、老年代的空间分配比例,可以通过java -XX:+PrintFlagsFinal -version命令查看

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第9张图片

  • uintx InitialSurvivorRatio = 8

    新生代Young(Eden/Survivor)空间的初始比例 = 8:代表Eden占新生代空间的80%

  • uintx NewRatio = 2

    老年代Old/新生代Young的空间比例 = 2:代表老年代Old是新生代Young的2倍

因为新生代是由Eden+s0+s1组成的,所以按照上述默认比例,如果Enen区内存大小是40M,那么两个Survivor区就是5M,整个新生代区就是50M,然后可以算出老年代Old区内存大小是100M,堆区总大小就是150M

创建对象的内存分配★★★★★

创建一个新对象,在堆内的分配内存

大部分情况下,对象会在Eden区生成,当Eden区装填满的时候,会触发Young Garbage Collection,即YGC垃圾回收的时候

依然存活的对象会被移送到Survivor区。Survivor区分为S0和S1两块内存区域。每次YGC的时候,它们将存活的对象复制到未使用的Survivor空间(S0或S1),然后将当前正在使用的空间完全清楚,交换两块空间的使用状态。每次交换时,对象的年龄就会+1

如果YGC要以送的对象大于Survivor区容量的上线,则直接移交给老年代。一个对象也不可能永远呆在新生代,在JVM中一个对象从新生代晋升到老年代的阈值默认值是15,可以在Survivor区交换14次后,晋升至老年代

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第10张图片

堆区的分代垃圾收集思想

出于效率的缘故,JVM 的垃圾收集不会对三个区域(伊甸区、幸存区、老年代)进行收集,大部分时候都是回收新生代, HotSpot 虚拟机将垃圾收集分为部分收集( Partial GC )和整堆收集( FulI GC)

部分收集:

  1. 新生代收集YGC(Minor GC/Young GC):回收新生代区域,频率比较高,因为大部分对的存活寿命比较短,在新生代里被回收性能耗费较小。例如:Serial、ParNew、Parallel Scavenge等垃圾收集器都是新生代收集
  2. 老年代收集Old GC:回收老年代区域,例如:Serial Old、CMS、Parallel Old等垃圾回收器都是老年代收集
  3. 混合收集(Mixed GC):收集整个年轻代区域及部分老年代区域,目前只有G1收集器有

**整机收集FGC(Full GC):**回收整个Java堆区,默认堆空间使用带到80%(可调整)的时候会触发FGC。频率根据访问量的多少决定,可能十天也可能一周左右一次(整机收集的频率越少越好)

GC组合垃圾回收只有YGC和FullGC、OldGC不可以单执行。原因是OldGC是STW机制+标记整理算法,相对耗时,只能在关键时刻使用,因此只有FullGC才能出发OldGC


GC垃圾回收的影响

GC耗时太长、GC次数太多会影响进程的性能、导致进程相应变慢、或者无法响应

  • YGC耗时:耗时在几十或者几百毫秒属于正常情况,用户几乎无感知,对程序影响比较少、耗时太长或者频繁、会导致服务器超时问题
  • YGC次数:太频繁、会降低服务的整机性能、高并发服务时、影响会比较大
  • FGC次数:越少越好。比较正常的情况几小时一次,或者几天一次
  • FGC耗时:耗时很长会导致线程频繁被停止,使应用响应变慢,比较卡顿

产生FGC的原因:

  1. 大对象: 系统一次性加载了过多数据到内存中,导致大对象进入了老年代
  2. 内存泄漏: 频繁创建了大量对象,但是无法被回收 (比如 流对象使用完后未调用 lose 方法释放资源) ,先引发 FGC ,最后导致 OOM 。
  3. 程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发 FGC
  4. 程序 BUG 导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发 FGC ,最后导致 OOM
  5. JVM 参数设置不合理: 包括总内存大小、新生代和老年代的大小、 Eden 区和 Survivor 区的
    大小、元空间大小、垃圾回收算法等等

堆区产生的错误

堆区最容易出现的就是OutOfMemoryError错误,这种错误的表现形式有以下两种:

  1. OutOfMemoryError:GC Overhead Limit Exceeded:当JVM花太多时间执行垃圾回收、并且只能回收很少的堆空间时、就会发生此错误
  2. OutOfMemoryError:Java heap space:如果在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误

此种情况,与配置的最大堆内存有关,且受限制于物理内存大小

元空间(MetaSpace)

用于存放类信息、常量、静态常量、JIT即时编译器编译后的机器代码等数据等/例如java.lang.Object类的原喜喜、Integer.MAX_VALUE常量等

JDK1.6:

HotSpot JVM使用Method Area方法去存储,也叫永久代(Permanent Generation)
1.方法去和"永久代(Permanent Generation)"的区别:方法去是JVM的规范。而永久代是JVM规范的一种实现,并且只有HotSpot JVM才有永久代,而对于其他类型的虚拟机,如JRockit(ORacle)、J9(IBM)并没有
2.方法区是一片连续的堆空间,当JVM加载的类信息容量超过了最大可分配空间,虚拟机会抛出OutOfMemoryError:PermGenSpace的Error
3.永久代的GC是和老年代(old generation)捆绑在一起,无论谁满了,都会出发永久代和老年代的垃圾收集
4.可以通过 -XX:PermSize=N 设置方法区(永久代)初始化空间,-XX:MaxPermSize=N 设置方法区(永久代)最大空间,超过这个值将会抛出错误:java.lang.OutOfMemoryError:PermGen

JDK1.7:

1.7是一个过度版本
将字符串常量池、静态变量转移到了堆区

JDK1.8:正式移除永久代,采用Meta Space元空间代替

元空间的本质和永久代类似,都是对 JVM 规范中方法区的一种具体实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过运行参数来指定元空间的大小。
	java 8 中PermGen永久代被移出HotSpot JVM的原因
1.由于PermGen内存经常会移除,容易抛出java,lang.OutOfMemoryError: PermGen错误
2.移除PermGen可以促进HotSpot JVM 与 JRockit VM 的融合,因为JRockit没有永久代

​ 上述运行结果可以看出,相同的代码,在JDK1.6会出现PermGen Space的永久代内存移除,而从JDK1,7和JDK1.8会出现Java heap space 堆内存移除,并且JDK1.8中PermSize和MaxPermGen参数已经无效。因此,在JDK1.7和JDK1.8中,已经将字符串常量由永久代转移到堆中,并且JDK1.8已经完全移除了永久代,采用元空间来替代

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第11张图片

1.-XX:MetaspaceSize参数:主要控制Meta Space GC发生的初始阈值,也就是最小阈值,当使用的Meta Space空间到达MetaspaceSize的时候,就会触发Metaspace的GC
2.-XX:MaxMetaspaceSize参数:最大空间,默认是没有限制的。在jvm启动的时候,并不会分配MaxMetaspaceSize这么大的一块内存出来,metaspace是可以一直扩容的,知道到达MaxMetasoaceSize

JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第12张图片

字符串常量池

  • String的两种创建方式:

    1. 第一种方式是在常量池中获取字符串对象
    2. 第二种方式是直接在对内存空间创建一个新的字符串对象
    // 先检查字符串常量池中有没有"abc",如果字符串常量池中没有,则创建一个,然后str1指向字符串常量池中的对象,如果有,则直接将str1指向"abc"
    String str1 = "abc";
    String str2 = new String("abc");   //堆中创建一个新的对象
    String str3 = new String("abc");   //堆中创建一个新的对象
    
    System.out.printf(str1 == str2);	//false
    System.out.printf(str2 == str3);	//false
    
  • String的intern()方法:

    检查指定字符串在常量池中是否存在?如果存在,则返回地址,如果不存在,则在常量池中创建

    String s1 = new String("abc");
    String s2 = s1.intern(); //查看字符串常量池中是否存在"abc",如果存在则返回地址,如果不存在则在常量池中创建
    string s3 = "abc";  // 使用常量池中的已有的字符串常量"abc"
    
    System.out.printf(str2 == str3);	//true
    
  • String的拼接

    String s1 = "str";
    String s2 = "ing";
    
    String s3 = "str"+"ing";  // 常量池中的新字符串对象
    String s4 = str1 + str2;  // 在堆中创建的新字符串对象
    String s5 = "string";		  // 常量池中的已有字符串对象
    
    System.out.printf(str3 == str4);	//false
    System.out.printf(str3 == str5);	//true
    System.out.printf(str4 == str5);	//false
    

    JAVA进阶之路JVM-3:JVM内存模型,运行时数据区域划分,程序计数器,虚拟机栈,本地方法栈,堆,元空间,字符串常量池_第13张图片

  • String s1 = new String("abc");这句代码创建了几个字符串对象?

    创建1或2个字符串,如果常量池中已存在字符串常量"abc",则只会在堆空间创建一个字符串常量"abc"。如果常量池中没有字符串常量"abc",那么它将首先在池中创建,然后再堆空间中创建,因此将创建总共2个字符串对象
    

你可能感兴趣的:(JVM,java,jvm,开发语言)