JVM内存架构

概要

众所周知,Java是基于jvm来运行的,jvm是Java运行的核心,了解JVM能使我们更好的了解Java语言,更好的解决线上排查问题。

JVM(Java虚拟机)是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

一、Java语言的运行流程

在这里插入图片描述
如图所示,Java文件通过编译转为字节码文件,JVM通过类加载器把字节码文件加载进JVM虚拟机,虚拟机调动底层系统资源执行Java程序。有了JVM这个抽象层以后,Java就可以实现跨平台运行了。

应用程序、JVM、操作系统的关系

应用程序、JVM、操作系统的关系
在这里插入图片描述

JVM架构

在这里插入图片描述
如上图所示,总结一下,JVM虚拟机共分为5大模块,分别为 类加载子系统、运行时数据区、执行引擎、本地方法接口、垃圾收集模块,我们按照顺序一一分析他们的作用和结构。

二、JVM内存架构

JVM内存,是Java代码的运行区域,共分为程序计数器、虚拟机栈、本地方法栈、堆、方法区五个部分。
在这里插入图片描述
不通版本虚拟机的内存结构是不同的,Java7和Java8的不同主要体现在方法区的实现上。

方法区是java虚拟机规范中定义的一种概念上的区域,不同的厂商可以对虚拟机进行不同的实现。
我们通常使用的Java SE都是由Sun JDK和OpenJDK所提供,这也是应用最广泛的版本。而该版本使用的VM就是HotSpot VM。通常情况下,我们所讲的java虚拟机指的就是HotSpot的版本。

JDK7和JDK8变化

在这里插入图片描述
对于Java8,hotSpots取消了永久代,那是不是证明没有方法区了呢?
当然不是,方法区不是一块区域,而是一个规范,只不过它的实现方式变了。
在Java8中,元空间(Metaspace)登上舞台,方法区存在于元空间(Metaspace)。同时,元空间不再与堆连续,而且是存在于本地内存(Native memory)中。

方法区Java8之后的变化

  • 移除了永久代(PermGen),替换为元空间(Metaspace)
  • 永久代中的类元信息(class metadata)被移入了本地内存(native memory)中
  • 永久代中的字符串常量池(interned string)和类静态变量(class static variiables)被转移到了Java堆(Java heap)中
  • 永久代参数->变成了元空间的参数

为什么要把永久代替换为元空间?

  • 字符串存在永久代中,容易出现性能问题和内存溢出
  • 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太容易出现内存溢出。
  • 永久代会为GC带来不必要的复杂度,并且回收效率较低

1、程序计数器

程序计数器:也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器(指示当前线程已经执行到了第几行),在虚拟机的概念模型里, 字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

PC寄存器的特点

  • 和计算机硬件的寄存器功能大体相似,但略有不同。
  • 当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
  • 程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
  • 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。
因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

2、虚拟机栈

虚拟机栈(Java Virtual Machine Stacks):线程私有的,生命周期和线程相同。虚拟机栈和线程同时创建,用于存储栈帧,每个方法在创建时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

虚拟机栈的大小设置
-Xss为每个线程启动的虚拟机栈分配大小

-Xss1m
-Xss1024k
-Xss1048576
public class TestVirtual {
    static long count = 0;
    public static void main(String[] args) {
        //测试虚拟机栈的大小
        count++;
        System.out.println(count);
        main(args);
    }
}

这段代码会不断地往虚拟机栈中添加栈帧,直到由于应用程序递归太深而发生堆栈溢出。

2.1、本地变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。

2.2、操作数栈
操作数栈也称为操作栈,是一个后入先出的栈,随着方法的执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量到操作数栈,再随着计算的进行将栈中的元素栈出到局部变量表或返回给方法调用者,也就是入栈/出栈操作。

2.3、动态链接
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。

2.4、方法出口(方法返回地址)
方法返回地址存放调用该方法的PC寄存器的值,一个方法的结束有两种方式:1、正常执行完成。2、出现异常而退出。无论哪种方式结束,都会回到该方法被调用的位置。方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。

3、本地方法栈

本地方法栈(Native Method Stacks) 与虚拟机栈所发挥的作用是非常相似的, 其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码) 服务, 而本地方法栈则是为虚拟机使用到的本地(Native) 方法服务。

特点
1、本地方法栈加载native的方法, native类方法存在的意义当然是填补java代码不方便实现的缺陷而提出的。
2、虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
3、是线程私有的,它的生命周期与线程相同,每个线程都有一个。

在Java虚拟机规范中,对本地方法栈这块区域,与Java虚拟机栈一样,规定了两种类型的异常:
(1)StackOverFlowError :线程请求的栈深度>所允许的深度。
(2)OutOfMemoryError:本地方法栈扩展时无法申请到足够的内存。

4、堆

对于Java应用程序来说, Java堆(Java Heap) 是虚拟机所管理的内存中最大的一块。 Java堆是被所有线程共享的一块内存区域, 在虚拟机启动时创建。 此内存区域的唯一目的就是存放对象实例, Java 世界里“几乎”所有的对象实例都在这里分配内存。

