JVM之内存区域划分、类加载和垃圾回收

  • Post author:
  • Post category:其他





前言

程序在执行之前,需要通过javac将java代码编译为字节码(class文件),jvm则需要把字节码通过一定的方式进行类加载器把文件加载到运行时数据区,再通过特定的执行引擎将字节码翻译成底层系统指令交给CPU执行。java程序是一个名为java的进程,这个进程就是jvm,jvm是java运行的基础,也是实现一次编译到处执行的基础,所以深入了解JVM的运行时数据区、类加载过程以及垃圾回收有助于我们理解JVM执行过程。




一、JVM内存区域划分

jdk1.7以前可将内存区域划分为如下模块:

(1)程序计数器:放的是下一个要执行的指令的地址。

(2)方法区:放的是类对象。(加载好的类、静态变量)

(3)栈:放的是方法之间的调用关系。(局部变量)

(4)堆:放的是new的对象。(成员变量)


可参考之前写过的一篇文章。


jdk1.8以后可将内存区域划分为如下模块:

去掉方法区,加了一个元数据区。

之前方法区是在JVM申请到的这一块内存里划分了个区域;而元数据区是用本地内存(JVM内部,C++代码里搞的内存)

例:

public class Test {
	private int x = 0;
	private static int y = 10;
	public static void main(String[] args) {
		Test t = new Test();
	}
}

t是局部变量在栈上;

x是成员变量在堆上;

y是静态变量在方法区上。


变量在哪个部分,和变量类型无关,和变量的形态有关。



二、类加载



1.类加载是什么?

java程序在运行之前,需要先编译,将.java 文件编译为.class文件,运行的时候,JVM就会读取对应的.class文件,并解析内容,在内存中构造出类对象并进行初始化。这里的类对象(反射、Jackson、synchronized都有用到)描述了这个类有哪些属性、方法、继承哪个父类、实现哪个接口,同时类对象也是创建实例的具体依据。



2.类加载的过程

根据官方文档,可分为以下几步:

在这里插入图片描述

(1)加载:找到.class文件,读取文件内容,并且按照.class规范的格式来解析。

连接:

(2)验证:检查当前的.class里的内容格式是否符合要求。(.class的具体格式

官方文档

会有明确描述)

在这里插入图片描述

初始化:

(3)准备:给类里的静态变量分配内存空间。

(4)解析:初始化字符串常量,把符号引用替换成直接引用。

(3)初始化:针对类进行初始化,初始化静态成员,执行静态代码块,并且加载父类等等。



3.何时触发类加载?

使用到一个类的时候,就触发加载。

(1)创建这个类的实例;

(2)使用了类的静态方法/静态属性;

(3)使用类的子类(加载子类会触发加载父类)。



4.双亲委派模型

JVM加载类,是由类加载器(class loader)进行负责的,JVM自带了多个类加载器,各自负责各自的片区,如下所示,当然也可以自己实现。

(1)Bootstrap ClassLoader:负责加载标准库中的类。

(2)Extension ClassLoader:负责加载JVM扩展的类。

(3)Application ClassLoader:负责加载自己的项目里的自定义类。

描述上述类加载器相互配合的过程,就是双亲委派模型。

在这里插入图片描述

a.这三个类加载器存在父子关系;

b.进行类加载的时候,输入的内容,全限定类名,例如:java.lang.Thread;

c.加载的时候,从Application ClassLoader开始;

d.某个类加载器开始加载的时候,不会立即扫描自己负责的路径,而是先把任务委派给父“类加载器”来先进行处理;

e.找到最上面的Bootstrap ClassLoader,再往上没有父类加载器了,只能自己手动加载了;

f.如果父亲没找到类,就交给自己的儿子,继续加载;

g.如果一直找到最下面的Application ClassLoader也没有找到类,就会抛出一个“类没找到”的异常,即类加载失败。

按照这样的顺序加载,最大的好处在与,如果自己写个类,正好与标准库中的类冲突,此时仍然保证加载可以加载到标准库中的类,防止代码加载错了带来问题。



