您的位置:首页 > 教育 > 培训 > 网站推广办法_山东网站优化推广_上海关键词优化排名软件_今日腾讯新闻最新消息

网站推广办法_山东网站优化推广_上海关键词优化排名软件_今日腾讯新闻最新消息

2025/5/9 8:57:01 来源:https://blog.csdn.net/wk200411/article/details/146157903  浏览:    关键词:网站推广办法_山东网站优化推广_上海关键词优化排名软件_今日腾讯新闻最新消息
网站推广办法_山东网站优化推广_上海关键词优化排名软件_今日腾讯新闻最新消息

JVM目录

  • 一.JVM内存区域划分
    • 1.内存区域介绍
    • 2.代码中变量位于的区域
  • 二.JVM类加载的过程
    • 1.类加载的具体步骤
      • (1)加载
      • (2)验证
      • (3)分配内存空间
      • (4)解析
      • (5)初始化
    • 2.双亲委派模型
      • (1)双亲委派模型介绍
      • (2)工作流程
  • 三.JVM的垃圾回收机制(GC)
    • 1.需要回收的内存区域
    • 2.回收机制实现
      • (1)找出“垃圾”
        • i.引用计数器
        • ii.可达性分析(Java采用的方案)
      • (2)释放“垃圾”的内存空间
        • 1.标记- 清除
        • 2.复制算法
        • 3.标记-整理
        • 4.分代回收
    • 3.其他的垃圾回收器

一.JVM内存区域划分

1.内存区域介绍

1)程序计数器(比较小的空间)

保存了下一条要执行指令的地址。此处的程序计数器不再是CPU的寄存器了,而是内存空间。并且,此处的下一条要执行的指令是Java的字节码(不是CPU的二进制的机器语言)

2)堆

JVM上最大的空间,new出来的对象都在堆上。

3)栈

函数中的局部变量,函数的形参,函数之间的调用关系。

Java虚拟极栈:JVM之上运行的Java代码的方法调用关系

本地方法栈:JVM里头,C++代码的函数调用关系。

4)元数据区(方法区)

Java程序中的指令(指令都包含在类的方法中)

保存了代码中涉及到的类相关的信息

类的static属性

在一个Java进程中,元数据区和堆是只有一份的(同一个进程中的所有线程都是共用一份数据的),程序计数器和栈可能有多份,当一个Java进程中有多个线程的时候,每个线程都有自己的程序计数器和栈

线程就代表一个执行流,每个线程就需要保存自己的程序计数器,每个线程也需要记录自己的调用关系。
在这里插入图片描述

2.代码中变量位于的区域

类似于这样一段的代码,需要明确其中变量存在于JVM内存区域的哪个区域。一个变量处于哪个内存区域,这个和变量是不是“内置类型”无关,而是和变量的形态有关。

(1)局部变量:栈

(2)成员变量:堆

(3)静态成员变量:元数据区(方法区)
在这里插入图片描述
a,b都是成员变量,均在堆区。

c,d是静态成员变量,均在元数据区(d是new了一个对象,new出来的对象是在堆区,但是把这个堆上的内存地址,赋值给了d引用类型的变量)

e,f都是局部变量,均在栈区(f也是new了一个对象, new出来的对象依旧在堆区,但是把这个堆上的内存地址,赋值给了这个f变量)

二.JVM类加载的过程

运行java进程的时候,JVM就需要读取 .class中的内容,并且执行里面的指令(读取 .class中的内容就是类加载,把类涉及到的字节码从硬盘读取到内存中(元数据区),把这个 .class 中的指令转换成类对象)

加载一个 .class文件,就会对应创建一个类对象,类对象就包含了 .class文件中的各种信息(比如:类名,类的属性和方法,继承的父类,实现的接口等等······)也就说明类对象就是对象的说明书/蓝本

1.类加载的具体步骤

类加载分为五个环节,也可以分为三个环节,无论是那种都是对的,只是一些环节被归为一类而已,此处说的是五个环节

(1)加载

