【JavaEE】JVM

目录

JVM内存区域划分

类加载

类加载的基本流程

双亲委派模型

GC垃圾回收机制

GC的过程

JVM的垃圾回收过程

GC也存在缺陷


JVM内存区域划分

一个运行起来的Java进程,就是一个JVM虚拟机,需要从操作系统申请一大块内存,然后就会把这个内存,划分成不同的区域,每个区域都有不同的作用。

1、方法区(1.7及其以前)/元数据区(1.8开始)

存储的内容就是类对象,.class文件加载到内存之后,就成了类对象

2、堆

存储的内容,就是代码中new的对象(占据空间最大的区域)

3、虚拟机栈(栈)

存储的内容是代码执行过程中,方法之间的调用关系

4、程序计数器

比较小的空间,主要就是存放一个"地址",表示下一条要执行的指令,在内存中的哪个地方,方法区里的每个方法,里面的指令,都是以二进制的形式,保存到对应的类对象。

5、本地方法栈

本地方法指的是使用native关键字修饰的方法,这个方法不是使用java实现,而是在jvm内部通过C++实现的。也就是JVM内部的C++代码调用关系。

PS

  1. 虚拟机栈和程序计数器每个线程都会有一份,堆和元数据区在整个JVM进程中只有一份
  2. 局部变量处于栈上,成员变量处于堆上,静态变量(也叫做类属性)也就是放在元数据区
  3. Test t=new Test();t这个变量引用一个new出来的对象,但是t本身还是一个局部变量,还是存储在栈上。
  4. 局部变量处于栈上;成员变量处于堆上;静态变量处于方法区上

类加载

类加载的基本流程

java代码会被编译成.class文件(包含了一些字节码),java程序要想运行起来,就需要让jvm读取到这些.class文件,并且把里面的内容,构造成类对象,保存到内存的方法区中。

1、加载:找到.class文件,打开文件,读取文件类容,代码中,往往会给定某个类的"全限定名",例如java.lang.String,jvm就会根据这个类名,在一些指定的目录范围内查找。

2、验证:.class文件是一个二进制的格式。(某个字节,都是有某些特定含义的)就需要验证你当前读到的这个格式是否符合要求。

3、准备:给类对象分配内存空间(最终目的,是要构造出类对象),这里只是分配内存空间,还没有初始化,此时这个空间上的内存的数值,就是全0的,类的static成员就是全0的

4、解析:针对类对象中的字符串常量进行处理,进行一些初始化操作,java代码中用到的字符串常量,在编译之后,也会进入到.class文件中。(final String s="test")

5、针对类对象进行初始化,把类对象中需要的各个属性都设置好,还需要初始化好static成员,还需要执行静态代码块,可能还需要加载一下父类。

双亲委派模型

属于类加载中,第一个步骤,"加载"过程中,其中的一个环节,负责根据全限定类名,找到.class文件。

类加载器,是JVM中的一个模块

JVM中内置了三个类加载器:

  1. BootStrap ClassLoader
  2. Extension ClassLoader
  3. Application ClassLoader

双亲委派,类加载的过程(找.class文件的过程)

  1. 给定一个类的全限定类名,形如java.lang.String
  2. 从Application ClassLoader作为入口,开始执行查找的逻辑
  3. Application ClassLoader不会立即去扫描自己负责的目录(负责的是搜索项目当前的目录和第三方库对应目录),而是把查找任务交给它的父亲,Extension ClassLoader
  4. Extension ClassLoader也不会立即扫描自己负责的目录(负责的是JDK中一些扩展的库对应的目录)
  5. BootStrap ClassLoader也不想路基扫描自己负责的目录(负责的是标准库的目录),但因为BootStrap ClassLoader没有上层,只能亲自负责扫描标准库的目录。
  6. BootStrap ClassLoader没有扫描到,就会回到Extension ClassLoader,Extension ClassLoader就会扫描负责的扩展库的目录,如果找到,就执行后续的类加载操作,此时查找过程结束,如果没找到,还是把任务交给下层来执行
  7. Extension ClassLoader没有扫描到,就会回到Application ClassLoader,Application ClassLoader就会负责扫描当前项目和第三方库的目录,如果找到,就执行后续的类加载操作,如果没有找到,就会抛出一个ClassNotFoundException