堆的特点

1、是Java虚拟机管理的内存中最大的一块
2、是jvm中所有线程共享的。 堆中也包含私有的线程缓冲区(Thread Local Allocation Buffer (TLAB))
3、在虚拟机启动的时候创建
4、唯一的目的就是存放对象实例,几乎所有的对象实例以及数组都是在这里分配内存。
5、堆是垃圾收集器管理的主要区域
6、因此很多时候java堆也被称为“GC堆”(Garbage Collected Heap)。从内存回收的角度来看,由于现在收集器基本都采用分代收集算法,所以Java堆还可以细分为:新生代和老年代;新生代又可以分为:Eden 空间、FromSurvivor空间、To Survivor空间。
7、java堆是计算机物理存储上不连续的、逻辑上是连续的,也是大小可调节的(通过-Xms和-Xmx控制)。
8、方法结束后,堆中对象不会马上移出仅仅在垃圾回收的时候时候才移除。
9、如果在堆中没有内存完成实例的分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常

堆的空间大小

-Xms,-Xmx来控制堆的空间大小(测试详见代码)

例:-Xms5m -Xmx128m

堆的分类

Java8以后得堆空间分类:
在这里插入图片描述
年轻代和老年代
1)年轻代(Young Gen):年轻代主要存放新创建的对象,内存大小相对会比较小,垃圾回收会比较频繁。年轻代分成1个Eden Space和2个Suvivor Space(from 和to)。
2)年老代(Tenured Gen):年老代主要存放JVM认为生命周期比较长的对象(经过几次的Young Gen的垃圾回收后仍然存在),内存大小相对会比较大,垃圾回收也相对没有那么频繁。

新生代和老年代堆结构占比

  • 默认 -XX:NewRatio=2 , 标识新生代占1 , 老年代占2 ,新生代占整个堆的1/3
  • 修改占比 -XX:NewPatio=4 , 标识新生代占1 , 老年代占4 , 新生代占整个堆的1/5
  • Eden空间和另外两个Survivor空间占比分别为8:1:1
  • 可以通过操作选项 -XX:SurvivorRatio 调整这个空间比例。 比如 -XX:SurvivorRatio=8
  • 几乎所有的java对象都在Eden区创建, 但80%的对象生命周期都很短,创建出来就会被销毁
  • JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

在这里插入图片描述
对象的分配过程

JVM设计者不仅需要考虑到内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,因此还需要考虑GC执行完内存回收后是否存在空间中间产生内存碎片。

分配过程

  1. new的对象先放在伊甸园区。该区域有大小限制
  2. 当伊甸园区域填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园预期进行垃圾回收(Minor GC),将伊甸园区域中不再被其他对象引用的额对象进行销毁,再加载新的对象放到伊甸园区
  3. 然后将伊甸园区中的剩余对象移动到幸存者0区
  4. 如果再次触发垃圾回收,此时上次幸存下来的放在幸存者0区的,如果没有回收,就会放到幸存者1区
  5. 如果再次经历垃圾回收,此时会重新返回幸存者0区,接着再去幸存者1区。
  6. 如果累计次数到达默认的15次,这会进入养老区。 可以通过设置参数,调整阈值 -XX:MaxTenuringThreshold=N
  7. 养老区内存不足是,会再次出发GC:Major GC 进行养老区的内存清理
  8. 如果养老区执行了Major GC后仍然没有办法进行对象的保存,就会报OOM异常

在这里插入图片描述
堆GC
Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:一种是部分收集器(Partial GC)另一类是整堆收集器(Fu’ll GC)

部分收集器又分为:

  • 新生代收集器(Minor GC/Yong GC):只做新生代的垃圾收集
  • 老年代收集器(Major GC/Old GC):只做老年代的垃圾收集
  • 混合收集器(Mixed GC):收集整个新生代及老年代的垃圾收集 (G1 GC会混合回收, region区域回收)

整堆收集(Full GC):收集整个java堆和方法区的垃圾收集器

触发条件

  • 年轻代空间不足,就会触发Minor GC, 这里年轻代指的是Eden代满,Survivor不满不会引发GC
  • Minor GC会引发STW(stop the world) ,暂停其他用户的线程,等垃圾回收接收,用户的线程才恢复.
  • 老年代空间不足时,会尝试触发MinorGC. 如果空间还是不足,则触发Major GC
  • 如果Major GC , 内存仍然不足,则报错OOM
  • Major GC的速度比Minor GC慢10倍以上.

堆内存溢出 (重点!!)

public class TestHeapOutOfMemory {
    public static void main(String[] args) {
        // 无限循环,申请内存
        // 内存溢出
        while (true) {
            List<Test> tests = new ArrayList<>();
            tests.add(new Test());
        }
    }
}

常见原因:

  • 内存中加载的数据过多,如一次从数据库中取出过多数据;
  • 集合对对象引用过多且使用完后没有清空;
  • 代码中存在死循环或循环产生过多重复对象;
  • 堆内存分配不合理

5、元空间

在这里插入图片描述
而从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。 HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在,只不过取代永久代的是元空间(Metaspace)而已。