三、垃圾回收(GC)



1.GC是什么?

对于申请的内存,手动释放,最大的问题在于,容易忘记,就会发生内存泄漏,为了解决这个问题,程序员也想出来了一些方案,其中GC就是一个主流的方案,Java、Python、Js、Go、PHP…都有用,只需要负责申请内存,释放内存的工作交给JVM来完成,JVM会自动判定当前的内存是啥时候需要释放,认为这个内存不再使用了,就自动释放了。

GC中最大的问题是STW(Stop The World)问题。



2.GC回收哪部分内容?

GC主要针对堆来回收。

在这里插入图片描述


注意:一定要保证,内存不再使用才能回收。


GC中回收内存,不是以“字节”为单位,而是以“对象”为单位。



3.怎么回收?

(1)先找出垃圾;

(2)再回收垃圾(释放内存)。



(1)怎么判定对象是否是垃圾

如果一个对象也不再用了,就说明是垃圾。主要是通过引用来判定当前对象是否还能被使用,没有引用指向就视为是无法被使用的。



判定对象是否存在引用的办法:



1.引用计数(不是JVM采取的办法,Python、PHP等有用)

每次多一个引用指向该对象,计数器就+1,每次少一个引用执行该对象,计数器就-1。

当引用数值为0,则说明该对象无人使用,此时就可以释放了。

优点:简单,容易实现,执行效率也较高。

缺点:空间利用率低,尤其是小对象;可能会出现循环引用的情况。

例如:以下代码就会出现循环引用,内存无法被释放的情况。

class Test {
	Test x;
}
Test a = new Test();
Test b = new Test();
a.x = b;
b.x = a;
a = null;
b = null;



2.可达性分析(是JVM采取的办法)

约定一些特定的变量,成为“GC roots”,每隔一段时间,从GC roots出发,进行遍历,看看当前哪些变量是能够被访问到的,能被访问到的变量就成为“可达”,否则就是“不可达”。

GC roots:(1)栈上的变量;(2)常量值引用的对象;(3)方法区,引用类型的静态变量。



(2)具体怎么回收垃圾



1.标记清除

标记处垃圾之后,直接把对象对应的内存空间进行释放。

这种方式最大的问题:内存碎片。



2.复制算法

针对上面所说的内存碎片问题,来进行引入的办法。

(1)申请两倍内存,使用左侧时,右侧不用;使用右侧时,左侧不用;

(2)将“非垃圾”拷贝到另外一侧;

(3)再将之前的这一半整个释放。

缺点:

(1)空间利用率低;

(2)如果一轮GC下来,大部分对象要保留,只有少数对象要回收,这时候复制开销就很大。



3.标记整理

这个方法就是针对以上两种方法的缺点提出的办法,类似于顺序表删除元素,搬运操作。

(1)先标记垃圾;

(2)再将“非垃圾”进行搬运;

(3)释放垃圾。

这个方式,相对于上述的复制算法来说,空间利用率提高了,同时也解决了内存碎片问题,但是搬运操作也是比较耗时的。



4.引入“分代回收”

上述三个方式,都有缺点,我们需要根据实际的场景,因地制宜的解决问题。

分代回收,就是综合以上方法,根据对象不同的特点,采取不同的回收方法。

针对对象的年龄进行分类,把堆里的对象分成了新生代和老年代。

新生代GC扫描的频率更高;老年代GC扫描的频率降低。

在这里插入图片描述

(1)刚创建出来的新对象,进入伊甸区;

(2)进入伊甸区的对象,如果熬过一轮GC还存在,就通过复制算法,复制到生存区中;

(3)生存区的对象,也要经历GC的考验,每熬过一轮GC,通过复制算法拷贝到另一个生存区,只要这个对象不消亡就会在两个生存区中间来回拷贝;

(4)如果一个对象在生存区中经历了很多轮,还存在,就会把它放到老年代。

(5)对象来到老年代,定期进行GC的频率更低了,这里采取标记整理的方法来处理老年代对象。




版权声明:本文为qq_45283185原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。