JAVA--JVM

本文详细介绍了JVM架构,包括类加载器的工作原理,如启动类加载器、扩展类加载器和应用程序类加载器,以及双亲委派机制。接着,阐述了运行时数据区的各个组成部分,如程序计数器、Java栈、本地方法栈、方法区和堆,特别是堆的内存分配和垃圾回收机制。此外,还讨论了执行引擎和本地接口的作用。通过对JVM内存结构的理解,有助于优化Java程序的性能和避免内存溢出问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、JVM架构图

1.1、jvm所在位置

JVM是运行在操作系统之上的,它与硬件没有直接的交互。
在这里插入图片描述

1.2、JVM体系结构概览

在这里插入图片描述
1、类加载器(ClassLoader):负责加载字节码文件(.class文件)加载到内存中,并生成出类的数据结构模板,存放在方法区。

2、运行时数据区(RuntimeDataArea):是Java虚拟机在执行程序时为其分配和管理内存的一个逻辑区域,它负责存储和组织各个线程的执行信息、方法调用栈帧、对象实例以及类的结构数据等,以支持程序的运行。

3、执行引擎(ExecutionEngine):也叫解释器,负责执行编译后的字节码指令,交由操作系统执行。

4、本地库接口(NativeInterface):许Java程序调用本地方法(如C、C++方法),实现Java与本地代码的交互。

总结:
  首先通过类加载器(ClassLoader)会把 Java 代码转换成字节码, 运行时数据区(Runtime Data Area)再把字节码加载到内存中,而字节码文件只是 JVM 的一套指令集规范,并不能直接交给底层操作系统去执行,因此需要特定的命令解析器执行引擎(Execution Engine),将字节码翻译成底层系统指令,再交由 CPU 去执行,而这个过程中需要调用其他语言的本地库接口(Native Interface)来实现整个程序的功能。

2、类装载器(Class Loader)

在这里插入图片描述

2.1、简介

  类装载器负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);类装载器只负责class文件的加载,至于class文件是否可以允许,则由执行引擎决定。

2.2、虚拟机自带装载器

2.2.1、启动类加载器(Bootstrap)

  是嵌在JVM内核中的加载器,该加载器是用C++语言写的,主要负载加载JAVA_HOME/lib下的类库,该加载器无法被应用程序直接使用。
在这里插入图片描述

2.2.2、扩展类加载器(Extension)

  该加载器是用JAVA编写,且它的父类加载器是Bootstrap,是由sun.misc.Launcher$ExtClassLoader实现的,主要加载JAVA_HOME/lib/ext扩展目录中的类库。开发者可以使用扩展类加载器。

2.2.3、应用程序类加载器(AppClassLoader)

  负责加载ClassPath路径下的类包,我们自己创建的类所加载的类装载器就是这个。

2.2.4、自定义类加载器

  用户可以定制类的加载方式,通过继承抽象类Java.lang.ClassLoader;如tomcat。

2.2.5、类加载器之间的关系

  应用程序类加载器(AppClassLoader)的父类是扩展类加载器(Extension),扩展类加载器(Extension)的父类是启动类加载器(Bootstrap)为null;
  关系:AppClassLoader —> ExtensionClassLoader —>BootstrapClassLoader (null)
在这里插入图片描述

2.2.6、双亲委派(沙箱保护机制)

  为了测试效果。我们自定义一个类为String类,并在里面创建一个main方法,然后直接运行。
在这里插入图片描述
  首先加载的是Bootstrap加载器,由于JVM中以有java.lang.String这个类,所以会首先加载这个类,而不是自己写的类,而已有这个类中并没有main方法,所以会报“在类 java.lang.String 中找不到 main 方法”这个异常。
  这个问题就涉及到,如果有两个相同的类,那么java到底会用哪一个?如果使用用户自己定义的java.lang.String类,那么其他使用这个类的程序就会全部出错,所以,为了保证用户写的代码不污染java出厂自带的源代码,而提供了一种“双亲委派”机制,保证“沙箱安全”;即先找到先使用。

3、运行时数据区(RuntimeDataArea)

在这里插入图片描述

3.1、程序计数器或PC寄存器(ProgramCounterRegister)

在这里插入图片描述

  是一个运行中的Java程序,每当启动一个新线程时,都会为这个新线程创建一个自己私有的PC(程序计数器)寄存器。程序计数器的作用可以看做是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令;分支、循环、跳转、异常处理、线程恢复 等基础功能都需要依赖这个计数器来完成。如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是Natvie方法,这个计数器值则为空(Undefined),所以Natvie方法不归java管;计数器占用的内存空间非常小,几乎可以忽略不记。不会发生内存溢出(OutOfMemory=OOM)错误;
  总结:
    作用:用来存储执行下一条指令的地址。
    特点:是线程是私有的,不会存在内存溢出。