GC垃圾回收机制

让JVM自行判定,某个内存是否不再使用,如果这个内存后面确实不用了,JVM自动把这个内存给回收掉。

GC的过程

1、找到垃圾(不需要的内存)

在GC,有两种主流的方案

引用计数(python,PHP)

new出来的对象,单独安排一块空间,来保存一个计数器,这个计数器描述这个new出来的对象有几个引用指向它,当一个对象没有引用指向了,就可以视为是垃圾了。

java没有采取引用计数的方法,因为引用计数存在两个重要的问题:1、比较浪费内存 2、存在"循环引用"的问题

可达性分析(java)

可达性分析,本质上时间换取空间的这样的方式,有一个/一组线程,周期性的扫描我们代码中所有的对象,从一些特定的对象出发,尽可能的进行访问的遍历,把所有能够访问到的对象,都标记成"可达",反之,经过扫描之后,未被标记的对象就是垃圾了。

可达性分析比较消耗系统资源,开销比较大

2、回收垃圾

三种思路

标记清除

把对应的对象,直接释放掉,就是标签清除的方案。

缺点:但是这个方案会产生很多的内存碎片,释放内存,目的是为了让别的对象能够申请,申请内存,都是申请连续的内存空间。

复制算法

通过复制的方式,把有效的对象,归类到一起,再同一释放剩下的空间。

也就是说把内存分成两半,一次只用其中一半,这个方案可以有效解决内存碎片的问题,但是缺点也很明显:

  1. 内存要浪费一半,内存利用率不高
  2. 如果有效的对象非常多,拷贝开销就很大

标记整理

既能够解决内存碎片的问题,又能处理复制算法中利用率,类似于顺序表删除元素的搬运,但是搬运的开销仍然很大。

实际上JVM采取的释放的思路,是上述思路的结合体,让不同的方案,扬长避短。

JVM的垃圾回收过程

刚new的新对象,放到伊甸区,从对象诞生,到第一轮可达性分析扫描,这个过程中,虽然时间步长(往往就是毫秒-秒)但是,在这个时间里,大部分的对象都会还成为垃圾 。

(1)伊甸区==>>幸存区,复制算法,每一轮GC扫描之后,都把有效对象复制到幸存区中,伊甸区就可以整个释放了,因为真正需要复制的对象不多,非常适合复制算法

(2)GC扫描线程也会扫描幸存区,就会把活过GC扫描的对象(扫描过程中可达)拷贝到幸存区的另一个部分

(3)当这个对象已经在幸存区存活过很多轮GC扫描之后,JVM就认为这个对象短时间应该是释放不掉了,就会把这个对象拷贝到老年代。

(4)进入老年代的对象,虽然也会被GC扫描,老年代GC扫描的频率就会比新生代低很多。

新生代,主要使用复制算法;老年代,主要使用标记整理。分代回收是JVM中主要的回收思想方法

GC也存在缺陷

1、系统开销,需要有一个/一些特定的线程,不停的扫描内存中的所有的对象,看是否能够回收,此时是需要额外的内存+CPU资源的。

2、效率问题,这样的扫描线程,不一定能够即使的释放内存(扫描总是有一定的周期),一旦同一时刻,出现大量的对象需要被回收,GC产生的负担就会很大,甚至引起整个程序都卡顿(STW问题,stop the world)

相关推荐
赵闪闪1683 分钟前
Java学习路线:从新手到精通的实战指南
java·开发语言·学习
Leo Han11 分钟前
Spring中实现动态数据源切换,基于AbstractRoutingDataSource
java·数据库·spring
sika081944 分钟前
Unity C# 影响性能的坑点
java·开发语言
啦啦右一1 小时前
Spring |(七)AOP概念及工作流程
java·spring·mybatis
夏子曦1 小时前
java——spring中事务怎么实现的?原理是什么?
java
摸鱼小天才1 小时前
Java异常类型
java
编程修仙1 小时前
Java线程常用的方法
java·开发语言
冰零(lane)1 小时前
Spring Boot 动态数据源切换
java·数据库·spring boot
小馋喵知识杂货铺1 小时前
XSS--跨站脚本攻击
java
hummhumm1 小时前
第 38 章 -GO语言 事件驱动架构
java·javascript·后端·python·架构·golang·ruby