欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 新闻 > 资讯 > 八股文之JVM

八股文之JVM

2024/11/20 18:21:20 来源:https://blog.csdn.net/2301_77053417/article/details/139546677  浏览:    关键词:八股文之JVM

目录

1.JVM内存划分

2.JVM类加载过程

3.JVM垃圾回收机制GC

3.1.判断谁是垃圾

3.2.如何释放对应的内存


1.JVM内存划分

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

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

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

图示:

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

(2)每块内存区的功能

堆区:

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

栈:

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

元数据区:

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

程序计数器:

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

(3)实战划分内存

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

class Demo2 {}
class Demo1 {public int a;Demo2 b = new Demo2();public String c = "love";public static int d;}public class Test {public static void main(String[] args) {Demo1 e = new Demo1();}
}

对于变量: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)代码举例

class TT {}
public class GC {public static void main(String[] args) {TT a = new TT();TT b = a;}
}

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

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

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

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

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

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

代码举例:

class T {T t;
}
public class GC {public static void main(String[] args) {T a = new T();T b = new T();a.t = b;b.t = a;}
}

这是一段有问题的代码

图示分析:

存在的问题:当将引用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的扫描频率会大幅度降低。

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

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

版权声明:

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

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