把 .class 文件找到,代码中先见到类的名字,然后进一步的找到对应的 .class 文件(涉及到一系列目录查找的过程)找到后,
打开并读取文件内容

(2)验证

验证读到的 .class 文件的数据是否正确与合法,在Java标准文档中,明确定义了 .class 文件的格式是怎么样的
在这里插入图片描述
在这里插入图片描述

u4:4个字节的unsigned int,u2:2个字节的unsigned short这是在Java中的,c++则不一定,c++会因为不同的操作系统和编译器导致l同类型的字节数不同。
magic:魔幻数字,在二进制文件中的开头,有若干个字节,设置一个固定的常数进去,通过这个常数,标识当前这个文件是什么样的文件
minor_version和major_version:确保编译和运行时的JDK版本一致
constant_pool_count:常量池
cp_info:其他结构体
access_flags,this_class,super_class:代表这个类是public还是其他的修饰词,后续代表的是类和父类
fields_count ,fields[fields_count]:代表类中的属性
后面的都代表类中的方法

在这里插入图片描述

(3)分配内存空间

根据刚才读取到的内容,确定出类对象需要的内存空间,再申请这样的内存空间,并且把内存空间中的所有内容,都初始化为0(Java创建一个内存空间,都会把这个内存空间全部设置为0,后续再进一步的初始化)

类加载执行到第三步时,此时a的值还为0
在这里插入图片描述

(4)解析

主要针对类中的字符串常量进行处理,解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,也就是初始化常量的过程。

直接引用:平时谈到的代码中的引用,里面也保存了变量的地址。

符号引用:字符串常量,已经在 .class 文件中。

“hello”字符串常量,已经在 .class 文件中了,但是没有地址这样的概念,此时地址的概念就是内存的地址,取而代之的是,类似于文件偏移量的概念。
在这里插入图片描述

符号引用和直接引用图解:
在这里插入图片描述

(5)初始化

针对类对象做最终的初始化操作,执行静态成员的赋值语句。

在这里插入图片描述
执行类中的静态代码块,针对父类也要进行加载,在这个环节也会触发对父类的进行加载(执行上述的五个环节)

2.双亲委派模型

(1)双亲委派模型介绍

是类加载五个步骤中,第一个步骤里面的一个环节,给定 类全限定名,找到对应的class文件位置。

类加载器JVM中,已经内置了一些加载器,完成上述的内加载过程

JVM默认有三个类加载器:
不是Java父类子类的继承关系,而是类加载器中有一个parent这样的引用指向父亲
在这里插入图片描述

(2)工作流程

图解:
在这里插入图片描述
这样做的核心目的:
防止用户自己写的类,把标准库的类给覆盖了
保证标准库的类,被加载的优先级是最高的
扩展库其次,第三方库的优先级最低

三.JVM的垃圾回收机制(GC)

1.需要回收的内存区域

JVM中的内存区域的程序计数器和栈是不需要GC的,因为它们会跟随线程一起销毁。而堆是GC主要回收的地方,堆上new的对象就需要GC判断是否需要回收。元数据区中的类对象的类加载是有上限的,不会出现无限增长的情况,一般不需要GC。

堆的分区
在这里插入图片描述

2.回收机制实现

(1)找出“垃圾”

需要针对每个对象进行判定是否为“垃圾”,在Java中使用一个对象,一般都是通过引用来使用的,如果一个对象没有引用指向了,都可以认为这个对象是“垃圾”了。

i.引用计数器

给每个对象分配一个计数器,衡量有多少个引用指向,每次增加一个引用,计数器+1,每次减少一个引用计数器-1,当计数器减为0时,此时对象就是“垃圾”了(多线程的可重入锁也是类似的机制)

图解计数器:
第一步
在这里插入图片描述
第二步
在这里插入图片描述
第三步
在这里插入图片描述
第四步
在这里插入图片描述

这样的方案在Java中并没有采纳,而是python和PHP进行了使用

上述方案存在两个问题:
1.消耗了额外的空间(假设Test类就只有一个int成员(4个字节),此时为了引入计数器,最少也要一个short(2个字节),内存就多占用了50%)

