学习Java也有一段时间了,总感受有些东西学的不是很精通。例如Java内存区域究竟是怎么样的?程序是怎么跑的?对象是怎么存放的?这些都影响了我对本身的程序运行的熟悉程度。java
Java虚拟机在执行java程序的过程当中,会把它所管理的内存划分红若干个不一样的数据区域(每当运行一个java程序都会启动一个虚拟机)。有一本书叫作《Java虚拟机规范》,讲述了Sun公司对Java虚拟机
实现的相关规范,其中讲了虚拟机将所管理的内存分为如下几个部分:数组
程序计数器
虚拟机栈数据结构
本地方法区
堆
方法区多线程
其中方法区和堆是由全部线程共享的,例如使用ThreadPoolExecutor
建立多个线程时,堆与方法区均可以被多个线程读取。布局
程序计数器
学过计算机组成原理的人都会知道在CPU的寄存器中有一个PC寄存器,存放下一条指令地址,这里,虚拟机不使用CPU的程序计数器,本身在内存中设立一片区域来模拟CPU的程序计数器。只有一个程序计数器是不够的,当多个线程切换执行时,那就单个程序计数器就没办法了,虚拟机规范中指出,每一条线程都有一个独立的程序计数器。注意,Java虚拟机中的程序计数器指向正在执行的字节码地址,而不是下一条。学习
虚拟机栈
是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每一个方法执行的时候都会建立一个栈帧(我以为能够把它看做是一个快照,记录下进入方法前的一些参数,其实是方法运行时的基础数据结构),用于存放局部变量表,操做数栈,动态连接,方法出口等信息。每个方法从调用直到执行完成的过程都对应着一个栈帧在虚拟机中的入栈到出栈的过程。咱们平时把内存分为堆内存和栈内存,其中的栈内存就指的是虚拟机栈的局部变量表部分。局部变量表存放了编译期能够知道的基本数据类型,对象引用,和返回后所指向的字节码的地址。spa
本地方法区
与 虚拟机栈
所发挥的做用很相似,可是要注意一下,虚拟机规范中没有对本地方法区中的方法做强制规定,虚拟机能够自由实现,便可以不是字节码。可是也能够是字节码,这样虚拟机栈和本地方法区就能够合二为一,事实上,OpenJDK
和SunJDK
所自带的HotSpot虚拟机
就直接将虚拟机栈和本地方法区合二为一。操作系统
堆
这个概念应该不少人都很熟悉,例如初学C语言的时候,老师就会讲malloc方法会在堆中分配空间,这里也同样。这个区域是用来存放对象实例的,几乎全部对象实例都会在这里分配内存,虚拟机规范中讲:全部对象的实例以及数组都要在堆上分配。可是随着JIT(Just-in-time) 编译期的发展,有些时候也有可能在栈上分配(这里我也不是很明白其中的道理)。堆是java垃圾收集器管理的主要区域(不少时候会称为GC堆,不叫垃圾堆),垃圾收集器实现了对象的自动销毁。线程
方法区
也是各个线程共享的区域,它用于存储已经被虚拟机加载过的类信息,常量,静态变量,及时编译期编译后的代码(类方法)等数据。这里要讲一下运行时常量池,它是方法区的一部分,用于存放编译期生成的各类字面量和符号引用(其实就是八大基本类型的包装类型和String类型数据)。3d
最后还有一个直接内存,在JDK1.4版本中加入了NIO类,引入了基于通道(Channel)与缓冲区(Buffer)的I/O方式,也就是说经过这种方式,不会在运行时数据区域分配内存,这样就避免了在运行时数据区域来回复制数据,直接调用外部内存。
对于面向对象的一门语言,咱们无时不在经过new关键字建立对象,那么这个过程又是怎样的呢?
当虚拟机遇到一条new指令的时候,首先会去检查所new的类是否已经被加载,在哪里检查?固然在方法区,方法区存放了加载过的类信息。若是没有加载,那么先执行类的加载。
经过类加载检查后,虚拟机开始为新生对象分配内存,对象所须要的内存大小在类加载完成后已经能够肯定,这时候只要在堆中分配空间便可。分配内存有两种方式,第一种,咱们假设内存绝对规整,那么只要在用过的内存和没用过的内存间放置一个指针便可,每次分配空间的时候只要把指针向空闲空间移动相应距离便可。第二种,咱们假设空闲内存和非空闲内存夹杂在一块儿,实际上就是这种状况,那么就须要一个列表,去记录堆内存的使用状况,操做系统对内存的管理就是这样的。
那么,咱们还要考虑一个问题,即在多线程的状况下,只有一个指针怎么能确保一个线程分配了内存指针没修改的时候另外一个线程又分配内存不会覆盖以前的内存呢?这里有一种方法,让每个线程在堆中先预分配一小块内存(TLAB
本地线程分配缓冲),每一个线程只在本身的内存中分配内存。
最后,对象被成功分配内存。咱们知道经过一个对象,咱们能够经过getClass()方法获取类,默认比较两个对象实际比较的是对象内存的哈希值,这又是怎么实现的呢?其实在分配完内存后,虚拟机会对对象进行必要的设置,对象的类,对象的哈希码等信息都存放在对象的对象头中,因此分配的内存大小毫不止属性的总和。
对象在堆中的布局分为三个区域:对象头,实例数据,对齐填充。
对象头 包括两个部分,第一部分用于存储自身运行时的数据例如GC标志位,MonirGC次数,哈希码,锁状态,哪一个线程能够拥有等被称为MarkWord
(标记字)。第二部分存放指向方法区类数据的指针。在32位系统中,class指针大小为4字节,标记字大小为4字节。在64位系统中标记字大小为8字节。
实例数据 存放类的属性信息,包括父类的属性信息。数组的实例部分还包括数组的长度。实例信息按类分别4字节对齐。
对齐填充 这是虚拟机要求对象起始地址必须是8字节的整数倍,能够说对齐填充没有什么特别的含义。
咱们知道,引用是引用,对象实例是对象实例。引用存放在虚拟机栈中,数据类型为reference,对象实例存放在堆中。那么引用是如何指向对象实例的呢?
主流的访问方式有两种,第一种是经过句柄池
,若是使用句柄池,那么java堆
中将会划分出一部份内存做为句柄池,句柄包含对象类型指针指向方法区的类型信息,还有对象实例指针,指向堆中的实例地址。
第二种是reference引用直接指向堆中的对象实例,对象实例的对象头存放对象类型指针。
两种方法各有优点,第一种能够在对象实例在GC
时移动的时候只改变句柄池中的对象实例指针,而不用改变reference引用自己。第二种方法就是访问速度快,减小了一次指针定位的时间开销。目前HotSpot虚拟机
就采用的第二种方式。
了解java内存区域是对java的深刻学习,之前只知道有堆和栈的区分,如今咱们了解到了具体的堆栈的做用。内存是怎么划分的,对象是怎么存储的,方法和属性的存放区别。经过对这些内容的了解,会让咱们写java程序更加游刃有余,有的放矢。
更多文章:http://blog.gavinzh.com