3.2、java栈(java stack)

在这里插入图片描述

3.2.1、简介

  1. 也叫栈内存,每个线程运行时所需要的内存称之为栈内存。
  2. 栈是在线程创建时创建的,它的生命周期跟随着线程的生命周期,线程结束栈内存也就被释放,对于栈来说不存在垃圾回收问题。
  3. 栈的生命周期和线程一致,是线程私有的,所以线程是安全的(私有和公有判断线程是否安全)。
  4. 每个栈由一个或多个栈帧(Frame)组成,一个栈帧代表着每个方法每次调用时所占用内存。
  5. 每个方法体中的8种基本数据类型+引用类型都存储在栈内存中。
  6. 当一个方法中调用了另一个方法时,那么这个栈中会存放这两个方法的栈帧内存,以此类推。
  7. 栈的执行过程是:先进后出,后进先出;第一个产生的栈帧会放在最栈底,最后一个产生的栈帧会放在栈口;当所有栈帧执行完时,会从栈口开始释放内存。类似于弹夹中的子弹,第一颗被压进弹夹的子弹是最后射出的。
  8. 每个线程或者栈只能有一个活动栈帧,也就是正在栈口执行中的栈帧叫做活动栈帧。

补充:
栈管运行,堆管存储。
方法体中的引用变量和基本类型的变量都在栈上,其他都在堆上(类的成员变量是对象的属性,所以也在堆中)。
栈运行时存储什么:8种基本数据类型+引用类型+实例方法。

3.2.2、栈溢出

栈溢出一般有两个情况:

  1. 栈中栈帧过多,导致栈帧总和内存超过栈内存
    发生案例:一个方法无条件调用自己。
  2. 栈帧过大,导致占内存溢出。
    发生案例:json数据转换,json的data中无限出现数据。
  3. 栈溢出属于错误,不属于异常。
    在这里插入图片描述

3.3、本地方法栈(Native Method Stack)

在这里插入图片描述

  本地⽅法栈⽤于管理本地⽅法的调⽤,本地⽅法栈也是线程私有的,具体做法是本地方法栈中登记native⽅法,在执行引擎执⾏时加载本地⽅法库。
  Thread类中竟然有一个只有声明没有实现的方法,并使用native关键字。用native表示,也此方法是系统级(底层操作系统或第三方C语言)的,而不是语言级的,java并不能对其进行操作。native方法装载在本地方法栈(native method stack)中。
在这里插入图片描述

3.4、方法区(Method Area)(又叫元空间)

在这里插入图片描述
  供各线程共享的运行时内存区域。它存储了每一个类(*.class文件)的结构信息,例如常量、常量池、 静态变量和方法数据、构造函数和普通方法的字节码内容。这里要和java栈区分开来。java栈是占用运行时的内存,方法区是存储class类结构信息。

3.5、堆(Heap)–重难点。

在这里插入图片描述
  

3.5.1、简介

  堆(Heap)是jvm管理最大的一块内存空间,主要用于存放Java类的实例对象;通过new关键字创建的实例对象都会存放在堆内存;一个jvm实例只存在一个堆内存。堆的内存大小是可以调节的;
  特点是它是线程共享的,堆中的实例对象都需要考虑线程安全问题;有垃圾回收机制。
  堆在物理上分为两部分:新生代+老年代( jdk7之前分为:新生代+老年代+永久代 ); 1.8及以后把永久代移除堆内存了。移除堆内存之后改了名字为:元空间(Metaspace);也就是上面说的方法区(方法区就是元空间),元空间的大小默认就是主机运行内存的大小(可以调)。
  

3.5.2、堆结构解析

在这里插入图片描述
新生代(Young Generation Space)
   简称【Young】;占用整个堆空间的1/3。新生区是类对象的诞生、成长、消亡的区域;一个类对象在这里产生、应用、最后被垃圾回收器收集,结束生命。
  新生代分为三部分:
    伊甸园区(Eden Space):简称【Eden】,占整个新生代的8/10。
    幸存0区(Survivor 0 Space):别名【from】或者简称为【S0】,占整个新生代的1/10。
    幸存1区(Survivor 1Space):别名【To】或者简称为【S1】,占整个新生代的1/10。

老年代(Tenure generation space )
  简称【Oid】;占用整个堆空间的2/3。