存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存
存储内容不同:在原来的永久代上,存储的是类的元信息、常量池、静态变量等。现在类的元信息存储在元空间静态变量、常量池等存储在堆中,相当于原来永久代的数据,被元空间和堆给瓜分了。

为什么要废除永久代,设立元空间?

  • 在原来的永久代划分中,永久代需要存放类的元数据、静态变量和常量等。它的大小不容易确定,因为这其中有很多影响因素,比如类的总数,常量池的大小和方法数量等,-XX:MaxPermSize 指定太小很容易造成永久代内存溢出。
  • 永久代会为GC带来不必要的复杂度,并且回收效率偏低。

废除永久代的好处

  • 由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间。不会遇到永久代存在时的内存溢出错误。
  • 将运行时常量池从PermGen分离出来,与类的元数据分开,提升类元数据的独立性。
  • 将元数据从PermGen剥离出来到Metaspace,可以提升对元数据的管理同时提升GC效率。

6、方法区

方法区是一种概念上的区域,实际上并没有方法区这样一个专门的区域,它用于存储已被虚拟机加载的类信息、常量池、静态变量、即时编译器编译后的代码缓存等数据。

元空间、永久代是方法区具体的落地实现。方法区看作是一块独立Java堆的内存空间,它主要是用来存储所加载的类信息的

创建对象,各区域的声明:

在这里插入图片描述
方法区的特点:

  • 方法区与堆一样是各个线程共享的内存区域
  • 方法区在JVM启动的时候就会被创建并且它实例的物理内存空间和Java堆一样都可以不连续
  • 方法区的大小跟堆空间一样 可以选择固定大小或者动态变化
    方法区的对象决定了系统可以保存多少个类,如果系统定义了太多的类导致方法区溢出虚拟机同样会跑出(OOM)异常(Java7之前是 PermGen Space (永久带) Java 8之后 是MetaSpace(元空间) )
  • 关闭JVM就会释放这个区域的内存

方法区的内部结构

类加载器将Class文件加载到内存之后,将类的信息存储到方法区中。
在这里插入图片描述
方法区中存储的内容:

  • 类型信息(域信息、方法信息)
  • 运行时常量池

类型信息:

对于每个加载的类型(类class、接口interface、枚举enum、注解annotation)jvm必须在方法区中存储以下类型信息:

  1. 这个类型完整的有效名称(包名+泪名)
  2. 这个类型直接父类的完整有效名称
  3. 这个类型的修饰符(public、private、abstract、final等)
  4. 这个类型直接接口的一个有序列表

域信息

即为类的属性,成员变量

jvm必须在方法区中保存类所有的成员变量相关信息及声明顺序。

域的相关信息包括:域名称、域类型、域修饰符(pυblic、private、protected、static、final、volatile、transient的某个子集)

方法信息

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

  1. 方法名称方法的返回类型(或void)
  2. 方法参数的数量和类型(按顺序)
  3. 方法的修饰符public、private、protected、static、final、synchronized、native,、abstract的一个子集
  4. 方法的字节码bytecodes、操作数栈、局部变量表及大小( abstract和native方法除外)
  5. 异常表( abstract和 native方法除外)。每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

方法区设置:

jdk8之前:

  • 通过-xx:Permsize来设置永久代初始分配空间。默认值是20.75M
  • -XX:MaxPermsize来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M
  • 当JVM加载的类信息容量超过了这个值,会报异常OutofMemoryError:PermGen space。

jdk1.8及以后:

元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定

jps #查看进程号
jinfo -flag MetaspaceSize 进程号 #查看Metaspace 最大分配内存空间
jinfo -flag MaxMetaspaceSize 进程号 #查看Metaspace最大空间

7、运行时常量池

常量池VS运行时常量池

1.常量池是存在字节码文件中
2.运行时常量池是存在于方法区中
常量池: 存放编译期间产生的各种字面量与符号引用
运行时常量池: 常量池表在运行时的表现形式

编译后的字节码文件中包含了类型信息、域信息、方法信息等。通过ClassLoader将字节码文件的常量池中的信息加载到内存中,存储在了方法区的运行时常量池中。

8、直接内存

在JDK 1.4中新加入了NIO(New Input/Output) 类, 引入了一种基于通道(Channel) 与缓冲区 (Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存, 然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了 在Java堆和Native堆中来回复制数据。

在这里插入图片描述
NIO的Buffer提供一个可以直接访问系统物理内存的类——DirectBuffer。DirectBuffer类继承自ByteBuffer,但和普通的ByteBuffer不同。普通的ByteBuffer仍在JVM堆上分配内存,其最大内存受到最大堆内存的 限制。而DirectBuffer直接分配在物理内存中,并不占用堆空间。

通过使用堆外内存,带来的好处:

  1. 改善堆过大时垃圾回收效率,减少停顿。Full GC时会扫描堆内存,回收效率和堆大小成正比。Native的内存,由OS负责管理和回收。
  2. 减少内存在Native堆和JVM堆拷贝过程,避免拷贝损耗,降低内存使用。
  3. 可突破JVM内存大小限制。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值