JVM虚拟机

2023-12-13 10:11:07

目录

一,内存区域划分

二,类加载过程

2.1 类加载的基本流程

2.2 双亲委派模型

三,垃圾回收机制 - GC

3.1 找到垃圾

3.2 回收垃圾


一,内存区域划分

一个运行起来的Java进程,就是一个JVM虚拟机,就需要从操作系统申请一大块内存。这块内存空间会被划分成以下几个部分:

  1. 方法区:又叫元数据区,用来存储类对象(?.class文件加载到内存)。
  2. 堆:这里存储的内容,就是代码中 new 的对象,是内存中占据空间最大的区域。
  3. 栈:用来存储方法之间的调用关系。主要存储方法的入口,方法返回的位置,方法的形参,返回值,局部变量...
  4. 程序计数器:主要用来存放 "地址" ,表示下一条要执行的指令的地址,指令是以二进制的形式存储在方法区中,并保存在对应的类对象中。
  5. 本地方法栈:指的是使用native关键字修饰的方法,该方法是通过C++代码实现,这块区域是用来存储JVM内部的C++代码的调用关系。

注:在JVM进程中,堆和与元数据区只有一份,而虚拟机栈和程序计数器则是每一个线程中都有一份。

常见的面试题:问下面代码中,变量 n, a, t 分别处于内存中的哪个区域?

class Demo{

    public int n = 100;

    public static int a = 1;

    void test(){
        Demo t = new Demo();
    }

}
  • n 是成员变量,是存储在new的对象的区域中,上面讲到new的对象都存储在堆上,所以变量n存储在堆上。
  • a 是静态变量,而静态变量又称类属性,也就是说它是包含在类对象中的,所以变量a存储在方法区中。
  • t 是局部变量,局部变量是存储在栈中的。

总结:一个变量存储在哪里,与变量是否是引用类型,是否是基本类型无关,与变量的形态是密切相关的。局部变量存储在栈上;成员变量存储在堆上;静态变量存储在方法区中。

二,类加载过程

2.1 类加载的基本流程

java代码会被编译成 .class文件,Java程序想要运行起来,就需要让JVM读取到这些 .class 文件,并且把里面的内容构造成类对象,保存到方法区中。这个类加载过程,主要分为5个步骤:

1)加载

找到.class文件,打开文件,读取文件内容,往往代码中会给定类的 "全限定类名",例如:java.util.ArraysList,java.lang.*,JVM会根据这个类名,在一些指定的目录范围内查找。

2)验证

.class文件是一个二进制的格式,(某些字节都是有特定含义的),需要验证当前JVM读到的格式是否符合要求。当前读到的.class文件的格式必须和官方提供的格式一样,下图是官方的.class文件格式。

3) 准备

给类对象分配内存空间,这里只是分配内存空间,没有初始化,此时空间上内存的数值仍然是全0.

4)解析

针对类对象中的包含的字符串常量进行处理,进行一些初始化操作。java代码中用到的字符转常量在编译后,也会进入到.class文件中。

比如:final String str = "abc",与此同时,.class文件的二进制指令也会有一个引用str被创建出来,又因为引用本质上存储的是一个地址,而在文件中,没有地址这样的概念,所以在.class文件中,str的初始化语句,就会被设置成一个 "文件的偏移量",通过偏移量就能找到 "abc" 所在的位置,当这个类真的被加载到内存中时,再把这个偏移量替换为真正的地址。

5)初始化

针对类对象进行初始化,把类对象中需要的各个属性设置好,初始化static成员,执行静态代码块,如果有父类还要加载以下父类

2.2 双亲委派模型

双亲委派模型是属于类加载中"加载"这一环节,负责根据全限定类名查找 .class文件。

在讲双亲委派模型之前,先了解一下类加载器,类加载器是JVM中的一个模块,是专门用来进行类加载操作的,JVM中内置了三个类加载器:1. BootStrap ClassLoader? 2. Extension ClassLoader? 3. Application ClassLoader?

