captain376头像
关注
JVM(Java Virtual Machine)封面图

JVM(Java Virtual Machine)

目录

JVM-Java虚拟机/解释器

一次编写,到处编译:

一次编写,到处执行:

JVM三个核心内容(面试常考)

1.JVM内存区域划分

1.程序计数器

2.栈

虚拟机栈

栈帧

本地方法栈

3.堆

4.元数据区

类的元信息

类对象

反射:

5.补充:

内存溢出

线程、进程内存的不同

2.JVM类加载机制

1.类加载流程

加载

验证

ClassFile

准备

针对字符串常量进行初始化

初始化

2.类加载的时机

3.双亲委派模型

3.JVM的垃圾回收

1.找出垃圾

1.引入

2.可达性分析

GCRoots

2.识别出是垃圾之后,如何释放内存?

1.标记清除

2.复制算法

标记整理

分代回收

·结语


·JVM-Java虚拟机/解释器

JVM可以实现“一次编写,到处运行”。

创建一个进程,就会自动创建一个JVM。

开发Java代码,需要下载JDK;只是运行,只需下载JRE即可。

一次编写,到处编译:

像C/C++这些语言都没有“虚拟机”这个机制,所以写一个代码,使用A平台编译,生成一个A的可执行程序;

使用B平台编译,生成一个B的可执行程序;

在切换平台的时候,就可能要调整代码,且不同的CPU有不同的指令集和,不同的操作系统有不同的API。

一次编写,到处执行:

而像Java/python/JavaScript等,都引入了“虚拟机”,实现“一次编写,到处运行”。

.java文件编译成.class字节码文件,把字节码文件打包成一个.jar包,不同版本的Java虚拟机都可以解释执行相同的字节码。

JVM三个核心内容(面试常考)

1.JVM内存区域划分

内存区域分为4部分:

(注:以下的堆、栈都是JVM上的,不要和数据结构、操作系统搞混)

1.程序计数器

程序计数器只存一个数字,这个值是下一条要执行的Java字节码指令的地址。

2.栈

虚拟机栈

给Java程序使用的栈,用来维护方法的调用信息。

调用方法时,按照调用顺序压入虚拟机栈,先进后出。

栈帧

栈帧存的是

1.调用方法的参数信息

2.方法内部的局部变量

3.方法结束后要返回的上一层方法的位置

4.返回值

本地方法栈

当Java代码调用到native方法时,就把栈帧压入本地方法栈,栈帧记录着C/C++方法的信息。

注:

本地方法栈与虚拟机栈的栈帧记录的内容不同。

native方法源码不在JVM中,在JDK的源码包中,运行时源码被编译为动态库文件xxx.dll,JVM启动时,会加载这个动态库,里面是机器码。

3.堆

最大的内存空间,new的对象都在堆上。

4.元数据区

元数据区存的是 类的元信息。类对象(class实例)存放在 堆内存,该对象引用指向 元数据区的 类的元信息。

类的元信息

类元信息指的是:JVM运行时,把.class文件读取到内存中,解析出来,通过一些特定的结构表示的原始模板数据,存在元空间。

类元信息包括:类名、父类、接口、方法、成员变量的定义、static属性、常量池等这个类的所有信息。

类对象

类对象指的是:JVM为这个类创建的普通对象的实例

类对象包括:持有引用,指向元空间的类元信息;提供一堆方法(反射API),用来读取元空间的元信息

反射:

程序运行期间,拿到一个类的全部信息,运行时才知道类是谁。

//拿到Class类对象
Class clazz = Demo.class;
//拿到test方法
Method method = clazz.getMethod("test");
//调用方法
method.invoke( new Demo() );

5.补充:

局部变量存储在栈上;

成员变量存储在堆上;

类对象/static修饰的属性 存储在 元数据区上;

内存溢出

栈溢出:调用方法信息过多,栈帧过多,导致栈溢出。

堆溢出:new的对象太多,导致堆溢出。

注:Java可以手动设置栈空间和堆空间大小。

线程、进程内存的不同

程序计数器、栈每个线程各有一份;

堆、元数据区一个进程只有一份。

2.JVM类加载机制

类加载:JVM把.class文件读取放到 内存中,构建出 类对象 和 生成元信息 的过程

1.类加载流程

分为5步:

加载

找到.class文件,打开文件,把文件读取到内存中

根据“全限定类名”,找到对应的.class文件

全限定类名:例如“java.util.Scanner”.

根据“双亲委派模型”来找全限定类名。

验证

根据读到的二进制内容,验证是否是合法的格式。

验证依据:ClassFile-JVM官方规定的.class文件的二进制结构模板。

ClassFile

注:二进制内容不包含注释!

u4-》unsigned int

u2-》unsigned short

1.

magic:二进制文件通常把开头的四个字节,设定成特殊值。

设magic的原因是:文件的后缀名可以随便改,所以不靠谱,要设置特殊标记。

JVM读到文件的前4个字节,如果读到CAFEBABE-》是class文件,继续;

不是CAFEBABE-》不是合法class文件,抛出异常,加载失败。

2.

minor_version(次要版本),major_version(主要版本)

描述了这个.class文件是通过哪个java版本的编译器生成的。

如果是Java17编译出来的.class,不能放到Java8的JVM上执行;

