探索JVM内存对象——从生到死

来源:互联网 发布:硬盘磁头坏了恢复数据 编辑:程序博客网 时间:2024/05/21 22:52

前阵子复习了一遍《深入理解Java虚拟机》前几章内容,重新梳理了一遍JVM中内存对象从创建,到GC回收的过程。

1 JVM的内存区域划分

谈到内存对象的生命历程,首先复习一下JVM的内存是一个怎样的状态。
没仔细学习JVM的内存时候,我的印象都是JVM内存分为“堆”和“栈”,其中对象实例在“堆”上;对象引用,类,方法相关信息放在“栈”上。(这让我想起了之前有人问我,http中“get”和“post”方法的区别,我想到的只有一个获取,一个提交数据····,若感兴趣可以移步到(经典)http中get和post方法的区别),直到仔细看了《深入理解Java虚拟机》并且整理了下学习笔记之后,发现其实JVM的内存比我之前所知的复杂很多。
下面讲正题,JVM对内存划分以下几个区域:

1.1 程序计数器

程序计数器是内存中比较小的一块空间,我理解的作用就是对当前线程执行的指令的一个“指示器”,比如记录当前执行哪一条指令。
值得一提的是,Java的多线程是通过线程之间轮流切换执行来实现的,因此每个线程都有独立的程序计数器,因此,这部分的内存是线程私有的。
最后说一点的是,程序计数器的内存区域是JVM内存中唯一一个没有规定OutOfMemoryError的区域。

1.2 Java虚拟机栈

这里的栈,就约等于我们常说“堆栈”里面的“栈”。
虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的时候会产生一个栈帧,这个栈帧存储局部变量表,操作数栈,动态链接,方法出口。方法从开始调用到执行完毕,就是栈帧出栈入栈的过程。
在Java虚拟机栈中,有一部分必要重要的区域,就是局变量表,存放着各种基本数据类型(booleanbytecharshortintfloatlongdouble,),对象引用(reference类型)。

1.3 本地方法栈

本地方法栈和Java虚拟栈原理上相似,本地方法栈是针对Native方法的,有的虚拟机还可能将两者合二为一,在这不细说。

1.4 Java堆

重头戏来了~
Java堆是作为程序员最关心的一块区域,这块区域主要是存放对象的实例。也是Java虚拟机所管理的内存中最大的一块区域。
平时我们说的对象回收,也就是主要讲Java堆。现在的回收方法一般采用的是分代收集方法,因此可以对Java堆进行划分:新生代老年代。如果再细分,还可以对新生代进行进一步划分:Eden区Survivor区

1.5 方法区

方法区用于存放被虚拟机加载的类信息常量静态变量,比如public final string str = "abc";就存到方法区中。
有些人说这块区域不会发生GC,因此这是区分方法区和Java堆的一个方法,有的时候方法区也叫永久代,但不是说这块区域不发生GC,回收主要是针对常量池回收和类型的卸载
必须要提的是,在方法区中有一块叫运行时常量池。他的作用如下图
常量池加载到运行时常量池

1.6 直接内存

这块内存不属于虚拟机数据区的一部分,也不属于JVM定义的数据内存,但是这块区域和Java中的NIO(New Input/Output)类有直接的关系,NIO类可以使用Native函数库直接分配堆外内存,然后通过Java堆中的DirectByteBuffer对象作为这块直接内存的引用进行操作。
个人的简单理解就是,直接内存在某种场合下,是Java堆Native堆的沟通桥梁,从Java层面看,利用NIO类来分配直接内存的空间,使用DirectByteBuffer对象对直接内存操作。

小结一下

Java内存的划分还是挺精细的,不是简简单单的“堆”和“栈”,下图是对Java内存划分的一个示例图,本人平时少用Visio,画的不是很好看:(
Java内存区域划分


复习完了JVM的内存是怎么一个情况之后,进入正题了:一个对象在JVM中从生到死是一个怎样的过程?

2 对象怎么“生”

从简单的例子来说,一般程序中创建一个对象用Object obj = new Object();方法。
对象创建
从图中可以看到,对象的创建大概分为了:执行new指令——检查对象是否加载——分配内存——内存对象初始化——栈reference引用,这几个步骤。
在这里有几个地方需要强调一下,首先是内存分配这一步骤,里面涉及比较复杂的操作。

2.1 内存分配

创建一个对象实例,需要在内存中划分一块区域作为存放该对象实例数据,怎么划分,划分的时候需要注意什么问题,在JVM中都是做了很多考虑的。

2.1.1 内存分配原则

内存分配遵循以下三个原则:
内存分配原则

2.1.2 内存分配的方法

内存分配方法有两种:指针碰撞空闲列表
内存分配方法

2.1.3 内存分配时线程安全问题

在分配内存的同时,存在线程安全的问题,即虚拟机给A线程分配内存过程中,指针未修改,B线程可能同时使用了同样一块内存。
在JVM中有两种解决办法:

  • 同步处理,即CAS(compare & swap)搭配失败重试的方式
  • 将内存分配的动作按线程分配到不同空间中,每个线程都有一块内存,成为本地线程分配缓冲。
    内存分配的问题大概就这些,还需要探讨的是,Java栈如何引用堆中的实例对象呢?

2.2 栈引用堆对象

主要有两种办法:句柄直接指针
栈引用堆对象
两种方法都要自己的优缺点。

方法 优点 句柄 对象被移动时,只需修改句柄的地址,无需修改reference本身 直接指针 访问速度快,节省了一次指针定位的开销

3 对象怎么“死”

首先声明一点,这里说的对象,是主要针对Java堆中的对象实例,GC对回收对象之前,首先要做的就是对Java堆中的对象进行分类:哪些是“活”的,哪些是“死”的。

第一步

首先判断哪些对象是“死”的。
系统判断对象是活是死
Java对判断对象是否已死主要采用可达性分析方法,无论是引用计数法,还是主流的可达性分析法,都涉及到对象引用的问题。所以,在这里有必要复习一个概念:对象引用

3.1 对象引用

对象引用分为:强引用,软引用,弱引用,虚引用。
对应的解释如下图
对象引用

3.2 可达性分析法

既然主流是可达性分析法,那么也看看该方法的大体实现步骤有哪些咯~
可达性分析

3.3 清理垃圾

要清理的对象已经确定了,接下来就是对需要清理的对象,执行“死刑”。
在这里,主要讲两个方面:清理垃圾的方法清理垃圾的工具,即算法和回收器。
清理对象

以上就是我看了《深入理解Java虚拟机》中关于JVM中内存对象如何创建,和如何被清除的过程,以上是我对该过程的自身理解,如果不对的地方,请大神们指正。
同时,墙裂安利《深入理解Java虚拟机》这本书,讲的很深入。

原创粉丝点击