JVM运行时数据区域和垃圾回收

学习JVM对大多人来说,对大多人来说学习曲线都是比较陡的,但是其实也是可以顺藤摸瓜从基础学起,比如从内存分配以及垃圾回收学起,而这一部分,首先涉及到运行时数据区域的概念,这一篇文章先从运行时数据区域开始介绍,然后介绍内存分配和垃圾回收。让我们开始吧!

一,运行时数据区域

Java虚拟机在执行Java程序的过程中,会把它管理的内存划分为5个数据区域,先直接上图

search screenshot

我们可以看到右边三个数据区域是属于线程隔离的数据区,随着用户线程启动和结束而建立和销毁。也就是这一部分的内存分配和回收都是会比较准确地执行。而相比之下,左边的线程隔离数据区,则就没那么准确了,虽然也是会自动回收垃圾,但是需要GC(垃圾回收器)进行一些考量和计算。到这里注意到没有,我们平时讲的垃圾回收,其实不是针对所有内存区域的,而是只是针对左边的线程隔离数据区,它们包括方法区。接下来从这两个数据区域讲起。

1.1 由所有线程共享的数据区域

这个区域包括方法区

1.1.1 Java堆

这个区域主要存储对象实例和数组,为了弄清楚对象实例的概念,我们先看看下面这行代码

Object object = new Object();

此处其实产生了两个对象,一个是引用对象,一个是实例对象,前者就是此处的object,后者就是此处new Object()创建的对象,引用对象记录着实例对象的起始地址。而存储位置二者也不同,实例对象存储在堆上,引用对象存储在栈上(关于栈后续会讲解)。

我们平时说的垃圾回收主要针对的就是堆,所有有时也称堆为GC堆

1.1.2 方法区

方法区用于存储类信息,常量,静态变量,即时编译器编译后的代码,这个区域是出了堆之外的另外一个需要垃圾回收的区域,这个区域的垃圾回收主要是针对常量池和类型的卸载,这个区域虽然也有垃圾回收,但是回收的条件很苛刻,尤其是类型的卸载。

关于常量池,这里做个简单的介绍,介绍一下字符串常量池

常量池

String具有不可变性,因为String类时final类型,不可以被修改,那么为什么string需要被设置为final呢?

JVM在内存中留出一块特殊的内存区域,成为“String常量池”,当编译器创建一个字面值的字符串时,比如String s = “abc”,可以检验池内是否已经存在相同的String字面值,如果找到,则直接让引用指向它,如果没有则创建一个新的字符串再指向它,所以,会有一种情况,多个引用指向同一个常量池中的字面值字符串,任何一个引用改变字符串其他引用页得改变,所以,就得让String具有不可变性。

然后,我们需要分清以下这个概念,关于字符串,内存中有两个不同的部分,一个是字符串常量池,一个是堆。

假设目前字符串常量池中没有“abc”,那么

String s = "abc"    //将创建一个对象,一个引用
String s = new String("abc")  //将创建两个对象,一个引用
String s = new String("abc").intern()  //将创建一个对象,一个引用

第一行代码只创建了一个字符串对象放在常量池中,但是第二行代码为什么就创建了两个对象呢?

因为使用了new关键字,所以Java将在堆中创建一个新的String对象,并且引用变量s将引用它。此外,字面值“abc”将被放入常量池,所以也就有了两个对象,一个在堆中,一个在String常量池。

另外,关于intern方法,它只创建一个常量池中的对象。

到此为止,我们介绍完了方法区这两个线程共享的数据区域,接下来开始介绍线程隔离数据区域。

1.2 线程隔离数据区域

一共有三个线程隔离数据区域,它们分别是Java虚拟机栈,本地方法栈,程序计数器

1.2.1 Java虚拟机栈

Java虚拟机栈其实就是我们平时说的栈,我们前面举过的例子,

Object object = new Object();

此处建立的引用变量object就存储在栈中。

Java虚拟机栈描述了Java方法执行的内存模型,每个方法被执行的时候会同时创建一个栈帧,此处栈帧的概念不用细纠,此处只要知道它是描述方法的相关信息的实体即可。

Java虚拟机栈的栈帧存储了各个方法的局部变量表、操作数栈,动态链接,方法出口等信息。每个方法开始执行和结束,对应着栈帧的进栈和出栈。

