JVM的内存区域划分,类加载过程,GC垃圾回收机制总结

发布时间:2024年01月15日

1、JVM内存区域划分

JVM(Java虚拟机):一个运行起来的Java进程,是进程,那必然就会从操作系统中申请内存。再把这些内存分区,干不同的事。分区有五种:方法区(元数据区),堆区,栈区,程序计数器,本地方法区(native)。

(1)?方法区(元数据区 1.8版本这样叫):存储的内容就是类对象编译生成的.class文件,加载到内存后,就变成类对象了。即先编译生成.class文件,JVM执行.class文件过程中,就变成类对象。

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

(3)?栈区(Stack):存储的内容就是在代码执行过程中,方法之间的调用关系。在栈里,一个一个的元素被称为栈帧,每个栈帧就代表了一个方法调用,栈帧里就包含着这个方法的入口,方法返回的位置,方法的形参,方法的返回值,局部变量等等。栈帧进行出栈入栈的操作。

(4)?程序计数器:比较小的空间,主要就是存放一个"指令地址",表示下一条要执行的指令的地址。执行的指令是在方法区里的。即就是指令是在方法区中的,而指令的地址是在程序计数器中的。

编译生成.class文件的同时,类中的成员方法都会被编译成二进制的指令,并放到.class文件中。等到JVM执行该文件时,即类加载的时候,产生类对象,二进制指令也在类对象中。

刚开始调用方法,程序计数器记录的就是方法的入口的地址。随着一条一条的执行指令,每? 执行一条,程序计数器的值都会自动更新。

(5)?本地方法区(native):指的是用native关键字修饰的方法。是用C++实现的。

一段代码中某个变量处于哪个区域?

成员变量:堆区

局部变量:栈区

静态变量:方法区(元数据区)

Test t?= new Test(); //这段代码的出现的位置,变量t肯定是一个局部变量。变量t和new出来的Test对象不是一个东西。

t这个变量里存的是new出来的对象的地址,并不是对象本身。t又作为一个局部变量,是在栈区存储的。而new出来的对象是在堆区存储的。

一个JVM进程里,可能有多个线程,每个线程,都有自己的程序计数器和栈空间,这些线程共用一份堆和方法区。


2、JVM中类加载过程

1、类加载的基本流程

JVM执行.class文件,即类加载过程。把里面的内容变成类对象并保存到内存的方法区中。基本流程如下:

  1. 加载:找到.class文件,打开文件,读取文件内容。
  2. 验证:验证.class文件的格式是否符合要求,.class文件是一个二进制的格式。JVM是由? ? ? ? ? ? ? ? ? ? ?C++写的,会验证一个类似于结构体的信息,包含版本,变量等等,看是否正确。
  3. 准备:给类对象分配内存空间。(目的是为了构造出类对象)
  4. 解析:针对类对象中包含的字符串常量(final)进行处理,进行一些初始化操作。
  5. 初始化:针对类对象进行初始化。初始化static成员,执行静态代码块。可能需要加载一下父类。

2、双亲委派模型

属于类加载的第一个步骤(找.class文件)。负责根据 “全限定类名”?找到.class文件。形如java.lang.String就叫全限定类名。双亲其实不是双亲,是翻译的问题。

这里就要提到类加载器,是JVM中的一个模块。JVM内置了三个类加载器。并非继承关系。是这几个ClassLoader里有一个parent这样的属性,指向了一个"类加载器"。大概关系如下:

BootStrap ClassLoader? ? 爷

Extension ClassLoader? ? 父

Application ClassLoader? 子

类加载的过程(找.class文件的过程)

1、给定一个类的全限定类,形如java.lang.String。

2、从Application ClassLoader作为入口,开始执行查找的逻辑。

3、Application ClassLoader不会立即去扫描自己负责的目录,而是把查找的任务向上交给他的父亲 Extension ClassLoader。父亲扫描完后,自己才负责搜索项目当前目录和第三方库对应目录

4、Extension ClassLoader也不会立即扫描自己负责的目录。也是把查找的任务向上交给他的父亲? ?BootStrap ClassLoader ,父亲扫描完后,自己才开始扫描自己负责的JDK中一些扩展的库