查找 .class文件的过程:

  1. 给定一个类的全限定类名,比如:java.lang.String
  2. 从Application ClassLoader 作为入口,开始执行查找逻辑
  3. Application ClassLoader (负责搜索项目当前目录和第三方库对应的目录),不会立即去扫描自己负责的目录,而是把查找的任务,交给它的父亲,Extension ClassLoader
  4. Extension ClassLoader (负责JDK中的一些扩展的库对应的目录),不会立即去扫描自己负责的目录,而是把查找的任务,交给它的父亲,Bootstrap?ClassLoader
  5. Bootstrap?ClassLoader (负责的是标志库的目录),不会立即去扫描自己负责的目录,而是把查找的任务,交给它的父亲,但是它没有父亲,所以只能自己扫描目录,如果找到对应的.class文件,就打开读取该文件....,如果没找到,就把任务交给孩子
  6. 也就是Extension ClassLoader,如果Extension ClassLoader找到了,就打开读取该文件....,如果没找到,就把任务交给孩子
  7. 也就是Bootstrap?ClassLoader,如果Bootstrap ClassLoader找到了,就打开读取该文件....,如果没找到,就说明没有该文件,抛出ClassNotFoundExceptioon

?该流程的目的就是为了保证优先查找标准库中的类,其次是扩展库,最后是自己写的和第三方库

三,垃圾回收机制 - GC

在C和C++中,通常使用malloc或者new来申请内存空间,但是在使用后,也需要手动调用free或者delete来释放内存,这十分考验程序员的素养,而相比之下,Java提供了一种方案——垃圾回收机制(GC),让JVM自动判定某个内存是否不再使用,如果真的不在使用,JVM会自动把该内存给回收掉,这大大降低了程序员的负担。

GC机制回收的目标,其实是内存中的对象,对于Java来说,就是new出来的对象,也就是堆上的对象;局部变量是跟随栈帧的生命周期走的,方法执行结束,栈帧销毁,内存自然就释放了;静态变量的生命周期是整个程序,它是始终存在的,这意味着静态变量是不需要释放的。综上所述,真正需要GC释放的就是堆上的对象。

GC的实现需要两大步骤:1. 找到垃圾? ? ?2. 释放垃圾

3.1 找到垃圾

在GC圈中,有两套主流方案:

1)引用计数 【Python,PHP使用】

对于new出来的对象,再单独安排一块空间,来存储一个计数器,用来记录有多少引用指向new出来的对象,一旦引用计数为0,就说明这个对象不可能被访问到,此时该对象就可以视为垃圾。

?为什么Java不使用该方法呢?因为该方法存在两个缺陷:

  1. 比较浪费内存:计数器最少也要两个字节,但是有可能创建的对象很小,就2个字节,也就是说计数器占了50%的内存,这样的话,就会浪费内存
  2. 会存在 "循环引用" 问题:

2)可达性分析 【Java使用】

该方法本质上是时间换空间,有一个/一组线程,周期性的扫描代码中所有对象,把所有能访问到的对象标记为 "可达",反之,未被标记的对象就是垃圾。

该方法是出发点有很多,不仅仅是所有局部变量,还有常量池中引用的对象,方法区中静态引用类型的对象.....,这些统称为GCRoots.

注意,这里的可达性分析都是周期性进行的,所以它比较消耗系统资源,开销比较大。

3.2 回收垃圾

三种思路:

1. 标记清除:比较简单粗暴,把找到的对象,直接释放,但是这个方案非常不好,因为这样会产生很多的内存碎片,释放资源的目的是为了让别的代码能够申请,而申请的内存都是连续的,比如:申请一个2MB的空间

2. 复制算法:通过复制的方式,把有效的对象放到一起,再统一释放剩下的空间。

3. 标记整理

?

实际上,JVM采取的释放思路,是上述基础思路的结合,以此来扬长避短。

  1. 伊甸区存放刚new的新的对象,根据经验规律,从对象诞生到第一轮可达性分析,这个过程中大部分对象都会成为垃圾。
  2. 伊甸区 => 幸存区,使用复制算法,每一轮GC扫描后,都要把有效对象拷贝到幸存区的一边,伊甸区就可以整体释放。
  3. 幸存区分为两份,大小相同,每次GC扫描幸存区的时候,就会把有效对象拷贝到幸存区的另一边。
  4. 当这个对象已经在幸存区存活过很多轮GC扫描之后,JVM就认为这个对象短时间内是释放不掉了,就会把这个对象拷贝到老年代
  5. 进入老年代的对象,虽然也会被GC扫描,但是JVM认为这个对象短时间内是释放不掉,所以GC扫描的频率就会比新生代低很多。这也是为了减少GC扫描的开销。

我们将上述思想称为:分代回收

文章来源:https://blog.csdn.net/m0_74859835/article/details/134895383
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。