顾名思义,局部变量表就是存储方法中的局部变量,那么它会存储什么局部变量呢?

局部变量表存放了编译期可知的各种基本数据类型,boolean、byte、char、short、int、float、long、double,以及引用类型(reference),引用类型的概念不同于对象本身,我们可以理解为它是对象的名字或者起始地址。

1.2.2 本地方法栈

本地方法栈和Java虚拟机栈的概念比较相似,后者是针对Java方法,前者是针对native方法,我们一般只需要了解到这里即可。

1.2.3 程序计数器

学习过汇编语言的人应该都接触过程序计数器,由于在java中我们对它接触甚少,此处就只做一个简单的介绍。

程序计数器可以记录当前线程所执行的字节吗的行号,每个线程都有一个程序计数器,这也使得线程切换后能正确恢复现场。

二、 关于new一个对象的过程

以上我们介绍了5个数据区域,三个是线程隔离的,它们有:Java虚拟机栈,本地方法栈,程序计数器。另外还有两个数据区域是线程共享的,这两个区域是垃圾回收发生的地方,它们分别是:堆,方法区。

从上面的介绍我们可以发现,其中程序计数器和本地方法栈两个概念我们只需要知道皮毛即可,平时经常接触的是堆,方法区,Java虚拟机栈,我们接下来举一个例子来囊括这三个知识点,还是上文出现过的例子,如下:

Object object = new Object();

这一行简单的代码涉及了堆,方法区,Java虚拟机栈三个数据区域,

Object object这部分的语义映射到Java虚拟机栈的本地变量表,做为一个reference引用类型存在。

new Object()这一部分映射到堆中,在堆中描述该对象的数据,另外还需要在堆中还需要保存能查找到该对象类型数据的地址,这些类型数据就是保存方法区中。

方法区保存对象的对象类型,父类,实现的接口等信息。

三、对象访问

以上介绍完了各个运行时数据区域的特点和应用场景,接下来介绍一个基本每天都会接触的概念,就是引用类型reference。引用主要用于指向堆中的对象实例的起始地址,其实现方式一般有两种:句柄和直接指针。

search screenshot
句柄
search screenshot
直接指针

这两种方式各有优劣,句柄的好处是当对象由于垃圾回收被移动时,只需要修改句柄中的实例数据指针,而reference本身存储的事稳定的地址,不需要修改。

直接指针的优点我们很容易看出来,它比句柄方式少了一次指针定位的时间,由于对象访问是很频繁的操作,所以这其中节约的指针定位时间是很可观的。

目前主流的虚拟机,部分使用了句柄访问对象,也有部分使用直接指针访问对象。

四、 垃圾回收

垃圾回收,英文叫Garbage Collection, GC,其实垃圾回收在Java语言之前就出现了,最早在Lisp语言中使用。其实垃圾回收是一个自动化的过程,那么学习它的意义何在呢?当遇到内存泄漏的问题时,具备一定垃圾回收的知识将对问题排查起到很大的作用,在Android开发中使用MAT时就会感受到。

4.1 对象死的证据

既然Java堆中存在垃圾回收,那么JVM是根据什么条件判定一个对象是需要被回收的呢?主要有两种方式,引用计数法和根搜索法。

4.1.1 引用计数法

引用计数法存在一个弊端:循环引用。如果对象a和对象b相互持有对方的引用,那么这种情况双方都很难被列入垃圾回收的范围。

4.1.2 根搜索法

根搜索法的思路是通过一系列名为GC Root,没有引用链到达任何GC Root的对象,则证明此对象是不可用的,这些对象将被列入回收的范围。

4.2 引用的扩展概念

JDK1.2开始,Java对引用的概念进行了扩充,将引用分为以下四种:

  • 强引用:Strong Reference,强引用就是我们常见的类似”Object object = new Object()”这类引用,只要强引用存在,永远不会被回收。

  • 软引用:Soft Reference,软引用描述有用但非必需的对象,软引用只有在即将发生内存溢出前,才会被回收,

  • 弱引用:Weak Reference,弱引用关联的对象,只能存活到下一次垃圾回收,下次垃圾到来时,无论内存是否足够,弱引用关联的对象都会被回收。

  • 虚引用:Phantom Reference,虚引用暂时很少用到,主要用于对象被收集器回收时能收到一个通知。