5、BootStrap ClassLoader 也不想立即扫描自己负责的目录。也想把任务向上交给他的父亲,结果发现自己没有父亲。于是直接自己开始扫描自己负责标准库的目录。?

6、上面都没有扫描到,就会回到Extension ClassLoader,就会开始扫描其负责的扩展库的目录。如果找到,就执行后续的类加载过程。没找到的话,就会把任务向下交给孩子来执行。

7、没有扫描到,就再回到Application ClassLoader 先扫描项目当前目录和第三方库对应目录。如果找到,就执行后续的类加载过程。没找到的话,就会抛出一个ClassNotFoundException异常。

三个类加载器执行顺序先从低往上走,再从下往上走。之所以搞这样一套流程,主要目的是为了确保标准库的类被加载的优先级最高(最先扫描),其次是扩展库,最后是自己写的类和第三方库(最后扫描)。

所谓的双亲委派模型,其实就是一个简单的"查找优先级"问题 。


3、JVM中的垃圾回收机制? GC (Garbage Collection)

这里所说的垃圾回收就是指回收释放在堆中new出来的对象。

1.找到垃圾

找到垃圾即就是如何判断一个对象是否还有效。还存活?

? ?1.可达性分析【Java】

? ?2.引用计数【Python,PHP】:new 出来的对象,会单独安排一块空间,来保存一个计数器。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?在Java中,使用对象,必须要依靠引用。如果一个对象没有引用? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?指向了,就可以视为是垃圾了。

Java中采用的是可达性分析。

使用时间换空间这样的手段。具体是有一个或一组线程,会周期性地扫描我们代码中所有的对象。从一些特定的对象出发,尽可能的遍历所有能够访问到的对象,并标记成"可达",那些在扫描之后未被标记的对象,就是垃圾了。遍历方式是树形遍历(二叉树/N叉树)。特定的出发点,比如,局部变量,常量池中引用的对象,方法区中的stack变量。 统称为GCRoots。

为啥不用引用计数?
  • 1.比较浪费内存

因为new一个对象,就得额外安排一块空间。一个计数器,应该是2字节。那么new的对象多了,计数器占据的空间就更多了。浪费内存。

  • 2.会存在"循环引用"问题

逻辑上跟死锁差不多。也就是说对象A引用对象B,对象B反过来又引用对象A,那么此时A,B对象的引用计数器都不为0,也就无法造成垃圾回收。

2.回收释放垃圾

有三种基本的思路:标记清除,复制算法,标记整理。但Java中采用的是这三种的结合。

1.标记清除:比较简单粗暴的释放方式。把对应的对象直接释放掉。缺点是会产生越来越多的内存碎片。即就是空闲的内存空间会不连续。导致后续能申请的空间会越来越小。

2.复制算法:通过复制的方式,把有效的对象归类到一起,再统一释放剩下的空间。即就是把内存分成两份,一份用来使用,一份用来复制。一次清楚完后,将使用部分中还有效的对象复制到另一半空间上(让连续在一起),这样就弥补第一种方式的缺点了。但是这种方式还是有缺点。比如内存要浪费一半,即要保证一半内存不使用,拿来专门复制。而且复制拷贝也需要开销,需要时间。要是有效的对象很多,那复制的开销就越大。

3.标记整理: 既能解决内存碎片的问题,又能处理复制算法中的问题。类似于顺序表删除元素后后面的元素往前补。但搬运的开销依然很大。

Java中回收垃圾是这样做的:

实际JVM采用并不是这三种方式的某一种。而是这三种的结合。根据经验规律,把堆区域划分出了更多更细的区域,如:新生代,幸存区(这个区域又被划分成两个,用法和第二种复制算法一样),老年代。

最开始对象都是在新生代区里,当一个对象经过一次GC扫描后,还是有效,就会被拷贝到幸存区,当在幸存区中经过GC多次扫描还是有效对象后,就会被拷贝到老年代。能转到老年代,说明这个对象生命周期还是挺长的,并且老年代中GC扫描频率会减少许多。

分代回收思想JVM中主要的回收的思想方法。

文章来源:https://blog.csdn.net/m0_73381672/article/details/135581739
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。