Java基础之详细理解回收机制

来源:互联网 发布:网络直播策划 编辑:程序博客网 时间:2024/06/06 10:40

在以前从事C/C++开发的时候,内存的管理一直是需要被谨慎考虑的内容。在C语言中,我们使用库函数malloc()和free()两个库函数来实现从堆中分配内存与释放,而C++则使用操作符new和delete来实现内存的管理,对于这两个方式,后者是操作符而前者是库函数,后者能够被编译器处理而前者着重于对内部数据实现构造,在面向对象设计中,后者能更好的结合构造函数对自定义对象实现内存分配。但是,在接触了Java之后,我们在内存的管理上可以轻松许多,关键是Java实现了内存的自动管理模式,具体是怎么样的呢?


一、JVM的内存模型

要了解Java是如何进行内存回收的,有必要先了解JVM内部的内存模型:
先来看第一张图:

这张图基本描述了一个JVM内的运行线程所涉及的5个部分。因为JVM是基于多线程机制的并行计算模型,每个线程有自己独立的内存运行空间,因此线程与线程之间不进行干扰,另一方面虚拟机内存的数据又对所有线程进行共享。是不是有点晕了?先来逐个了解每个组成部分:

(1)、程序计数器

程序计数器是用来指向当前的运行线程A在运行指令时,该指令的字节码的位置或行号。在JVM执行多线程并行时,本质是多个线程之间的轮转机制,因为一个CPU同时只能执行一条指令,为了最大化利用CPU的运行资源,于是使用了线程的轮转,当一条线程执行处于闲置状态时,为了防止该线程阻塞后续的线程请求,该线程就会被撤下而替换别的线程,这样可以使得CPU一直在执行从而提高效率。那么当线程A被撤下时,后面肯定还会重新切换上线程A继续执行,该如何得知之前线程被撤下前执行到什么位置了呢?程序计数器就是为了实现这个功能的,它保存了当前执行的字节码在线程内存中的偏移量,从而实现了良好的后续执行。从这里可以得知,每个线程有自己独立的程序计数器内存空间用来指向自己的执行过程,所以这对线程而言是私有的。
如果一个线程执行的是Java方法,那么程序计数器指向的是虚拟机字节码的指令地址(行号),如果执行的是本地方法,则计数器为空。

(2)、Java虚拟机栈
Java虚拟机栈描述的是线程中Java方法执行时的内存模型:每个Java方法在线程内被执行时都会创建一个栈帧,这个栈帧存储局部变量表、操作栈等信息。其中最关键的是局部变量表,该表存储了在执行该Java方法时编译期可知的基本数据类型(short、int、long、byte、char、double、float、boolean)、对象引用类型等。局部变量表的大小在编译期就已经确定了,执行的时候不会修改该表的大小。需要注意的是该表也属于线程私有的。

(3)、本地方法栈
既然Java虚拟机栈是为线程中的Java方法服务的,那么本地方法栈就是为线程中执行的本地方法服务的,其存储了本地方法使用是需要用的相关基本数据和其他信息。

(4)、Java堆
Java堆是JVM需要维护的最大内存区域。它是对所有的线程进行共享的内存区域。在该区域中,几乎负责了所有的对象实例的内存空间分配以及数组的空间分配。Java堆的JVM进行内存回收的主战场。当前主流的内存回收算法分代算法也是主要在该区域进行内存划分的。该区域主要负责各个对象实例的分配。

(5)、方法区
方法区主要用于存储虚拟机加载时加载的类信息、常量数据、静态变量、以及编译器编译后的相关方法的代码。它也是被所有的线程共享的区域。就好比一个类的静态变量在类加载时被加载,并对类共享。这个静态变量或静态方法就存储在方法区中被各个线程需要时调用。
在方法区中有一个主要构成部分是常量池。该池子里存储的是编译器生成的各种字面量和符号引用。

说完了以上5个部分,我们再来看下面一张图就容易理解了:

二、JVM的内存划分

在了解了以上JVM的内存模型之后,可以对JVM的内存进行有效的划分。这里的划分主要是对Java堆进行划分。因为该部分区域是GC回收的主要战场。在进行内存回收时,Java堆主要划分出2个世代,方法区(非堆)划分出一个世代。这个划分方法是基于垃圾回收的分代回收机制。堆中划分的两个世代分别是Young Generation和Old Generation
而在Young Generation中,又被划分出3个区域,分别是Eden(伊甸园..这名字我也是醉了)、From Suvivor和To Survivor。Eden当中主要是用来给新建立的对象实例开辟内存空间用的,而剩下的两个区域From Survivor和To survivor大小一样,主要是用来存储进行一次垃圾回收之后剩下的对象。

在Old Genneration当中主要用来存储存活了比较久的对象。

而在方法区,也就是非堆区,会划分出Permanent Generation,目的是存储一些类信息、静态字段等数据,这些数据会随着类被虚拟机开始加载而存在。基本不参与垃圾回收。

三、JVM的垃圾回收任务及原理

在上面,了解了JVM在Java堆和非堆中进行的内存划分,了解了各个区域下划分的子区域及对应的存储数据。现在就可以介绍JVM是如何执行内存回收的:
对象实例的内存分配主要在Young Generation->Eden当中,该区域是一块连续的空闲的内存区域。因此在该区域进行内存的分配非常快速,因为不需要进行可用内存区域的查找。而在Young Generation->From Survivor和To Generation中,这两个存活区始终有一个是空的,那么在进行垃圾回收时,JVM通过算法查找出Eden当中不活跃的对象实例,然后将活跃的对象实例复制到其中的一个空白的存活区中,而另一个存活去存储了上一次垃圾回收时存储的对象实例,对该区域进行搜索将活跃的对象也复制到那个空白的存活去中,这样,其中的存活区就存储了活跃对象实例,接下来把剩下的两个区域Eden和其中一个Survivor置空即可。如此一来,Eden又空了,就又可以进行内存开辟了,另一个存活区清空了,可以为下次垃圾回收提供活跃对象的存储场所。
在这个过程中有一个问题,随着垃圾回收的不断执行,存活区内的对象实例越来越多,那么该怎么办呢?Java垃圾回收机制会对该存活区内的对象进行算法统计,将存活时间偏长的对象复制到Old Generation当中,用来释放存活区的存储空间。
以上就是几个存储区域的作用。

上面描述的是Young Generation当中的内存回收机制,那么对于Old Generation和Permannent Generation中的对象,该如何进行内存回收呢?采用的回收算法是另一种:
称之为“标记->清除->压缩”。
标记值得是标记活跃的对象,清除是回收存活过久的对象,压缩是将内存进行压缩,是所有的对象保存在一端,留下另一端空白的内存区域方便开辟新的对象空间。这样一来降低了内存碎片,提高了内存的利用率。




四、JVM的垃圾回收方式

JVM提供了3种垃圾回收方式,分别是串行GC、并行回收GC、并行GC。
(1)、串行GC
指的是在整个扫描和复制过程中采用单线程的方式来进行,适用于单CPU、新生代空间较小及对暂停时间要求不是很高的应用。该回收方法在回收时程序会被暂停,分代回收就是用的串行GC方式。
(2)、并行回收GC
在执行过程中采用多线程并行的方式来执行。
(3)、标记回收。
主要针对用于Old Generation当中的回收机制。

以上就是我对JVM垃圾回收的资料总结和自己的理解,希望对大家有所帮助。
0 0