来源:《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)》
作者:周志明
运行时数据区域:
程序计数器:
较小的内存空间,当前线程所执行的字节码的行号指示器。作用:程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都依赖计数器;
Java虚拟机栈:
线程私有,生命周期与线程相同。作用:存储局部变量表、操作数栈、动态连接、方法出口等信息。 ( HotSpot虚拟机的栈容量是不可以动态扩展的,以前的Classic虚拟机倒是可以。所以在HotSpot虚拟 机上是不会由于虚拟机栈无法扩展而导致OutOfMemoryError异常——只要线程申请栈空间成功了就不 会有OOM,但是如果申请时就失败,仍然是会出现OOM异常的???)
本地方法栈:
为虚拟机使用到的本地方法服务。(作用:类似于Java虚拟机栈,具体虚拟机可以根据需要自由实现它)
Java堆:
被所有线程共享的一块内存区域,虚拟机启动时创建。作用:存放对象实例。所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如 何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。
方法区:
各个线程共享的内存区域,作用:存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。JDK8以前被称为 永久代,JDK8后被称为 元空间。运行时常量池:存放编译期生成的各种字面量与符号引用。(运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入Class文件中常量池的内容才能进入方法区运行时常 量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是String类的 intern()方法。)
直接内存:
非虚拟机运行时数据区的一部分,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存大小以及处理器寻址空间的限制。
对象:(HotSpot虚拟机为例)
对象创建:先去检查这个指令的参数是否能在常量池中定位到 一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那 必须先执行相应的类加载过程。在类加载检查通过后,接下来虚拟机将为新生对象分配内存。分配内存方法:“指针碰撞”、”空闲列表“;
对象内存布局:
划分为三部分:对象头(Header)、实例 数据(Instance Data)和对齐填充(Padding)。
对象头部分:包括两类信息。第一类是用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部 分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32个比特和64个比特,官方称它 为“Mark Word”。
第二部分:是类型指针,即对象指向它的类型元数据的指针,Java虚拟机通过这个指针 来确定该对象是哪个类的实例(并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话 说,查找对象的元数据信息并不一定要经过对象本身,)。
实例数据:对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。这部分的存储顺序会 受到虚拟机分配策略参数(-XX:FieldsAllocationStyle参数)和字段在Java源码中定义顺序的影响。 HotSpot虚拟机默认的分配顺序为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),从以上默认的分配策略中可以看到,相同宽度的字段总是被分配到一起存 放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作 用。(对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。)
对象访问:
Java程序会通过栈上的reference数据来操作堆上的具 体对象。reference:一个指向对象的引用,未定义通过什么方式定位和访问堆中对象具体位置。主流访问方式:使用句柄和直接指针。
垃圾收集器与内存分配策略:
垃圾收集器:主要处理的就是 Java堆和方法区的内存区域。
垃圾回收算法:
垃圾收集器在对堆进行回收前,第一件事情就 是要确定这些对象之中哪些还“存活”着,哪些已经“死去”(“死去”即不可能再被任何途径使用的对 象)了。
引用计数算法:
在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可 能再被使用的。
缺陷:当对象objA和objB都有字段instance,赋值令 objA.instance=objB及objB.instance=objA,除此之外,这两个对象再无任何引用时。实际上这两个对象已 经不可能再被访问,但是它们因为互相引用着对方,导致它们的引用计数都不为零,引用计数算法也 就无法回收它们。
可达性分析算法:
基本思路:通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过 程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。(为目前主流的垃圾回收算法)
在Java技术体系里面,固定可作为GC Roots的对象包括以下几种:
·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。
· ·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
··在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。 ·
·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 ·
·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 ·
·所有被同步锁(synchronized关键字)持有的对象。 ·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
引用:
强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回 收掉被引用的对象。
·软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内 存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存, 才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。
·弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只 能生存到下一次垃圾收集发生为止。当垃圾收集器