[JavaSE] JVM

目录

1.简单介绍JVM

[2. JVM内存区域划分](#2. JVM内存区域划分)

[3. JVM类加载](#3. JVM类加载)

[3.1 类加载的步骤](#3.1 类加载的步骤)

[3.2 类加载中的双亲委派模型](#3.2 类加载中的双亲委派模型)

[4. 垃圾回收(GC)](#4. 垃圾回收(GC))

[4.1 JVM的垃圾回收](#4.1 JVM的垃圾回收)

[4.2 找到垃圾](#4.2 找到垃圾)

[4.3 释放垃圾](#4.3 释放垃圾)


1.简单介绍JVM

JVM就是Java虚拟机,是专门为Java程序服务的模型,可以将Java代码编译后的字节码文件通过JVM将字节码转换成不同操作系统对应的机器码进而运行,是Java跨平台性的核心机制。

2. JVM内存区域划分

一个Java进程就相当于一个Java虚拟机,JVM向操作系统申请一块内存空间,JVM再自己对这块内存空间进行分配,划分了四个核心区域:

程序计数器:

用来存储当前线程正在执行的Java方法对应的JVM中的指令地址。

方法区(元空间):

用来记录被JVM加载的类的信息,静态变量,常量等数据,相当于把.class文件加载到内存里面,比如:类的名字,类的使用范围,继承自哪些类,实现了那些接口,方法的名字,返回值类型,参数的类型和数量。

Java虚拟机栈:

每个线程都会创建一个Java虚拟机栈,与线程的生命周期相同,每个方法在执行时候都会创建一个栈帧,用来存储局部变量表,操作数栈,返回出口等信息,每调用一个方法就会创建一个栈帧放到栈里面,JVM的栈是利用C++代码实现的用来存放Java方法的栈。

堆:

用来保存对象的实例的,是JVM里面最大的内存区域,如果对象不使用了就会回收掉。

Test t = new Test();

这里的t是局部变量,是放在栈上的。

t是成员变量是放在堆上的。

t是静态变量是放在元数据区的。

元数据区和堆,一个进程共享一个,程序计数器和栈,一个进程存在多个。

3. JVM类加载

3.1 类加载的步骤

类加载就是将.class文件加载成类文件,通过三个阶段,具体是五个步骤来实现类加载的。

加载:

首先根据类的全限定名(包名+类名)找到.class文件的位置,将.class文件里面的二进制字节流的静态存储结构,转换为方法区中运行时的数据结构,并且在在内存中创建一个该类的对象,作为后续该类的各种数据的访问入口。

验证:

确保读取到的字节流数据符合JVM的要求,并且保证这个被加载的类不会危害到虚拟机的安全。

准备:

给类中的静态字段分配内存空间,并设置默认的初始值,如果是被final修饰的静态字段,不会分配,因为被final修饰的字段在类编译时候就分配了。

解析:

针对字符串常量,进行初始化,将.class文件中解析出来的字符串常量放到内存中,也就是元空间中的常量区。

初始化:

执行类的构造方法,初始化类的各种属性,包括静态成员,如果有父类还未被加载,就会触发父类的加载。

3.2 类加载中的双亲委派模型

在JVM中有专门的模块来进行类加载过程,这样的模块就是类加载器。

JVM中默认的类加载器有三种:

BootstrapClassLoader,在Java标准库的目录里面进行查找。

ExtensionClassLoader,在Java拓展库的目录里面查找。

ApplicationClassLoader,在Java的第三方库目录或者当前项目目录里面找。

双亲委派模型的过程就是:

类加载器接收到类加载的请求后,需要通过全限定类名来找到对应的class文件,首先从ApplicationClassLoader作为入口,将请求传给引用的ExtensionClassLoader,然后ExtensionClassLoader把类加载请求传给引用的BootstrapClassLoader来加载请求,接着BootstrapClassLoader在标准库里面找是否有符合要求的class文件,如果没有找到,就把请求返回给ExtensionClassLoader,ExtensionClassLoader在Java拓展库中寻找,没有找到,把请求返回给ApplicationClassLoader,ApplicationClassLoader在第三方库和当前项目中寻找,如果没有找到就会抛出异常。

4. 垃圾回收(GC)

4.1 JVM的垃圾回收

JVM中的程序计数器,栈区的生命周期都是随着线程的结束而销毁的。而JVM的垃圾回收主要是针对堆里面的不再使用的对象的回收,而GC的工作流程就是找到垃圾,释放垃圾。

4.2 找到垃圾

找到垃圾的方法有两种:引用计数和可达性分析。

引用计数:

引用计数是在内存中每创建一个新对象,就会申请一小块空间来表示该对象被多少引用指向,每有一个引用指向该对象,引用计数就会+1,如果某个对象的引用计数为0,该对象就没有引用使用,就会被释放。

该方法存在的缺陷:

如果对象内存比较小,而引用计数占用的内存的占比就会比较大,比较消耗内存空间。

有可能出现"循环引用"这样的问题:

执行完左边的逻辑后,此时两个对象不再使用,应该释放,但是此时的两个对象的引用计数都是1,不能释放,此时就导致了循环引用的问题。

可达性分析:

JVM通过一些引用的对象,比如栈中的引用类型的局部变量,常量池引用指向的对象,引用类型的静态成员变量,作为起点,然后进行遍历,判断每个对象是否能访问到,如果能访问到的对象就标记为可达,遍历完成后,所有为不可达的对象都应该被释放。

4.3 释放垃圾

释放垃圾的算法有下面几种:

标记-清除算法:

该算法直接将对象释放掉,但是会产生内存碎片化的问题,导致空闲内存是不连续的,在申请内存时,都是申请连续的内存的,这种方法会导致,空闲内存很大,但连续的空闲内存很小,导致可申请的内存空间很小。

复制算法:

该算法是将可用的内存空间分成两部分,一部分空闲着,一部分用来存储对象,将存储对象内存中的垃圾对象释放后,然后将不是垃圾的对象复制到空闲的内存空间,这样剩下的内存空间就是连续的了。

该算法的缺点:

该算法的内存利用率比较低,并且如果对象比较大或者多的情况,复制的成本就比较大。

标记-整理算法:

该算法是在清理完垃圾对象后,将不是垃圾对象的对象进行移动,移动成连续的内存空间。

该方法解决了内存利用率低的问题,但是如果对象比较多或者大的情况,移动的成本就比较大。

Java释放垃圾的机制是:分代回收。

这里的代指的是对象的年龄,也就是GC的次数,如果某个对象经过一次GC后没有被删除,此时该对象的年龄就会+1。

分代回收是将堆分成新生代和老年代两个区域,并且将新生代分为伊甸区和幸存区两种类型的区域,伊甸区比幸存区大。

新创建的对象会先放在伊甸区,经过一轮GC后,会将存活下来的对象通过复制算法放到一个空闲的幸存区,然后幸存区里面的对象也会经过GC,幸存下来的对象会放到另一个幸存区里面,如果一个对象经过多次GC之后,仍然幸存,就会晋升成为老年代,而老年代内部的学习销毁对象是通过标记-整理算法来实现的,因此新生代GC的次数比较频繁,老年代GC的次数比较少。

如果一个对象很大,会直接晋升成为老年代。

而在JVM中会有专门的垃圾收集器模块来完成上面的GC过程,比如:CMS G1 ZGC等。

相关推荐
是一个Bug4 小时前
ConcurrentHashMap的安全机制详解
java·jvm·安全
【非典型Coder】7 小时前
jvm 方法区和永久代
jvm
yueqc110 小时前
虚拟机(一):JVM
jvm
没有bug.的程序员13 小时前
JVM 与 Docker:资源限制的真相
java·jvm·后端·spring·docker·容器
上78将13 小时前
JVM的程序计数器
jvm
萧曵 丶15 小时前
CompletableFuture 底层原理详解
java·jvm·多线程·并发编程
Query*16 小时前
JVM性能调优【一】—— 理论篇
jvm
Query*16 小时前
JVM性能调优【二】—— 工具篇
jvm
Han.miracle16 小时前
Java 8 Lambda 表达式与方法引用的语法优化及实战应用研究
java·开发语言·jvm