第2章 Java内存区域与内存溢出异常
2.1 概述
为什么要了解内存管理? 正是因为Java程序员把内存控制的权利交给了Java虚拟机,一旦出现内存泄漏和溢出方面的问题,如果不了解虚拟机是怎样使用内存的,那么排查错误将会成为一项异常艰难的工作。
内存溢出和内存泄漏的区别?
http://blog.csdn.net/buutterfly/article/details/6617375 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。 内存泄露memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。 memory leak会最终会导致out of memory!
2.2 运行时数据区域
http://blog.csdn.net/u012152619/article/details/46968883
2.2.1 程序计数器 程序计数器是干什么的? 字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令, 分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器, 各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。
简单的回答:几乎不占有内存。用于取下一条执行的指令。
2.2.2 Java虚拟机栈 栈也是线程私有的,它的生命周期与线程相同。 Java虚拟机栈是干什么的? 它为Java方法服务。虚拟机栈描述的是Java方法执行的内存模型: 每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。 每一个方法从调用到执行完成的过程,就对应着一个栈帧的虚拟机栈中入栈到出栈的过程。 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和returnAddress类型(指向了一条字节码指令的地址)。 局部变量表所需的内存空间在编译期间完成分配。
简单回答:每个线程执行每个方法的时候都会在栈中申请一个栈帧,每个栈帧包括局部变量区和操作数栈,用于存放此次方法调用过程中的临时变量、参数和中间结果。
2.2.3 本地方法栈 本地方法栈(Native Method Stack)与虚拟机栈所发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。
简单回答:用于支持native方法的执行,存储了每个native方法调用的状态。
Sun HotSpot把虚拟机栈和本地方法栈合二为一。
2.2.4 Java堆 Java堆是被所有线程共享的一块内存区域,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。 Java堆是垃圾收集器管理的主要区域。 现在收集器基本都采用分代收集算法,所以Java堆中可以细分为:新生代和老年代; 再细致一点的有Eden空间、From Survivor空间、To Survivor空间等。 进一步划分的目的是为了更好地回收内存,或者更快地分配内存。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。 新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。 新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。 Edem : from : to = 8 :1 : 1 这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。 JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块Survivor区域是空闲着的。 因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。
2.2.5 方法区 方法区在一个jvm实例的内部 方法区(Area Method)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
2.2.6 运行时常量池 运行时常量池(Runtime Constant Pool)是方法区的一部分。用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
2.2.7 直接内存
2.3 HotSpot虚拟机对象探秘 主要讲述虚拟机内存中的数据,如何创建、如何布局以及如何访问。 探讨了HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
2.3.1 对象的创建 对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。 对象创建的时候,需要考虑如何划分可用空间。 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。
2.3.2 对象内存布局 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
2.3.3 对象的访问定位 目前主流的访问方式有使用句柄和直接指针两种。
2.4 实战OutOfMemoryError异常
2.4.1 Java堆溢出
2.4.2 虚拟机栈和本地方法栈溢出
2.4.3 方法区和运行时常量池溢出
2.4.4 本机直接内存溢出
第3章 垃圾收集器与内存分配策略
3.1 概述 GC需要完成3件事: 1.哪些内存需要回收 2.什么时候回收 3.如何回收
为什么要去了解GC和内存分配呢? 当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时, 我们就需要对这些“自动化”的技术实施必要的监控和调节。
垃圾回收针对的是Java堆和方法区
3.2 对象已死吗 “死去”的对象才需要回收。如何确定一个对象是否“死去”。
3.2.1 引用计数算法 算法思路:给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1; 当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。 主流的Java虚拟机里面没有选用引用计数器算法来管理内存,其中最主要的原因是
它很难解决对象之间相互循环引用的问题。
3.2.2 可达性分析算法 主流的商用程序语言,使用这种算法。 算法思路:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
哪些对象可以作为GC Roots呢? 1.虚拟机栈(栈帧中的本地变量表)中引用的对象 2.方法区中类静态属性引用的对象 3.方法区中常量引用的对象 4.本地方法栈中JNI(即一般说的Native方法)引用的对象
总结就是,方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。
3.2.3 再谈引用
引用类型: 1.强引用(Strong Reference) 只要存在强引用,就不会回收。类似“Object obj = new Object()” 2.软引用(Soft Reference) 3.弱引用(Weak Reference) 4.虚引用(Phantom Reference)
3.2.4 生存还是死亡 即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finallize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
3.2.5 回收方法区 方法区(或者HotSpot虚拟机中的永久代)的垃圾收集主要回收两部分:废弃常量和无用的类。 回收废弃常量和回收Java堆中的对象非常类似。比如回收常量池中的字面量,字符串“abc”,当前系统没有任何一个String对象引用常量池中的“abc”常量,它就会被回收。常量池中的类(接口)、方法、字段的符号引用也与此类似。
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。“无用的类”要满足下面3个条件: 1.该类所有的实例都被回收 2.加载该类的ClassLoader已经被回收 3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.3 垃圾收集算法 介绍算法的思想和发展过程
3.3.1 标记-清除算法 算法分“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
算法有两个不足: 1.效率问题,标记和清除两个过程的效率都不高 2.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
3.3.2 复制算法
用于回收新生代 为了解决效率问题,“复制”收集算法出现。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把以使用过的内存空间一次清理掉。 代价是将内存缩小为了原来的一半。
3.3.3 标记-整理算法
用于回收老年代 思路:分成“标记”、“整理”、“清除”3个步骤 1.首先标记出所有需要回收的对象 2.让所有存活的对象都向一端移动 3.直接清理掉端边界意外的内存
3.3.4 分代收集算法 根据对象存活周期的不同将内存划分为新生代和老生代。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法, 只需要付出少量存活对象的复制成本就可以完成收集。 老年代中因为独享存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”。
3.4 HotSpot的算法实现
3.4.1 枚举根节点 当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。
3.4.2 安全点 程序运行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。 安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”的最明显特征就是指令序列服用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Sagepoint。
3.4.3 安全区域
3.5 垃圾收集器
3.6 内存分配与回收策略 Java自动内存管理就是自动化的解决两个问题: 1.给对象分配内存 2.回收分配给对象的内存
3.6.1 对象优先在Eden分配 大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。
3.6.2 大对象直接进入老年代 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制(因为,新生代采用复制算法收集内存)。
3.6.3 长期存活的对象将进入老年代 如果对象的Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。
3.6.4 动态对象年龄判断 如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
3.6.5 空间分配担保
第6章 类文件结构
6.1 概述 我们编写的程序编译成二进制本地机器码(Native Code)
6.2 无关性的基石 Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
6.3 Class类文件的结构 Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。 1.无符号数属于基本的数据类型 2.表是有多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯已“_info”结尾。 表用于描述有层次关系的复合结构的数据。
6.3.1 魔数与Class文件的版本 每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接收的Class文件。 第5和第6字节是次版本号(Minor Version) 第7和第8字节是主版本号(Major Version) 高版本的JDK能向下兼容以前版本的Class文件,不能向上兼容。
6.3.2 常量池 紧接着主次版本号之后的常量池入口。 首先,是一项u2类型的数据,代表常量池荣祥计数值(constant_pool_count)。 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References): 1.字面量:比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等 2.符号引用:属于编译原理方面的概念,包括以下3类常量: a.类和接口的全限定名(Fully Qualified Name) b.字段的名称和描述符(Descriptor) c.方法的名称和描述符 在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用 不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。
6.3.3 访问标志 这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
6.3.4 类索引、父类索引与接口索引集合 Class文件中由这三项数据来确定这个类的继承关系。 1.类索引:用于确定这个类的全限定名 2.父类索引:用于确定这个类的父类的全限定名 3.接口索引集合:用于描述这个类实现了哪些接口
6.3.5 字段表集合 字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
6.3.6 方法表集合
6.3.7 属性表集合
6.4 字节码指令简介
6.5 公有设计和私有实现
6.6 Class文件结构的发展
第7章 虚拟机类加载机制
7.1 概述
7.2 类加载的时机 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地
开始,而
解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行绑定(也称为动态绑定或晚期绑定)。 注意,这里是“开始”,而不是按部就班的“进行”或“完成”。
类需要在什么时候加载并没有严格约束。但是它必须在类的
初始化之前完成。而加载、验证、准备自然在此之前开始。 类的初始化:有且只有5中情况必须立即对类进行“初始化” 1.使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候,以及调用一个类的静态方法的时候 2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化 3.当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化 4.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类 5. 这5种场景的行为成为
对一个类进行主动引用。 其他情况成为
被动引用。被动应用不会触发初始化。
被动引用例子: 1.通过子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。 2.通过数组定义来引用某个类,不会触发这个类的初始化。 3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不糊触发定义常量的类的初始化。
7.3 类加载的过程 Class文件最终都要加载到虚拟机中之后才能运行和使用。虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化? 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用Java类型,这就是虚拟机的类加载机制。 Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。比如,如果编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类。
7.3.1 加载 在加载阶段,虚拟机完成3件事情 1.通过一个类的全限定名来获取定义此类的二进制字节流 2.将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 3.在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
7.3.2 验证 这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段大致完成4个阶段的检验动作:
1.文件格式验证 这一阶段验证字节流是否符合
Class文件格式的规范,并且能够被当前版本的虚拟机处理。
2.元数据验证 这一阶段是对类的元数据信息进行语义校验,保证不存在不符合
Java语言规范的元数据信息。比如,这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类);这个类的父类是否继承了不允许被继承的类(final类),等等。
3.字节码验证 这一阶段将对类的方法进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
4.符号引用验证 符号引用验证可以看做是对类自身以外的信息进行匹配性校验。比如,符号引用汇总通过字符串描述的全限定名是否能找到对应的类。
7.3.3 准备 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。而类变量在代码中的赋值,存放在类构造器<clinit>()方法之中,在初始化阶段才会执行。
7.3.4 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程 符号引用以一组符号来描述所引用的目标。 直接引用可是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
7.3.5 初始化 到了初始化阶段,才真正开始执行类中定义的Java程序代码。根据程序员通过程序制定的主观计划去初始化
类变量和其他资源。初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以复制,但是不能访问。
<clinit>()方法与类的构造函数不同(或者说实例构造器<init>()方法)不同,它不需要显时地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
由于父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都要阻塞等待,直到活动线程执行<clinit>()方法完毕。
7.4 类加载器
7.4.1 类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
7.4.2 双亲委派模型 绝大部分Java程序都会使用到一下3种系统提供的类加载器: 1、启动类加载器(Bootstrap ClassLoader) 负责将放在<JAVA_HOME>\lib目录中的,并且是虚拟机识别的类加载到虚拟机内存中 2、扩展类加载器(Extension ClassLoader) 负责加载<JAVA_HOME>\lib\ext目录中的类库 3、应用程序类加载器(Application ClassLoader) 也称为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。