前阵子复习了一遍《深入理解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虚拟机栈中,有一部分必要重要的区域,就是局变量表,存放着各种基本数据类型(boolean
,byte
,char
,short
,int
,float
,long
,double
,),对象引用(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,画的不是很好看:(
复习完了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虚拟机》这本书,讲的很深入。