• 八股文之JVM


    目录

    1.JVM内存划分

    2.JVM类加载过程

    3.JVM垃圾回收机制GC

    3.1.判断谁是垃圾

    3.2.如何释放对应的内存


    1.JVM内存划分

    在一个Java程序运行起来之后,jvm就会从操作系统中申请一块内存,然后就会将该内存划分成多个部分,用于不同的用途。

    (1)主要划分成五个部分

    堆、栈、元数据区(方法区)、常数计数器(很少涉及)

    图示:

    像堆区中的各个部分划分,在后面的垃圾回收机制再拿出来鞭策一遍,在这里就不先做任何的赘述。

    (2)每块内存区的功能

    堆区:

    整个内存区域中最大的区域,用于存放java代码中new出来的对象、和成员变量。

    栈:

    一般java是使用jvm虚拟机栈,这里保存了方法的调用关系、局部变量等。也就是每个方法怎么被调用,被谁调用等

    元数据区:

    元数据区,以前也成为方法区,用来存放类对象、类属性(静态成员)、常量

    程序计数器:

    属于内存中最小的区域,用来保存要执行的下一条指令的地址

    (3)实战划分内存

    看下嘛的一段代码,查看下面的变量和对象分别处于哪一块内存中

    1. class Demo2 {
    2. }
    3. class Demo1 {
    4. public int a;
    5. Demo2 b = new Demo2();
    6. public String c = "love";
    7. public static int d;
    8. }
    9. public class Test {
    10. public static void main(String[] args) {
    11. Demo1 e = new Demo1();
    12. }
    13. }

    对于变量:a,b,c,d,e 其中a,b,c都属于成员变量,存在于堆上;而e属于局部遍历,存在栈上;而d属于静态成员变量,属于类属性,存在于元数据区。它们只是属于变量,里面存有一个值,会指向另一块内存空间。

    各对象内存分布和指向关系

    以上就是对应的各种关系。

    对于上述的四个区域,堆和元数据区,在整个进程中只有一份,而栈和程序计数器,是每个线程都有一份。

    2.JVM类加载过程

    一个.java文件变成.class文件的过程,也是从硬盘加载到内存中,得到类对象的过程。

    (1)类加载的五个环节

    加载、验证、准备、解析、初始化

    (2)每个环节对应的作用

    1.加载

    在硬盘上找到对应的.class文件,并且读取.class文件内容

    2.验证

    检查.class文件中的内容,是否符合要求(如文件格式等)

    3.准备

    给类对象分配内存空间

    4.解析

    对字符串常量初始化,把刚才.class文件中的常量内容取出来放到元数据区

    5.初始化

    针对类对象进行初始化,给静态成员初始化,也就是执行静态代码块

    (3)“双亲委派模型”

    在第一步的加载环节,目的是打开.class文件,前提就是需要通过“全限定类名”找到文件才能进行打开,所以“双亲委派模型”就是寻找.class文件的一种机制。

    在这个环节,涉及到一个概念:类加载器

    在JVM中也会包含一些类,负责完成后续的类加载工作。其中JVM内置了三个类加载器,负责加载不同的类

    分别是三个类:BootstrapClassLoader、ExtentionClassLoader、ApplicationClassLoader

    1)三个类加载器作用

    所以成为双亲委派模型,但是跟准确的说法为:父亲委派模型或者单亲委派模型

    2)加载过程

    那么双亲委派模型是如何找类的呢?我们举一个例子,假设我们自己写了一个java程序,会给定一个全限定类名

    3)双亲委派模型应对的场景

    如果自己的代码中写的类的名字和标准库/扩展库冲突了,JVM会确保加载的类是标准库的类(不会加载自己写的类),如果标准库中的类无法加载,那么Java进行就没有办法正常工作了。

    这样还有一个好处,就是可以确保自己写的类肯定可以被加载到。

    3.JVM垃圾回收机制GC

    对于Java,回收垃圾采取的是自动回收策略,策略也称为GC。

    对于GC来说,回收的其实是堆上的内存。而对于堆,保存的主要是对象,换句话说,也就是主要回收对象,那怎么回收对象呢?主要有两个步骤:判断谁是垃圾和如何释放其对应的内存。

    3.1.判断谁是垃圾

    在判断谁是垃圾这一步,Java是采取很保守的做法,也就是可以保证只会释放后续不会再使用的对象,后续仍会使用到的对象,是不会进行回收的,所以才用的策略是:判断某个对象是否存在引用指向,如果没有引用指向,就可以判断为垃圾,反之不行。

    判断谁是垃圾,GC有两种策略:引用技术和可达性分析,而JVM采取的策略只有可达性分析,引用计数则不是。

    (1)引用计数

    1)策略:每创建一个对象,就在对象前面多开辟一块空间,用来计数使用,有一个引用指向该对象,计数变量就+1,如果计数器为0,则需要回收该对象。

    2)代码举例

    1. class TT {
    2. }
    3. public class GC {
    4. public static void main(String[] args) {
    5. TT a = new TT();
    6. TT b = a;
    7. }
    8. }

    对于实例化的TT对象,当前有两个对象指向它,所以计数值为2(分别是引用a和b),如果a=null或者b=null,则计数值-1,两个都置为null,则计数为0.

    对于引用计数存在两个问题

    问题一:会消耗额外的内存空间

    如果对象本身的内存比较大,相比来说计数的空间就很小;但是如果对象内存空间很小,那么计数空间就会显得很大,就会浪费很大的空间

    问题二:存在“循环引用”问题

    这类问题就会让外部代码无法对 对象进行释放

    代码举例:

    1. class T {
    2. T t;
    3. }
    4. public class GC {
    5. public static void main(String[] args) {
    6. T a = new T();
    7. T b = new T();
    8. a.t = b;
    9. b.t = a;
    10. }
    11. }

    这是一段有问题的代码

    图示分析:

    存在的问题:当将引用a和b置为null时,按理来说这两个对象是要被回收了,但是这里却不会,因为计数不为0,不能回收。所以这就是引用计数最大的一个问题。

    (2)可达性分析

    JVM采取的是可达性分析,既解决了空间问题,也解决了循环引用问题。

    1)定义:JVM会把对象之间的引用关系,定义成树形结构,JVM可以不停的从根节点开始遍历,可以访问到的对象,成为“可达”,剩下的就是“不可达”。

    标记为不可达的对象,也就被标记成垃圾。

    2)举例

    例如这样的结构,如果将c=null,那么c后面的两个对象f和g就变成不可达,就要成为垃圾。

    访问这棵树的所有节点,都是要通过根节点a开始。

    3)确定根节点

    对于根节点,可能的情况有:栈上的引用局部变量、常量池中引用的对象、方法区中的静态成员。

    3.2.如何释放对应的内存

    当已经标记好垃圾之后,该怎么回收呢?下面介绍。一共四种,前面三种是铺垫,最后一种才是JVM真正使用的

    (1)标记-清除

    策略:直接把标记为垃圾对象的对应的内存回收,已经回收的内存,其他对象可以使用

    缺点:这样做很存在内存碎片问题,后续就很难申请到一大块连续的空间

    (2)复制算法

    策略:要删除空间1中abcd中的ac,就直接将bd复制到另一块空间中

    缺点:严重浪费空间。比如有8G内存,就只能使用4G

    (3)标记-整理

    策略:把不需要释放的内存空间覆盖到需要释放的空间上。可以解决内存碎片问题

    缺点:时间开销很大

    (4)分代回收

    策略:根据不同的场景,采取不同的回收策略,这里的回收策略也就是上述介绍到的。

    如何根据场景呢?就是要根据时间来,哪个时间呢?就是对象的年龄。

    因为GC主要回收的地方就是堆区,所以堆区上会有对应的分区,不同的区代表对象的年龄

    Eden:称为伊甸区,新创建的对象都处于这里。

    这里的对象生命周期都很短,一般经过一轮GC就会成为垃圾。从伊甸区到达生存区,采取的是复制算法

    s0:称为生存区,一般经过一轮还未被回收,就会到达下一个阶段

    从生存区到达幸存区,也是通过复制算法

    s1:称为幸存区

    这里也称为生存区,到达这里也需要通过复制算法。

    Old区:到达这里的对象都会成为老年代的对象,GC的扫描频率会大幅度降低。

    对于老年代中的垃圾,就会通过标记-整理的方法进行回收

    总结一下:伊甸区、生存区、幸存区都是通过复制算法回收垃圾很搬运到下一个区,对于老年区,则是通过标记-整理回收垃圾。

  • 相关阅读:
    一文了解Pycharm快捷键
    外汇天眼:一步错步步错,投资者表示真后悔遇到DIFX杀猪盘
    Lnmp架构之mysql数据库实战2
    【K8S】常用的 Kubernetes(K8S)指令
    02-React中JSX存在的意义
    已解决!Cannot resolve plugin org.apache.maven.plugins:maven-compiler-plugin:3.8.1
    SpringBoot 入门
    canvas绘图API
    Yarn的Tool接口案例
    【C进阶】内存函数
  • 原文地址:https://blog.csdn.net/2301_77053417/article/details/139546677