JVM 内存模型

JVM 内存模型

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

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

运行时数据区域划分

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

版本的差异:

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

JDK 1.8 以后 分为:线程共享 (Heap 堆区、MetaSpace 元空间)、线程私有(虚拟机栈、本地方法栈、程序计数器)

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

程序计数器(Program Counter Regist ...

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

字节码解释器在解释执行字节码文件工作时,每当需要执行一条字节码指令时,就通过改变程序计数器的值来完成。程序中的分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

程序执行过程中,会不断的切换当前执行线程,切换后,为了能让当前线程恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,并且各线程之间计数器互不影响,独立存储。

Java虚拟机栈(VM Stack)

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

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

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

虚拟机栈运行原理

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

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

在活动线程中,只有位于栈顶的帧才是有效的,称为当前活动栈帧,代表正

在执行的当前方法。

在 JVM 执行引擎运行时,所有指令都只能针对当前活动栈帧进行操作。虚拟

机栈通过 pop和 push 的方式,对每个方法对应的活动栈帧进行运算处理,方

法正常执行结束,肯定会跳转到另一个栈帧上。

活动栈帧被弹出的方式:

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

return 语句

抛出异常

虚拟机栈可能产生的错误:

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

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

虚拟机栈的大小

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

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

Linux : 1024k

MacOs : 1024k

Windows:默认值依赖于虚拟机的内存

本地方法栈(Native Method Stack)

本地方法栈用于虚拟机调用的 Native 方法

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

堆(Heap)

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

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

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

  1. 堆区是线程共享共享的区域,同时也是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 之后仍然存活的对象。

堆空间的大小设置

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

  • -Xms : 设置初始堆内存,例如: -Xms64m
    • Xmx : 设置最大堆内存,例如: -Xmx64m
  • -Xmn : 设置新生代内存,例如: -Xmn32m

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

上述输出结果结果分析

  • InitialSurvivorRatio = 8

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

  • uintx NewRatio = 2

老年代 0ld/新生代 Young 的空间比例=2:代表老年代 01d 是新生代 Young的2倍

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

创建对象的内存分配

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

大部分情况下,对象会在 Eden 区生成,当 Eden 区装填满的时候,会触发 Young Garbage Collection, 即 YGC 垃圾回收的时候,在 Eden 区实现清除策略,没有被引用的对象则直接回收。

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

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

堆区的分代垃圾收集思想

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

  1. 新生代收集 YGC(Minor GC/Young GC):回收新生代区域,频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。例如: Serial、ParNew、ParallelScavenge 等 垃圾收集器都是新生代收集;

  2. 老年代收集 O1d GC(Major GC/Old GC):回收老年代区域,例如: Serial 0ld 、 CMS、 Parallel Old 等垃圾收集器都老年代收集;

  3. 混合收集(Mixed GC):收集整个年轻代区域及部分老年代区域,目前只有G1 收集器有

整堆收集 FGC(Full GC):

回收整个 Java 堆区,默认堆空间使用到达 80%(可调整)的时候会触发 FGC。以我们官网的生产环境为例,一般比较少会触发 FGC,有时 10 天或一周左右会有一次。

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

GC垃圾回收的影响

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

无法响应。

· YGC 耗时:耗时在几十或者几百毫秒属于正常情况,用户几乎无感知,对程序影响比较少。耗时太长或者频繁,会导致服务器超时问题。

YGC 次数:太频繁,会降低服务的整体性能。高并发服务时,影响会比较大。

FGC 次数:越少越好。比较正常情况几个小时一次、或者几天才一次。

FGC 耗时:耗时很长会导致线程频繁被停止,使应用响应变慢,比较卡顿。

产生FGC的原因:

  1. 大对象:系统一次性加载了过多数据到内存中,导致大对象进入了老年代。

  2. 内存泄漏:频繁创建了大量对象,但是无法被回收(比如 IO流对象使用完后未调用 close 方法释放资源),先引发 FGC,最后导致 O0M。

3.程序频繁生成一些长生命周期的对象,当这些对象的存活年龄超过分代年龄时便会进入老年代,最后引发 FGC。

4.程序 BUG 导致动态生成了很多新类,使得 Metaspace 不断被占用,先引发 FGC,最后导致 O0M。

  1. JVM 参数设置不合理:包括总内存大小、新生代和老年代的大小、Eden区和 Survivor 区的大小、元空间大小、垃圾回收算法等等。

堆区产生的错误

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

  1. OutOfMemoryError: GC Overhead Limit Exceeded :当 JVM 花太多时间执行垃圾回收,并且只能回收很少的堆空间时,就会发生此错误。

  2. OutOfMemoryError: Java heap space:假如在创建新的对象时,堆内存中的空间不足以存放新创建的对象,就会引发此错误。

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

元空间(Meta Space)

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

JDK1.6:

HotSpot JVM 使用 Method Area 方法区存储,也叫永久代(Permanent Generation).

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

JDK1.7:

将字符串常量池、静态变量转移到了堆区。

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

元空间的本质和永久代类似,都是对 JVM规范中方法区的一种具体实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过运行参数来指定元空间的大小。

Java 8 中 PermGen 永久代为什么被移出 HotSpot JVM?

  • 由于 PermGen 内存经常会溢出,容易抛出 java.lang.OutOfMemoryError:PermGen 错误;
  • 移除 PermGen 可以促进 HotSpot JVM 与 JRockit VM 的融合,因为 JRockit 没有永久代;

字符串常量池

String 的两种创建方式:

第一种方式是在常量池中获取字符串对象;

第二种方式是直接在堆内存空间创建一个新的字符串对象;

java 复制代码
//先检查字符串常量池中有没有"abcd",如果字符串常量池中没有,则创建一个,然后str1 指向字符串常量池中的对象,如果有,则直接将 str1 指向"abcd"

String str1 = "apesource";
String str2= new String("apesource");//堆中创建一个新的对象
String str3= new String("apesource");//堆中创建一个新的对象

System.out.println(str1 == str2); //false
System.out.println(str2 == str3); //false

String 的 intern()方法:

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

java 复制代码
String s1 = new String("Apesource");
String s2= s1.intern();//查看字符串常量池中是否存在"Apesource",
                      //如果存在则返回地址,如果不存在,则在常量池中创建
String s3="Apesource";//使用常量池中的已有字符串常量"Apesource"

System.out.println(s2 == s3);// true,地址相同

String 的拼接:

java 复制代码
String str1 = "str";
String str2 = "ing";

String str3="str"+"ing";//常量池中的新字符串对象
String str4=str1+ str2;//在堆中创建的新字符串对象
String str5="string";//常量池中的已有字符串对象

System.out.println(str3 == str4); //false
System.out.println(str3 == str5); //true
System.out.println(str4 == str5); //false

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

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

相关推荐
Swift社区2 小时前
在 Swift 中实现字符串分割问题:以字典中的单词构造句子
开发语言·ios·swift
没头脑的ht2 小时前
Swift内存访问冲突
开发语言·ios·swift
没头脑的ht2 小时前
Swift闭包的本质
开发语言·ios·swift
wjs20242 小时前
Swift 数组
开发语言
吾日三省吾码3 小时前
JVM 性能调优
java
stm 学习ing3 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
湫ccc4 小时前
《Python基础》之字符串格式化输出
开发语言·python
弗拉唐4 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi775 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器