2.引入计数器可能导致“循环引用”使得上述的判定出错

第一步
在这里插入图片描述
第二步
在这里插入图片描述
第三步
在这里插入图片描述
这两个对象的引用计数都不是0,不能被释放,但是这两个对象又无法被使用,就造成了类似于死锁的场景。循环引用也是有解的,需要引入更多的机制(环路检测)代价就更大了。

ii.可达性分析(Java采用的方案)

在JVM中,专门搞了一些周期性的线程,扫描代码中所有的对象,判定某个对象是否是“可达”(可以被访问到),对应的,不可达的对象就是垃圾了。

1)JVM有一个所有对象的总名单

2)JVM针对当前教室里面的对象进行点名操作(被点到的,回答到),没有回答到的,就是缺课的(也就是“垃圾”)

通过类似于二叉树的结构,获取到根节点,就可以对整个树进行遍历,遍历就能够得到每一个节点的对象,此时这些对象都是可
达的。如果此时根节点的右节点为null,则为不可达。

类似于这样的树结构,当c不可达时,f也是不可达的,此时的f的可达性依赖于c的可达性。
在这里插入图片描述

可达性分析的起点称为GC root,一个程序中,GC root不是只有一个,而是有很多(栈上的局部变量(引用类型),方法去中,静态的成员变量(引用类型),常量池引用指向的对象,这三个GC root)

可达性分析,其实是挺消耗时间的,尤其是你的程序中,对象特别多的情况下,就需要很多时间。

(2)释放“垃圾”的内存空间

1.标记- 清除

直接针对内存中对应的对象进行释放
在这里插入图片描述
但是上述的做法会引入“内存碎片问题”,对象的释放是随机的,很可能释放的这个内存不是连续的,虽然把上述的内存释放掉了,但是整体这些空闲内存并没有连在一起,后续申请内存的时候,就无法申请(申请的内存必须是连续的)

2.复制算法

将一块内存分成两块,同一时刻,只使用其中的一半,当释放垃圾之前,把不是垃圾的对象拷贝到另一半中(确保拷贝的对象是连续的)然后把需要释放垃圾的这一半的空间都是释放了

第一步
在这里插入图片描述
第二步在这里插入图片描述

但是复制算法的缺点也很明显:内存空间利用率太低了;如果存活下来的对象比较多,复制的成本也比较大。

3.标记-整理

非常类似于,顺序表删除中间的元素。

第一步
在这里插入图片描述
第二步
在这里插入图片描述

但是像这样进行搬运的开销也不小。

4.分代回收

JVM中真实的解决方案,是把上述几个方案综合一下,取长补短,也就转换成了分代回收

JVM根据对象的年龄(可达性分析,周期性的每次经过一轮扫描,对象仍然存活(不是垃圾)年龄+1),把对象进行区分

新创建的对象存储在伊甸区,根据经验规律,绝大部分的新对象,活不过第一轮GC,留存下来的对象,拷贝到幸存区。
在这里插入图片描述
幸存区是两个相等的空间,也是按照复制算法(反复进行多次在幸存者区的两个内存空间中来回进行复制,只要通过GC的考验,就能够存活下来)
新生代中,真正要拷贝的对象不多(经验规律),但是内存利用率低(幸存区中的另一个内存就会被浪费,但是幸存区的内存空间比较小,所以没关系)
在这里插入图片描述

如果一个对象在幸存区,已经反复被拷贝多次,也不是垃圾,年龄则会不断增长,达到一定程度后,对象就要被拷贝到老年代了。

根据经验规律,老年代中的对象的生命周期都会比较长,老年代的对象当然也需要进行可达性分析,但是老年代进行GC的频率就会降低,另外,老年代也是通过标记整理(需要整理的次数也不多)

3.其他的垃圾回收器

分代回收时JVM中GC的基本思想方法,具体落实到JVM的实现层上,JVM还提供了多种“垃圾回收器”(对上述的分代回收,做进一步的扩充和具体实现)

重点了解CMS和G1
在这里插入图片描述

CMS:
在这里插入图片描述

G1:在这里插入图片描述

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com