第1章 走近Java
我们可以把Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK(Java development Kit),JDK是用于支持Java程序开发的最小环境。
可以把Java API子集和Java虚拟机这两部分统称为JRE(Java Runtime Environment)
第2章 Java内存区域与内存溢出异常
2.2 运行时数据区域
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。
2.2.1 程序计数器
程序计数器(Program Counter Register)是一块较小的内存空间,它可以 看作是当前线程所执行的字节码的行号指示器。
为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。
此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
2.2.2 Java虚拟机栈
虚拟机栈为虚拟机执行的Java方法(也就是字节码)服务。
Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Strack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型)和returnAddress类型(指向了一条字节码指令)。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据只占用1个。
局部变量表所需的内存空间在编译期间完成分配
在Java虚拟机规范中,对这个内存区域规定了两种异常情况:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展(当前大部分的Java虚拟机栈都可动态扩展,只不过Java虚拟机规范也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常。
2.2.3 本地方法栈
线程私有。
本地方法栈和虚拟机栈所发挥的作用非常相似,它们的区别不过是:本地方法栈为Java虚拟机使用到的Native方法服务, 虚拟机栈为虚拟机执行的Java方法(也就是字节码)服务。
有的虚拟机(譬如Sun HotSpot虚拟机)直接把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常。
2.2.4 Java堆
Java堆是Java虚拟机管理内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
此内存区域唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
所有的对象实例和数组都在堆上分配——>渐渐变得不是那么绝对。
Java堆是垃圾收集器管理的主要内存区域,因此也称“GC堆”(Garbage Collected Heap),现在收集器基本都采用分代收集算法,所以Java堆还 可分为:新生代和老年代;再细致一点有Eden空间、From Survivor空间、To Survivor空间等。
无论哪个区域,存储的都是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续的即可,就像我们的磁盘空间一样。
实现时可固定也可扩展,当前主流虚拟机都是按可扩展来实现的(通过-Xmx和-Xms来控制)。
如果在堆中没有内存完成实例分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
2.2.5 方法区
线程共享。
它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java虚拟机规范把方法区描述为堆的一个部分,但是它却有一个别名叫做Non-Heap(非堆)
习惯在HotSpot开发的许多开发者,把方法区称为“永久代”(Permanment Generation),本质上二者并不等价。
不需要连续的内存和可以选择固定大小或者可扩展,还可选择不实现垃圾回收。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError。
2.2.6 运行时常量池
运行时常量池(Runtime Constant Pool)是方法区的一部分。
class文件有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池(Constant Pool Table)。
当常量池无法再申请到内存时会抛出OutOfMemoryError。
2.3 HotSpot虚拟机对象探秘
2.3.1 对象的创建
虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
“指针对撞”(Bump the pointer)分配方式; 假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲空间挪动一段与对象大小相等的距离。
空闲列表(Free List)分配方式:如果java堆并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
确保对象创建在并发情况下是线程安全的方法:(1)对分配内存空间的动作进行同步出理->实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。(2)把内存的分配动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB。
对象的对象头信息()
2.3.2 对象的内存布局
对象在内存中存储的布局可分为3块区域:1.对象头(Object Header)2.实例数据(Instance Data)3.对齐填充
HotSpot虚拟机的对象头包括两部分信息:(1)用于存储对象自身运行时数据,如哈希吗(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
(2)另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。查找对象的元数据信息并不一定要经过对象本身。
实例数据部分是对象存储的有效信息。
对齐填充并不是必然存在,没有特别含义,仅仅起着占位符的作用。
2.3.3 对象的访问定位
对象访问的方式:1.使用句柄 2.直接指针
HotSpot使用直接指针的方式。
使用句柄的特点:reference存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身并不需要修改。
直接指针:速度更快,节省了一次指针定位的时间开销。
堆最小值:-Xms
堆最大值:-Xmx
第3章 垃圾收集器与内存分配策略
3.1概述
垃圾收集(Garbage Collection,GC)。
程序计数器、Java虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,内存自然跟着回收了,因此不需要过多考虑内存回收问题,Java堆和方法区却不一样。
3.2.1 引用计数法
给对象中添加一个引用计算器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器值为0的对象就是不可能再被使用的。
它很难解决对象之间相互循环引用的问题。
3.2.2 可达性分析算法(Reachability Analysis)
通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明对象是不可用的。
在Java语言中,可作为GC Roots的对象包括下面几种:
-
虚拟机栈(栈帧中的本地变量表)中引用的对象
-
方法区静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中JNI(即一般说的Netive方法)引用的对象
3.2.3 再谈引用
引用定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。
引用包括:
-
强引用(Strong Reference)
-
软引用(Soft Reference)
-
弱引用(Weak Reference)
-
虚引用(Phantom Reference)
强引用:“Object obj=new Object()”;只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象(被强引用的对象不会被回收)
//TODO