元空间(Permanent Space)
  用于存放JDK自身所携带的 Class,Interface 的元数据,也就是说它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭 JVM 才会释放此区域所占用的内存。
  在jdk1.7之前叫做永久代,元空间与永久代之间最大的区别在于永久代使用的是JVM的堆内存,但是java8以后的元空间并不在jvm中,而是使用本机物理运行内存。
  因此,默认情况下,元空间的大小仅受本地运行内存限制。类的元数据放入本机内存, 字符串池和类的静态变量放入 java 堆中,这样可以加载多少类的元数据就不再由MaxPermSize 控制, 而由系统的实际可用运行内存空间来控制。

3.5.3、堆的运行过程

  当第一次一些类的实例对象产生时进入【Enden区】,直到占满整个【Enden区】 ,当对象占满【Enden区】时,触发【轻量级垃圾回收(Young GC)】操作,将一些不再使用的对象回收,如果【Enden区】还有正在使用没有被回收的对象就拷贝至【From区】供程序继续使用,并把这些放入【From区】的对象的年龄+1,然后清空整个【Enden区】;当【Enden区】空间再次被占满时,再次触发【轻量级垃圾回收(Young GC)】操作,将【Enden区】的没有被回收的对象存放在【To区】,并把对象的年龄+1,而且同时也会回收【From区】的对象,如果没被回收的对象也会拷贝存入【To区】,并且年龄再次+1,然后清空整个【Enden区】和【From区】,这时将【From区】和【To区】身份进行替换,将【To区】转换为【From区】,同理将【From区】转换为【To区】,转换后的【From区】继续供程序使用。往后以此内推,也就是随时保证【To区】要为空。
  如果一些对象的年龄15次之后,还没有被回收,则存入【老年代】继续供系统使用。如果到最后,【老年代】也满了,那么就【老年代】进行【重量级垃圾回收(Full GC)】,如果进行了【重量级垃圾回收(Full GC)】之后,还是无法腾出【老年代】的空间,就会报【java.lang.OutOfMemoryError: Java heap space】异常也就是OOM异常。

  如果出现java.lang.OutOfMemoryError: Java heap space异常,说明Java虚拟机的堆内存不够。原因有二:
  (1)Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
  (2)代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。

3.5.4、查看jvm默认的内存大小。

在这里插入图片描述

public class Test {

    public static void main(String[] args) {

        System.out.println(Runtime.getRuntime().availableProcessors());//获取本机cup核数。
        long totalMemory = Runtime.getRuntime().totalMemory();//返回 Java 虚拟机中的内存总量。
        long maxMemory = Runtime.getRuntime().maxMemory();//返回 Java 虚拟机使用的最大内存量。
        System.out.println("-Xms:TOTAL_MEMORY = " + totalMemory + "(字节)、" + (totalMemory / (double) 1024 / 1024) + "MB");
        System.out.println("-Xmx:MAX_MEMORY = " + maxMemory + "(字节)、" + (maxMemory / (double) 1024 / 1024) + "MB");

    }
}

在这里插入图片描述

3.5.5、idea调节jvm内存大小

  在生产过程中,jvm的初始内存和最大内存一定要配置为一样大。
  理由:避免GC和应用程序争抢运行内存,导致内存的峰值呼高呼低,产生停顿。

在IDEA中设置JVM的内存大小
在这里插入图片描述
选择指定的项目设置jvm内存的大小。
在这里插入图片描述
-Xms1024m -Xmx1024m -XX:+PrintGCDetails
释:
  -Xms1024m :设置jvm的默认值
  -Xmx1024m :设置jvm的最大值
  -XX:+PrintGCDetails :打开jvm的GC日志信息

运行结果
  PSYoungGen :新生代
  ParOldGen:老年代
  Metaspace:元空间
在这里插入图片描述

4、执行引擎(Execution Engine)

![在这里插入图片描述](https://img-blog.csdnimg.cn/221c6b593
e3e41c28cf71e0f803d4e23.png)

  负责将jvm指令解析成计算机命令,提交给操作系统执行。

5、本地接口(Native Interface)

在这里插入图片描述

   本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生的时候是 C/C++横行的时候,要想立足,必须有调用 C/C++程序,于是就在内存中专门开辟了一块区域处理标记为native的代码,它的具体做法是 Native Method Stack中登记 native方法,在Execution Engine 执行时加载native libraies。
   目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket通信,也可以使用Web Service等等,不多做介绍。

6、栈、堆、方法区之间的交互关系

方法区: 存放着person.class这个类被加载之后的结构信息(包括声明的常量)。
java栈: 方法中声明了一个Person类型的变量p。
堆: 存放着Person()这个实例对象。
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值