对于Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每一个New和delete而操心,且不容易出现内存泄漏和内存溢出问题,但是问题一旦出现那就GG了,所以最好的方式就是多少还是懂一点吧
先来了解下从古至今的JVM种类
根据《Java虚拟机ui饭(JavaSE 7版)》规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域(图片来源:网络)
1、程序计数器(线程私有)
这是一块较小的内存空间,它可以当作是当前线程执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一跳需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
因为Java虚拟机具备多线程,所以每条线程都需要一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,故称之为“线程私有”的内存。
2、Java虚拟机栈(线程私有)
Java虚拟机栈也是线程私有的,它的生命周期与线程相同。每个方法都会创建一个栈帧(用于支持虚拟机进行方法执行的数据结构,通俗地说就是一种栈结构)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直到执行的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
经常有人将Java内存区分为堆内存和栈内存,这种分法比较粗糙,Java内存区域的划分实际上远比其复杂。
(在Java虚拟机规范中,对这个区域规定了两种异常状况:若线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;若虚拟机栈在动态扩展无法申请到足够的内存,就会抛出OutOfMemoryError异常。)
3、本地方法栈(线程私有)
与虚拟机栈所发挥的作用非常相似,它们的区别不过是虚拟机栈为虚拟机执行字节码服务而本地方法栈则为虚拟机使用到的Native方法服务。
在虚拟机规范中,并未强制规定本地方法栈种方法使用的语言、方式与数据结构,故可以灵活的使用它,甚至有些虚拟机直接把本地方法栈与虚拟机栈合二为一了。
4、Java堆(线程共享)
Java堆是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,故被称为“GC堆”。在Java堆上还可以细分为:新生代和老年代。从内存分配的角度来看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(TLAB),但其存储的本质是对象实例这一点是不会改变的,这样划分的目的是为了更好的回收内存和分配内存。
Java堆可以处于物理上不连续的内存空间,只要逻辑上连续即可。
5、方法区(线程共享)
用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但它却有一个别名叫作 Non-Heap,目的应该是与Java堆区分开来。
有人更愿意将其称之为“永久代”,其实这是不对的,仅仅是HotSpot虚拟机将GC分代收集扩展至方法区,至于其他的虚拟机是不存在这个概念的。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
6、运行时常量池
它时方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池存放。
Java虚拟机堆Class文件每一部分的格式都有严格规定,每个字节用于存储那种数据都必须符合规范上的要求才会被虚拟机认可、装载和执行,但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。它是方法区的一部分,所以受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。
7、直接内存
并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中规定的内存区域,但这部分内存被频繁的使用,而且也可能会导致OutOfMemoryError异常。
在JDK1.4中新加入(New Input/Output)类,引入一种基于通道与缓冲区的I/O方式,它可以使用本地函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。故而显著提高性能,避免在Java堆和Native堆中来回复制数据。
因为不属于Java堆,但属于内存的一部分,所以若忽略掉直接内存,使得各个内存区域总和大于物理内存的限制,会导致动态扩展时出现OutOfMemoryError异常。
——————————————————————————————
学完了内存区域分布,再来看看对象是如何在内存上被创建出来(图片来源:网上)
虚拟机遇到一条New指令时,首先会去检查这个指令的参数是否能够在常量池中定位i到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程,在类加载过程检查通过后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定。
1、假设Java堆中内存绝对规整,即用过的内存站一边,空闲的内存站一边,那么只需要移动一个指针指示器即可分配对象内存,此法称为”指针碰撞“2、假设内存不是那么规整,即已经使用的内存和空闲内存相互交错,那就没有办法简单地进行指针碰撞,虚拟机必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划给对象实例,并更新列表上的记录,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
内存分配完成后,虚拟机许需要将分配到的内存空间都初始化为零值,如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。(也就是我们常说的默认初始化)
对象在内存的布局
对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对齐填充。
对象头
分为两部分信息,第一部分用于存储对象自身的运行时数据(如哈希码、GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等),第二部分是类型指针(即对象指向它的类元数据的指针)。
实例数据
HotSpot虚拟机默认的分配策略为:longs/doubles、ints、shorts/chars、bytes/booleans、oops。从分配策略中可以看出,相同宽带的字段总是被分配到一起。在满足这个前提条件下,在父类中定义的变量会出现在子类之前(父类的构造函数执行顺序就是一个例子)。
对齐填充
这个并不是必然的,仅仅起着占位符的作用。由于HotSpotVM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数,故,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
小结
以上内容参考自《深入理解JAVA虚拟机》 这是一本不可多得的好书,至少在我看来,这是我看过《汇编语言》王爽版之后的第二本国人写的奇书,值得点赞!