一、前言
经过一番思想斗争,我决定好好的学习一下JVM,而对于一个JVM的初学者《深入理解Java虚拟机》当然是必须拜读的神作,所以这个专栏暂时会记录我阅读时的笔记吧,以后有可能真正深入学习Java虚拟机后,可能会有一些自己研究的成果,不过这估计是很久以后的事情了,看过这本书的也可以接机复习一下相关的知识,没有看过书的,我尽量把我所学到的知识写的通俗易懂一些,不过还是及其推荐阅读一下《深入理解Java虚拟机》这本书,当然阅读这本书之前需要学习过计算机系统、计算机组成原理,如果没有相关的知识背景,可能会看起来很困难,这里同时推荐一本书《深入理解计算机系统》,豆瓣评分9.9的神作,对于一个非底层程序员来说,这本书就把底层所有需要知道的知识全部讲解了,最后当然是如果有错误,希望指正,我会立即更改,以免误导他人,好了那我就开始记录我的读书笔记了。
这里需要说明一下,《深入理解Java虚拟机》这本书之讲解到了JDK1.7,所以如果出现和文章不同的内容,可能是版本高于1.7的原因。
二、Java内存区域的划分
我在一年前开始学习Java的时候,马士兵的视频上就总是讲解对象是存储在堆中,引用是存放在栈上的,在看了这本书之后,发现这种想法是不准确的,Java虚拟机的内存区域分别为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。
1、程序计数器
学习过计算机系统的应该都知道程序计数器是什么东西,程序计数器用来存放计算机需要执行的下一条指令的地址,在JVM中,功能也是这样,用来存放JVM下一条虚拟机字节码指令的地址,虚拟机字节码指令就是JVM中的指令集,不过为了支持Java的多线程,每个线程都会有一个自己的程序计数器,各个线程之间互不影响,所以这部分内存是线程私有的。另外因为JVM在运行Java程序时可能会调用Native方法(本地方法,也就是当前计算机系统的API),所以如果执行的是本地方法,程序计数器的值为空。因为程序计数器只占用很小的一部分内存空间,所以并不会发生内存溢出的情况。
2、Java虚拟机栈
我们通常所说的引用存放在栈上,栈就指的是虚拟机栈,虚拟机栈同样是线程私有的,虚拟机栈的作用是控制方法的执行,我们可以想象当我们运行一个方法时,就相当于把所有运行该方法需要的数据,一股脑的打包到一起,然后压入虚拟机栈中,方法运行完成后,再出栈,虚拟机栈就是用来控制方法的执行的。而运行一个方法所需要的信息有很多,这里会把所有的信息打包后放到一个栈帧中,栈帧中主要放了:局部变量表(就是方法中定义的局部参数、还有形参)、操作数栈、动态链接、方法出口。你可能对上面的这些名词不是很懂,但是其实无所谓,现在只是有个印象,以后这些名词都会深入的讲解,现在只需要有个印象。
3、本地方法栈
本地方法栈我们一听名字就可以猜出和本地方法有关,其实和虚拟机栈类似,本地方法栈主要用来控制本地方法的运行。
4、Java堆
我们从初学Java开始,应该就会接触到Java堆,我们会知道Java中的对象在运行期间就是放在堆中的,我们从笼统的堆深入的学习一下,看一看Java堆到底是什么,但是这里也只是概览一下,后面的文章会有更详细的讲解。
Java堆是线程共享的,所有线程的对象都创建在一个Java堆中,现在随着技术的进步,可能有部分对象并不一定在堆上,但是大部分的对象都是存储在堆中。Java堆是垃圾收集器(GC)的重点关注对象,根据GC采用的收集算法,Java堆可以分为:新生代和老年代(更细致的分法再将GC的时候细说),从内存分配的角度看,线程共享的Java堆中可能会根据线程划分线程私有的分配缓冲区(Thread Local Allocation Buffer ,TLAB)其实就是给每个线程分配一块内存,避免线程中对象的冲突。当然以上的分法都不会影响堆中存放对象这个事实,只是为了更好地垃圾回收,或者更快的分配内存。我们可以通过-Xmx
和-Xms
这两个虚拟机参数来规定堆所占用内存空间的最大值和最小值。
5、方法区
现在我们已经知道了在程序运行期间,对象是放在堆中的,而方法执行所需要的数据是存放在虚拟机栈中的,调用本地方法的数据存放在本地方法栈中,那么我们程序中的类、常量、静态变量等信息存放在哪里呢?答案是方法区。
方法区是线程共享的内存区域,它用于存储虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据,在JDK1.8之前,这部分内存区域通常叫做永久代,但是在JDK1.8之后,就再也没有永久代了,这部分区域在书中被称为方法区,我另外看了一篇文章:Java永久代去哪儿了,上面叫做元空间,不过这里还是称呼为方法区。因为方法区中的数据生命周期普遍比较长,所以垃圾收集行为比较少见,主要是对常量池的回收和类的卸载。
方法区中有一部分是运行时常量池,用来存放程序运行期间的常量值。
6、直接内存
直接内存并不属于Java的内存区域,但是却和Java有关,我们在进行I/O操作的时候,可能会使用本地方法直接分配Java堆外的内存,可以提高I/O操作的性能,但是这部分内存并不属于Java堆,受限于主机的内存有限,可能会导致内存溢出。
三、初探对象的创建
在我们编写程序时,创建一个对象经常就是new一个对象,但是在虚拟机中,对象是如何创建的呢?我们来研究一下。
当虚拟机遇到一个new指令时,首先会去方法区的运行时常量池中查看是否有该类的符号引用(这里的符号引用指的是类的全限定名),如果找不到,说明没有这个类,如果有,说明有这个类,然后继续检查这个类是否已经被虚拟机加载、解析、初始化过,如果没有,就会进行这些操作,将类加载到方法区中。
在进行过上面的检查后,虚拟机会为新生的对象分配内存,在分配内存时,如果Java堆中的堆存是绝对规整的(就是用过的内存和空闲的内存是分开的,然后中间用一个指针表示分界线),那么分配内存就是把指针调整一下,把空闲的区域分出一部分来当做新对象的内存,这种分配方式称为“指针碰撞”,如果内存并不是规整的,使用过的内存和空闲的内存相互交错,那么虚拟机就需要维护一个表格来记录那些内存时使用过的,那些是空闲的,然后从空闲的内存中取出足够大的一部分作为新对象的内存空间,这种方法叫做“空闲列表”,选择哪种分配方法是由Java堆是否规整决定的,而Java堆是否规整是由垃圾收集器是否带有压缩整理功能决定的。
至于如何在分配内存的时候实现线程间的安全,一种是使用CAS配上失败重试来保证线程安全(如果不知道什么是CAS可以自己百度一下),另一种就是上面提到过的线程分配缓冲。
内存分配完成后,虚拟机将分配到的内存空间全部初始化为零值,接下来,虚拟机对对象进行必要的设置,将对象所属哪个类,对象的哈希码、对象的GC分代年龄等信息存放在对象的对象头中,、至此为止,虚拟机层面的对象创建完成,一个新的对象已经产生了。
四、对象的内存布局
我们现在来学习一下对象在内存中的储存布局,可以分为三个区域:对象头、实例数据、对齐填充。
1、对象头
对象头中储存了两部分信息,第一部分存储了对象自身的运行时数据,比如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。另一部分就是该对象的类型指针,用于确定该对象是哪个类的实例,如果该对象是一个数组,还会包含数组的长度信息。
2、实例数据
就是该对象在程序代码中所定义的各种类型的字段内容(包括继承自父类的),注意对象的方法并不存储在这里,方法存储在虚拟机栈中。
3、对齐填空
就是为了确保对象的内存大小必须是8字节的整数倍。
五、对象的访问定位
对象的访问定位主要包含两种:
1、句柄
Java堆中将会划分出一块内存来作为句柄池,reference中 存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信 息,如下图所示。
20170709144433534.jpg
2、直接指针
使用直接指针访问,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址,如下图所示。
20170709144642209.jpg
网友评论