如果是Java8编译出来的.class,能放到Java17的JVM上执行;

“向前兼容”

准备

准备元数据区。

例如:

给类对象分配内存,所有静态变量同一赋默认值。

static int count,内存分配,count=0;

static int sign,内存分配,sign=0;

实例变量s,s2,a,,b不处理.

针对字符串常量进行初始化

JVM解析Test.class,发现字面量“hello”

把“hello”放到字符串常量池,字符串拿到地址,但是实例变量s此时还没有拿到这个地址,等到实

例初始化

初始化

执行静态变量显示赋值:count=100;sign没有赋值,默认0.

执行static静态代码块:控制台打印“静态代码块执行”。

到这里,Test类加载全部结束。静态变量count=100,sign=0保存在元空间的类对象中。

2.类加载的时机

一个类的类加载只有一次。

1.第一次new这个类的实例时

2.调用这个类的静态成员变量/静态成员方法时

3.子类的加载,也会触发父类的类加载。

3.双亲委派模型

出现在类加载第一步:加载,找.class文件的全限定类名

涉及到类加载器

JVM中,默认包含3个类加载器:

BootstrapClassLoader(引导类加载器)-负责加载Java标准库中的类

ExtensionClassLoader(扩展类加载器)-负责加载Java扩展库中的类

ApplicationClassLoader(应用类加载器)-负责加载第三方库,以及当前项目中的类

如果在3个类加载器中都找不到全限定类名,就会抛出ClassNotFound异常。

实际上双亲委派模型是约定了“类加载的优先级”

标准库最先加载,其次是拓展库,最后是第三方库/当前项目类

3.JVM的垃圾回收

C++:智能指针应对内存泄露

Java:垃圾回收GC应对内存泄露,JVM引入垃圾回收机制更好地应对内存泄漏(不是100%避免)。

GC回收的是堆上的内容,回收时以 对象 为一个单位回收,不能以字节为单位!

怎么确定一个对象是垃圾呢?——》一个对象没有引用指向就是垃圾(Java使用对象只能通过引用使用)。

如果判断不准,就不回收!

JVM有四种引用:强引用、弱引用、虚引用、软引用。

1.找出垃圾

1.引入

引用计数(Java未采用)

就是在每个对象的内存上安排一块空间存储一个整数,整数的值就是对象引用的个数。如果值为0就释放空间。

缺点:1.消耗更多的内存空间

2.循环引用问题

对象无法被释放,也无法被使用!

2.可达性分析

JVM专门指派一些线程,这些线程周期性的扫描代码中申请的内存,自动判定当前这个内存是否已经不再使用了。

一个Java代码中,对象之间有关联关系,这些关系组成很多树/图;

可达性分析:从一个树的根节点开始,经过的节点都标记为“可达”;

JVM知道当前进程中有多少个对象,除去标记的为“可达”的,剩下的都是“不可达的”,不可达的即为垃圾;

随着代码的执行,对象之间的关系也在不停变化,因此可达性分析周期性进行。

GCRoots

GCRoots:GC遍历的起点。

GCRoots来源于:

1.栈上局部变量;

2.常量池引用指向的对象;

3.所有引用类型的静态成员。

一轮GC尽可能地遍历所有GCRoots,尽可能标记可达。

缺点:消耗更多的CPU资源;

消耗更多的时间。

2.识别出是垃圾之后,如何释放内存?

1.标记清除

清除垃圾后内存不连续。

内存碎片会造成无法申请连续大内存问题!!!

2.复制算法

把内存分为两步分:

要删除1,3,把2,4复制一份到右边:

然后删除左边部分全部的:

优:避免了内存碎片问题。

缺:空间利用率小,复制开销大

标记整理

标记后,类似于顺序表删除:

删除:

优:避免空间浪费

缺:复制开销大

分代回收

Java实际上综合了上述策略,构成了一个复杂的方案:

分代回收:根据不同对象的情况/特点,采取不同的方案.

年龄指标:如果某个对象年龄比较大有很大概率存活下去。

对象的年龄:与GC的扫描轮次、GC的周期有关,不是轮次+1,年龄就+1;

针对老年代的扫描,称为Major GC(开销较大,频率较低)

针对新生代的扫描,称为Minor GC(开销较小,频率较高)

1.新new的对象,放入伊甸园中

2.伊甸园中的对象经过第一轮GC,淘汰掉绝大部分。

没有淘汰掉的,经过复制算法,进入幸存区;

3.下一轮GC会对幸存区的对象扫描,再淘汰掉一大批对象。

没有淘汰掉的,经过复制算法,进入另一个幸存区;

由经验规律发现,每轮GC淘汰掉大部分新对象,因此触发复制的对象就很少,避免了复制算法拷贝开销大的问题;

幸存区只占10%,浪费的内存空间小了。

4.随着每轮GC的进行,对象就会再新生代的幸存区来回拷贝,拷贝一次,年龄+1

5.经过一定时间,对象的年龄达到一定阈值。把这个对象拷贝到老年代。

6.进入老年代之后,GC的频率就低了。针对老年代的对象的回收时曹勇 标记整理 的方式处理。

7.占据内存很大的对象直接进入老年代

上述分代回收过程是简化版,真实情况复杂很多!!!

山高万仞只登一步

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/captain376/article/details/166368667

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--