深入jvm 讲得比较清楚

http://www.jianshu.com/p/759e02c1feee


深入理解 JVM
1 Java 技术与 Java 虚拟机 说起 Java,人们首先想到的是 Java 编程语言,然而事实上,Java 是一种技术,它由四方面组成: Java 编 程语言、Java 类文件格式、Java 虚拟机和 Java 应用程序接口(Java API)。它们的关系如下图所示:

图1

Java 四个方面的关系

运行期环境代表着 Java 平台,开发人员编写 Java 代码(.java 文件),然后将之编译成字节码(.class 文件)。 最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有 选择的转换成机器码执行。从上图也可以看出 Java 平台由 Java 虚拟机和 Java 应用程序接口搭建,Java 语言则是进入这个平台的通道, Java 语言编写并编译的程序可以运行在这个平台上。 用 这个平台的结构如 下图所示:

在 Java 平台的结构中, 可以看出,Java 虚拟机(JVM) 处在核心的位置,是程序与底层操作系统和硬件无 关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和 Java 操作系统, 其中依赖于平台的部 分称为适配器;JVM 通过移植接口在具体的平台和操作系统上实现;在 JVM 的上方是 Java 的基本类库 和扩展类库以及它们的 API, 利用 Java API 编写的应用程序(application) 和小程序(Java applet) 可以在 任何 Java 平台上运行而无需考虑底层平台, 就是因为有 Java 虚拟机(JVM)实现了程序与操作系统的分离, 从而实现了 Java 的平台无关性。 那么到底什么是 Java 虚拟机(JVM)呢?通常我们谈论 JVM 时,我们的意思可能是: 1. 2. 3. 对 JVM 规范的的比较抽象的说明; 对 JVM 的具体实现; 在程序运行期间所生成的一个 JVM 实例。

对 JVM 规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》 (《Java 虚拟机规范》)中被详细地描述了;对 JVM 的具体实现要么是软件,要么是软件和硬件的组合, 它已经被许多生产厂商所实现,并存在于多种平台之上;运行 Java 程序的任务由 JVM 的运行期实例单个 承担。在本文中我们所讨论的 Java 虚拟机(JVM)主要针对第三种情况而言。它可以被看成一个想象中的机 器,在实际的计算机上通过软件模拟来实现,有自己想象中的硬件,如处理器、堆栈、寄存器等,还有自 己相应的指令系统。 JVM 在它的生存周期中有一个明确的任务,那就是运行 Java 程序,因此当 Java 程序启动的时候,就产生 JVM 的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面我们从 JVM 的体系结构和它的运 行过程这两个方面来对它进行比较深入的研究。 2 Java 虚拟机的体系结构 刚才已经提到,JVM 可以由不同的厂商来实现。由于厂商的不同必然导致 JVM 在实现上的一些不同,然 而 JVM 还是可以实现跨平台的特性,这就要归功于设计 JVM 时的体系结构了。 我们知道,一个 JVM 实例的行为不光是它自己的事,还涉及到它的子系统、存储区域、数据类型和指令这 些部分,它们描述了 JVM 的一个抽象的内部体系结构,其目的不光规定实现 JVM 时它内部的体系结构, 更重要的是提供了一种方式,用于严格定义实现时的外部行为。每个 JVM 都有两种机制,一个是装载具有 合适名称的类(类或是接口),叫做类装载子系统;另外的一个负责执行包含在已装载的类或接口中的指令, 叫做运行引擎。每个 JVM 又包括方法区、堆、 Java 栈、程序计数器和本地方法栈这五个部分,这几个部 分和类装载机制与运行引擎机制一起组成的体系结构图为:

图3

JVM 的体系结构

JVM 的每个实例都有一个它自己的方法域和一个堆,运行于 JVM 内的所有的线程都共享这些区域;当虚 拟机装载类文件的时候,它解析其中的二进制数据所包含的类信息,并把它们放到方法域中;当程序运行 的时候,JVM 把程序初始化的所有对象置于堆上;而每个线程创建的时候,都会拥有自己的程序计数器和 Java 栈,其中程序计数器中的值指向下一条即将被执行的指令,线程的 Java 栈则存储为该线程调用 Java 方法的状态;本地方法调用的状态被存储在本地方法栈,该方法栈依赖于具体的实现。 下面分别对这几个部分进行说明。 执行引擎处于 JVM 的核心位置,在 Java 虚拟机规范中,它的行为是由指令集所决定的。尽管对于每条指 令, 规范很详细地说明了当 JVM 执行字节码遇到指令时, 它的实现应该做什么, 但对于怎么做却言之甚少。 Java 虚拟机支持大约 248 个字节码。每个字节码执行一种基本的 CPU 运算,例如,把一个整数加到寄存器, 子程序转移等。Java 指令集相当于 Java 程序的汇编语言。 Java 指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有 0 个或多个操作数,提供操作所 需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。 虚拟机的内层循环的执行过程如下: do{ 取一个操作符字节; 根据操作符的值执行一个动作; }while(程序未结束) 由于指令系统的简单性,使得虚拟机执行的过程十分简单,从而有利于提高执行的效率。 指令中操作数的数量 和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。例如,一个 16 位 的参数存放时占用两个字节,其值为: 第一个字节*256+第二个字节字节码。 指令流一般只是字节对齐的。指令 tableswitch 和 lookup 是例外,在这两条指令内部要求强制的 4 字节边界 对齐。 对于本地方法接口,实现 JVM 并不要求一定要有它的支持,甚至可以完全没有。Sun 公司实现 Java 本地 接口(JNI)是出于可移植性的考虑,当然我们也可以设计出其它的本地接口来代替 Sun 公司的 JNI。但是这 些设计与实现是比较复杂的事情,需要确保垃圾回收器不会将那些正在被本地方法调用的对象释放掉。 Java 的堆是一个运行时数据区,类的实例(对象)从中分配空间, 它的管理是由垃圾回收来负责的:不给程序员 显式释放对象的能力。Java 不规定具体使用的垃圾回收算法,可以根据系统的需求使用各种各样的算法。

Java 方法区与传统语言中的编译后代码或是 Unix 进程中的正文段类似。 它保存方法代码(编译后的 java 代 码)和符号表。在当前的 Java 实现中,方法代码不包括在垃圾回收堆中,但计划在将来的版本中实现。每个类 文件包含了一个 Java 类或一个 Java 界面的编译后的代码。可以说类文件是 Java 语言的执行代码文件。 为了保证类文件的平台无关性,Java 虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考 Sun 公司的 Java 虚拟机规范。 Java 虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似。Java 虚拟机的寄存 器有四种: 1. 2. 3. 4. pc: Java 程序计数器; optop: 指向操作数栈顶端的指针; frame: 指向当前执行方法的执行环境的指针;。 vars: 指向当前执行方法的局部变量区第一个变量的指针。

在上述体系结构图中,我们所说的是第一种,即程序计数器,每个线程一旦被创建就拥有了自己的程序计 数器。当线程执行 Java 方法的时候,它包含该线程正在被执行的指令的地址。但是若线程执行的是一个本 地的方法,那么程序计数器的值就不会被定义。 Java 虚拟机的栈有三个区域:局部变量区、运行环境区、操作数区。 局部变量区 每个 Java 方法使用一个固定大小的局部变量集。它们按照与 vars 寄存器的字偏移量来寻址。局部变量都 是 32 位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址。(例 如,一个具有索引 n 的局部变量,如果是一个双精度浮点数,那么它实际占据了索引 n 和 n+1 所代表的存储空 间)虚拟机规范并不要求在局部变量中的 64 位的值是 64 位对齐的。 虚拟机提供了把局部变量中的值装载到 操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。 运行环境区 在运行环境中包含的信息用于动态链接,正常的方法返回以及异常捕捉。 动态链接 运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法的 class 文件代码在引用要调用的方法和要访问的变量时使用符号。 动态链接把符号形式的方法调用翻译成实 际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相 应的偏移地址。动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。 正常的方法返回 如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。 执行 环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过 已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。 异常捕捉 异常情况在 Java 中被称作 Error(错误)或 Exception(异常),是 Throwable 类的子类,在程序中的原因是:①动 态链接错,如无法找到所需的 class 文件。②运行时错,如对一个空指针的引用。程序使用了 throw 语句。 当异常发生时,Java 虚拟机采取如下措施:

? ?

检查与当前方法相联系的 catch 子句表。每个 catch 子句包含其有效指令范围,能够处理的异常类 型,以及处理异常的代码块地址。 与异常相匹配的 catch 子句应该符合下面的条件:造成异常的指令在其指令范围之内,发生的异常 类型是其能处理的异常类型的子类型。如果找到了匹配的 catch 子句,那么系统转移到指定的异常处 理块处执行;如果没有找到异常处理块,重复寻找匹配的 catch 子句的过程,直到当前方法的所有嵌套的 catch 子句都被检查过。

?

由于虚拟机从第一个匹配的 catch 子句处继续执行,所以 catch 子句表中的顺序是很重要的。因为 Java 代码是结构化的,因此总可以把某个方法的所有的异常处理器都按序排列到一个表中,对任意可 能的程序计数器的值,都可以用线性的顺序找到合适的异常处理块,以处理在该程序计数器值下发生的 异常情况。

?

如果找不到匹配的 catch 子句,那么当前方法得到一个"未截获异常"的结果并返回到当前方法的调 用者,好像异常刚刚在其调用者中发生一样。如果在调用者中仍然没有找到相应的异常处理块,那么这 种错误将被传播下去。如果错误被传播到最顶层,那么系统将调用一个缺省的异常处理块。

操作数栈区 机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少 量寄存器或非通用寄存器的机器(如 Intel486)上,也能够高效地模拟虚拟机的行为。 操作数栈是 32 位的。 它 用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd 指令将两个 整数相加。相加的两个整数应该是操作数栈顶的两个字。这两个字是由先前的指令压进堆栈的。这两个整 数将从堆栈弹出、相加,并把结果压回到操作数栈中。 每个原始数据类型都有专门的指令对它们进行必须的操作。 每个操作数在栈中需要一个存储位置,除了 long 和 double 型,它们需要两个位置。 操作数只能被适用于其类型的操作符所操作。 例如,压入两个 int 类型的数, 如果把它们当作是一个 long 类型的数则是非法的。在 Sun 的虚拟机实现中,这个限制由字节码验证器强制 实行。但是,有少数操作(操作符 dupe 和 swap),用于对运行时数据区进行操作时是不考虑类型的。 本地方法栈,当一个线程调用本地方法时,它就不再受到虚拟机关于结构和安全限制方面的约束,它既可 以访问虚拟机的运行期数据区,也可以使用本地处理器以及任何类型的栈。例如,本地栈是一个 C 语言的 栈,那么当 C 程序调用 C 函数时,函数的参数以某种顺序被压入栈,结果则返回给调用函数。在实现 Java 虚拟机时,本地方法接口使用的是 C 语言的模型栈,那么它的本地方法栈的调度与使用则完全与 C 语言的 栈相同。 3 Java 虚拟机的运行过程 上面对虚拟机的各个部分进行了比较详细的说明,下面通过一个具体的例子来分析它的运行过程。 虚拟机通过调用某个指定类的方法 main 启动,传递给 main 一个字符串数组参数,使指定的类被装载,同 时链接该类所使用的其它的类型,并且初始化它们。例如对于程序: class HelloApp { public static void main(String[] args) { System.out.println("Hello World!"); for (int i = 0; i < args.length; i++ ) { System.out.println(args[i]); } } } 编译后在命令行模式下键入: java HelloApp run virtual machine 将通过调用 HelloApp 的方法 main 来启动 java 虚拟机,传递给 main 一个包含三个字符串"run"、"virtual"、 "machine"的数组。现在我们略述虚拟机在执行 HelloApp 时可能采取的步骤。 开始试图执行类 HelloApp 的 main 方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进 制代表, 于是虚拟机使用 ClassLoader 试图寻找这样的二进制代表。 如果这个进程失败, 则抛出一个异常。 类被装载后同时在 main 方法被调用之前,必须对类 HelloApp 与其它类型进行链接然后初始化。链接包含 三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及

把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。 类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的 父类必须被初始化。整个过程如下:

图 4:虚拟机的运行过程 4 结束语 本文通过对 JVM 的体系结构的深入研究以及一个 Java 程序执行时虚拟机的运行过程的详细分析,意在剖 析清楚 Java 虚拟机的机理。

慢慢琢磨 JVM
1 JVM 简介
JVM 是我们 Javaer 的最基本功底了, 刚开始学 Java 的时候, 一般都是从“Hello World”开始的, 然后会写个复杂点 class,然后再找一些开源框架,比如 Spring,Hibernate 等等,再然后就 开发企业级的应用,比如网站、企业内部应用、实时交易系统等等,直到某一天突然发现做

的系统咋就这么慢呢,而且时不时还来个内存溢出什么的,今天是交易系统报了 StackOverflowError,明天是网站系统报了个 OutOfMemoryError,这种错误又很难重现,只 有分析 Javacore 和 dump 文件,运气好点还能分析出个结果,运行遭的点,就直接去庙里烧 香吧!每天接客户的电话都是战战兢兢的,生怕再出什么幺蛾子了。我想 Java 做的久一点 的都有这样的经历,那这些问题的最终根结是在哪呢?—— JVM。 JVM 全称是 Java Virtual Machine,Java 虚拟机,也就是在计算机上再虚拟一个计算机,这和 我们使用 VMWare 不一样,那个虚拟的东西你是可以看到的,这个 JVM 你是看不到的,它 存在内存中。我们知道计算机的基本构成是:运算器、控制器、存储器、输入和输出设备, 那这个 JVM 也是有这成套的元素, 运算器是当然是交给硬件 CPU 还处理了, 只是为了适应“一 次编译,随处运行”的情况,需要做一个翻译动作,于是就用了 JVM 自己的命令集,这与汇 编的命令集有点类似,每一种汇编命令集针对一个系列的 CPU,比如 8086 系列的汇编也是 可以用在 8088 上的,但是就不能跑在 8051 上,而 JVM 的命令集则是可以到处运行的,因 为 JVM 做了翻译,根据不同的 CPU,翻译成不同的机器语言。 JVM 中我们最需要深入理解的就是它的存储部分,存储?硬盘?NO,NO, JVM 是一个内存 中的虚拟机,那它的存储就是内存了,我们写的所有类、常量、变量、方法都在内存中,这 决定着我们程序运行的是否健壮、是否高效,接下来的部分就是重点介绍之。

2 JVM 的组成部分
我们先把 JVM 这个虚拟机画出来,如下图所示:

从这个图中可以看到,JVM 是运行在操作系统之上的,它与硬件没有直接的交互。我们再来 看下 JVM 有哪些组成部分,如下图所示:

该图参考了网上广为流传的 JVM 构成图,大家看这个图,整个 JVM 分为四部分: ???Class Loader 类加载器 类加载器的作用是加载类文件到内存,比如编写一个 HelloWord.java 程序,然后通过 javac 编译成 class 文件,那怎么才能加载到内存中被执行呢?Class Loader 承担的就是这个责任, 那不可能随便建立一个.class 文件就能被加载的,Class Loader 加载的 class 文件是有格式要 求,在《JVM Specification》中式这样定义 Class 文件的结构:
ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; }

需要详细了解的话,可以仔细阅读《JVM Specification》的第四章“The class File Format”,这 里不再详细说明。 友情提示:Class Loader 只管加载,只要符合文件结构就加载,至于说能不能运行,则不是 它负责的,那是由 Execution Engine 负责的。 ???Execution Engine 执行引擎 执行引擎也叫做解释器(Interpreter),负责解释命令,提交操作系统执行。 ???Native Interface 本地接口 本地接口的作用是融合不同的编程语言为 Java 所用,它的初衷是融合 C/C++程序,Java 诞生 的时候是 C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用 C/C++程序,于是 就在内存中专门开辟了一块区域处理标记为 native 的代码,它的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载 native libraies。目前该方法使用的 是越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机,或者 Java 系统 管理生产设备,在企业级应用中已经比较少见,因为现在的异构领域间的通信很发达,比如 可以使用 Socket 通信,也可以使用 Web Service 等等,不多做介绍。 ???Runtime data area 运行数据区 运行数据区是整个 JVM 的重点。我们所有写的程序都被加载到这里,之后才开始运行,Java 生态系统如此的繁荣,得益于该区域的优良自治,下一章节详细介绍之。 整个 JVM 框架由加载器加载文件,然后执行器在内存中处理数据,需要与异构系统交互是 可以通过本地接口进行,瞧,一个完整的系统诞生了!

2 JVM 的内存管理
所有的数据和程序都是在运行数据区存放,它包括以下几部分: ???Stack 栈 栈也叫栈内存,是 Java 程序的运行区,是在线程创建时创建,它的生命期是跟随线程的生 命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈 就 Over。问题出来了:栈中存的是那些数据呢?又什么是格式呢? 栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集, 是一个有关方法(Method)和运行期数据的数据集, 当一个方法 A 被调用时就产生了一个栈帧 F1,并被压入到栈中,A 方法又调用了 B 方法,于是产生栈帧 F2 也被压入栈,执行完毕后, 先弹出 F2 栈帧,再弹出 F1 栈帧,遵循“先进后出”原则。 那栈帧中到底存在着什么数据呢?栈帧中主要保存 3 类数据:本地变量(Local Variables) , 包括输入参数和输出参数以及方法内的变量;栈操作(Operand Stack) ,记录出栈、入栈的 操作;栈帧数据(Frame Data) ,包括类文件、方法等等。光说比较枯燥,我们画个图来理 解一下 Java 栈,如下图所示:

图示在一个栈中有两个栈帧,栈帧 2 是最先被调用的方法,先入栈,然后方法 2 又调用了

方法 1,栈帧 1 处于栈顶的位置,栈帧 2 处于栈底,执行完毕后,依次弹出栈帧 1 和栈帧 2, 线程结束,栈释放。 ???Heap 堆内存 一个 JVM 实例只存在一个堆类存,堆内存的大小是可以调节的。类加载器读取了类文件后, 需要把类、方法、常变量放到堆内存中,以方便执行器执行,堆内存分为三部分: Permanent Space 永久存储区 永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class,Interface 的元数据,也 就是说它存储的是运行环境必须的类信息, 被装载进此区域的数据是不会被垃圾回收器回收 掉的,关闭 JVM 才会释放此区域所占用的内存。 Young Generation Space 新生区 新生区是类的诞生、 成长、 消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集, 结束生命。新生区又分为两部分:伊甸区(Eden space)和幸存者区(Survivor pace) ,所有 的类都是在伊甸区被 new 出来的。幸存区有两个: 0 区(Survivor 0 space)和 1 区(Survivor 1 space) 。当伊甸园的空间用完时,程序又需要创建对象,JVM 的垃圾回收器将对伊甸园区 进行垃圾回收, 将伊甸园区中的不再被其他对象所引用的对象进行销毁。 然后将伊甸园中的 剩余对象移动到幸存 0 区。若幸存 0 区也满了,再对该区进行垃圾回收,然后移动到 1 区。 那如果 1 区也满了呢?再移动到养老区。 Tenure generation space 养老区 养老区用于保存从新生区筛选出来的 JAVA 对象,一般池对象都在这个区域活跃。 三个区 的示意图如下:

???Method Area 方法区 方法区是被所有线程共享, 该区域保存所有字段和方法字节码, 以及一些特殊方法如构造函 数,接口代码也在此定义。 ???PC Register 程序计数器 每个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读 取下一条指令。

???Native Method Stack 本地方法栈

3 JVM 相关问题
问:堆和栈有什么区别 答:堆是存放对象的,但是对象内的临时变量是存在栈内存中,如例子中的 methodVar 是在 运行期存放到栈中的。 栈是跟随线程的,有线程就有栈,堆是跟随 JVM 的,有 JVM 就有堆内存。 问:堆内存中到底存在着什么东西? 答:对象,包括对象变量以及对象方法。 问:类变量和实例变量有什么区别? 答:静态变量是类变量,非静态变量是实例变量,直白的说,有 static 修饰的变量是静态变 量,没有 static 修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。 问:我听说类变量是在 JVM 启动时就初始化好的,和你这说的不同呀! 答:那你是道听途说,信我的,没错。 问:Java 的方法(函数)到底是传值还是传址? 答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的 地址。对于原始数据类型,JVM 的处理方法是从 Method Area 或 Heap 中拷贝到 Stack,然后 运行 frame 中的方法,运行完毕后再把变量指拷贝回去。 问:为什么会产生 OutOfMemory 产生? 答:一句话:Heap 内存中没有足够的可用内存了。这句话要好好理解,不是说 Heap 没有内 存了,是说新申请内存的对象大于 Heap 空闲内存,比如现在 Heap 还空闲 1M,但是新申请 的内存需要 1.1M,于是就会报 OutOfMemory 了,可能以后的对象申请的内存都只要 0.9M, 于是就只出现一次 OutOfMemory,GC 也正常了,看起来像偶发事件,就是这么回事。 但 如果此时 GC 没有回收就会产生挂起情况,系统不响应了。 问:我产生的对象不多呀,为什么还会产生 OutOfMemory? 答:你继承层次忒多了,Heap 中 产生的对象是先产生 父类,然后才产生子类,明白不? 问:OutOfMemory 错误分几种? 答 :分两 种,分 别是 “OutOfMemoryError:java heap size” 和”OutOfMemoryError: PermGen space”,两种都是内存溢出,heap size 是说申请不到新的内存了,这个很常见,检查应用或 调整堆内存大小。 “PermGen space”是因为永久存储区满了,这个也很常见,一般在热发布的环境中出现,是 因为每次发布应用系统都不重启, 久而久之永久存储区中的死对象太多导致新对象无法申请 内存,一般重新启动一下即可。

问:为什么会产生 StackOverflowError? 答:因为一个线程把 Stack 内存全部耗尽了,一般是递归函数造成的。 问:一个机器上可以看多个 JVM 吗?JVM 之间可以互访吗? 答:可以多个 JVM,只要机器承受得了。JVM 之间是不可以互访,你不能在 A-JVM 中访问 B-JVM 的 Heap 内存,这是不可能的。在以前老版本的 JVM 中,会出现 A-JVM Crack 后影响 到 B-JVM,现在版本非常少见。 问:为什么 Java 要采用垃圾回收机制,而不采用 C/C++的显式内存管理? 答:为了简单,内存管理不是每个程序员都能折腾好的。 问:为什么你没有详细介绍垃圾回收机制? 答:垃圾回收机制每个 JVM 都不同,JVM Specification 只是定义了要自动释放内存,也就是 说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不同,算法各异,这东西实在 没必要深入。 问:JVM 中到底哪些区域是共享的?哪些是私有的? 答:Heap 和 Method Area 是共享的,其他都是私有的, 问:什么是 JIT,你怎么没说? 答:JIT 是指 Just In Time,有的文档把 JIT 作为 JVM 的一个部件来介绍,有的是作为执行引 擎的一部分来介绍,这都能理解。Java 刚诞生的时候是一个解释性语言,别嘘,即使编译成 了字节码(byte code)也是针对 JVM 的,它需要再次翻译成原生代码(native code)才能被机 器执行,于是效率的担忧就提出来了。Sun 为了解决该问题提出了一套新的机制,好,你想 编译成原生代码,没问题,我在 JVM 上提供一个工具,把字节码编译成原生码,下次你来 访问的时候直接访问原生码就成了,于是 JIT 就诞生了,就这么回事。 问:JVM 还有哪些部分是你没有提到的? 答:JVM 是一个异常复杂的东西,写一本砖头书都不为过,还有几个要说明的: 常量池(constant pool) :按照顺序存放程序中的常量,并且进行索引编号的区域。比如 int i =100,这个 100 就放在常量池中。 安全管理器(Security Manager) :提供 Java 运行期的安全控制,防止恶意攻击,比如指定读 取文件,写入文件权限,网络访问,创建进程等等,Class Loader 在 Security Manager 认证通 过后才能加载 class 文件的。 方法索引表(Methods table) ,记录的是每个 method 的地址信息,Stack 和 Heap 中的地址 指针其实是指向 Methods table 地址。 问:为什么不建议在程序中显式的生命 System.gc()? 答:因为显式声明是做堆内存全扫描,也就是 Full GC,是需要停止所有的活动的(Stop The World Collection) ,你的应用能承受这个吗? 问:JVM 有哪些调整参数? 答:非常多,自己去找,堆内存、栈内存的大小都可以定义,甚至是堆内存的三个部分、新 生代的各个比例都能调整。

JVM 内存管理:深入 Java 内存区域与 OOM
Java 与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进 去,墙里面的人却想出来。

概述:
对于从事 C、C++程序开发的开发人员来说,在内存管理领域,他们即是拥有最高权力 的皇帝又是执行最基础工作的劳动人民——拥有每一个对象的“所有权”,又担负着每一个对 象生命开始到终结的维护责任。 对于 Java 程序员来说,不需要在为每一个 new 操作去写配对的 delete/free,不容易出 现内容泄漏和内存溢出错误, 看起来由 JVM 管理内存一切都很美好。 不过, 也正是因为 Java 程序员把内存控制的权力交给了 JVM,一旦出现泄漏和溢出,如果不了解 JVM 是怎样使用 内存的,那排查错误将会是一件非常困难的事情。

VM 运行时数据区域
JVM 执行 Java 程序的过程中,会使用到各种数据区域,这些区域有各自的用途、创建 和销毁时间。根据《Java 虚拟机规范(第二版)(下文称 VM Spec)的规定,JVM 包括下列 》 几个运行时数据区域: 1.程序计数器(Program Counter Register) : 每一个 Java 线程都有一个程序计数器来用于保存程序执行到当前方法的哪一个指令, 对于非 Native 方法, 这个区域记录的是正在执行的 VM 原语的地址, 如果正在执行的是 Natvie 方法,这个区域则为空(undefined) 。此内存区域是唯一一个在 VM Spec 中没有 规定任何 OutOfMemoryError 情况的区域。

2.Java 虚拟机栈(Java Virtual Machine Stacks) 与程序计数器一样,VM 栈的生命周期也是与线程相同。VM 栈描述的是 Java 方法调用 的内存模型:每个方法被执行的时候,都会同时创建一个帧(Frame)用于存储本地变 量表、操作栈、动态链接、方法出入口等信息。每一个方法的调用至完成,就意味着一 个帧在 VM 栈中的入栈至出栈的过程。在后文中,我们将着重讨论 VM 栈中本地变量表 部分。 经常有人把 Java 内存简单的区分为堆内存(Heap)和栈内存(Stack) ,实际中的区域远 比这种观点复杂, 这样划分只是说明与变量定义密切相关的内存区域是这两块。 其中所 指的“堆”后面会专门描述,而所指的“栈”就是 VM 栈中各个帧的本地变量表部分。本地

变量表存放了编译期可知的各种标量类型 (boolean、 byte、 char、 short、 int、 float、 long、 double) 对象引用 、 (不是对象本身, 仅仅是一个引用指针) 方法返回地址等。 、 其中 long 和 double 会占用 2 个本地变量空间(32bit) ,其余占用 1 个。本地变量表在进入方法时 进行分配, 当进入一个方法时, 这个方法需要在帧中分配多大的本地变量是一件完全确 定的事情,在方法运行期间不改变本地变量表的大小。 在 VM Spec 中对这个区域规定了 2 中异常状况:如果线程请求的栈深度大于虚拟机所 允许的深度,将抛出 StackOverflowError 异常;如果 VM 栈可以动态扩展(VM Spec 中 允许固定长度的 VM 栈)当扩展时无法申请到足够内存则抛出 OutOfMemoryError 异常。 , 3.本地方法栈(Native Method Stacks) 本地方法栈与 VM 栈所发挥作用是类似的,只不过 VM 栈为虚拟机运行 VM 原语服务, 而本地方法栈是为虚拟机使用到的 Native 方法服务。它的实现的语言、方式与结构并 没有强制规定, 甚至有的虚拟机 (譬如 Sun Hotspot 虚拟机) 直接就把本地方法栈和 VM 栈合二为一。 VM 栈一样, 和 这个区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。

4.Java 堆(Java Heap) 对于绝大多数应用来说,Java 堆是虚拟机管理最大的一块内存。Java 堆是被所有线程共 享的,在虚拟机启动时创建。Java 堆的唯一目的就是存放对象实例,绝大部分的对象实 例都在这里分配。这一点在 VM Spec 中的描述是:所有的实例以及数组都在堆上分配 (原文:The heap is the runtime data area from which memory for all class instances and arrays is allocated) ,但是在逃逸分析和标量替换优化技术出现后,VM Spec 的描述就显 得并不那么准确了。 Java 堆内还有更细致的划分:新生代、老年代,再细致一点的:eden、from survivor、 to survivor,甚至更细粒度的本地线程分配缓冲(TLAB)等,无论对 Java 堆如何划分, 目的都是为了更好的回收内存, 或者更快的分配内存, 在本章中我们仅仅针对内存区域 的作用进行讨论, Java 堆中的上述各个区域的细节, 可参见本文第二章 《JVM 内存管理: 深入垃圾收集器与内存分配策略》 。 根据 VM Spec 的要求, Java 堆可以处于物理上不连续的内存空间, 它逻辑上是连续的即 可, 就像我们的磁盘空间一样。 实现时可以选择实现成固定大小的, 也可以是可扩展的, 不过当前所有商业的虚拟机都是按照可扩展来实现的(通过-Xmx 和-Xms 控制) 。如果 在堆中无法分配内存,并且堆也无法再扩展时,将会抛出 OutOfMemoryError 异常。 5.方法区(Method Area) 叫“方法区”可能认识它的人还不太多,如果叫永久代(Permanent Generation)它的粉 丝也许就多了。它还有个别名叫做 Non-Heap(非堆) ,但是 VM Spec 上则描述方法区为 堆的一个逻辑部分(原文:the method area is logically part of the heap) ,这个名字的问 题还真容易令人产生误解,我们在这里就不纠结了。 方法区中存放了每个 Class 的结构信息, 包括常量池、 字段描述、 方法描述等等。 Space VM

描述中对这个区域的限制非常宽松,除了和 Java 堆一样不需要连续的内存,也可以选 择固定大小或者可扩展外,甚至可以选择不实现垃圾收集。相对来说,垃圾收集行为在 这个区域是相对比较少发生的,但并不是某些描述那样永久代不会发生 GC(至少对当 前主流的商业 JVM 实现来说是如此) 这里的 GC 主要是对常量池的回收和对类的卸载, , 虽然回收的“成绩”一般也比较差强人意,尤其是类卸载,条件相当苛刻。 6.运行时常量池(Runtime Constant Pool) Class 文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常 量表(constant_pool table),用于存放编译期已可知的常量,这部分内容将在类加载后进 入方法区(永久代)存放。但是 Java 语言并不要求常量一定只有编译期预置入 Class 的 常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的 String.intern()方法) 。 运行时常量池是方法区的一部分, 自然受到方法区内存的限制, 当常量池无法在申请到 内存时会抛出 OutOfMemoryError 异常。

7.本机直接内存(Direct Memory) 直接内存并不是虚拟机运行时数据区的一部分, 它根本就是本机内存而不是 VM 直接管 理的区域。但是这部分内存也会导致 OutOfMemoryError 异常出现,因此我们放到这里 一起描述。 在 JDK1.4 中新加入了 NIO 类,引入一种基于渠道与缓冲区的 I/O 方式,它可以通过本 机 Native 函数库直接分配本机内存, 然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。 这样能在一些场景中显著提高性能, 因为避免了在 Java 对和本机堆中来回复制数据。 显然本机直接内存的分配不会受到 Java 堆大小的限制,但是即然是内存那肯定还是要 受到本机物理内存(包括 SWAP 区或者 Windows 虚拟内存)的限制的,一般服务器管 理员配置 JVM 参数时, 会根据实际内存设置-Xmx 等参数信息, 但经常忽略掉直接内存, 使得各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制) ,而导致 动态扩展时出现 OutOfMemoryError 异常。

实战 OutOfMemoryError
上述区域中, 除了程序计数器, 其他在 VM Spec 中都描述了产生 OutOfMemoryError 下 ( 称 OOM)的情形,那我们就实战模拟一下,通过几段简单的代码,令对应的区域产生 OOM 异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。下文的代码都是基于 Sun Hotspot 虚拟机 1.6 版的实现,对于不同公司的不同版本的虚拟机,参数与程序运行结果可 能结果会有所差别。 Java 堆

Java 堆存放的是对象实例,因此只要不断建立对象,并且保证 GC Roots 到对象之间有 可达路径即可产生 OOM 异常。测试中限制 Java 堆大小为 20M,不可扩展,通过参数 -XX:+HeapDumpOnOutOfMemoryError 让虚拟机在出现 OOM 异常的时候 Dump 出内存映像以 便分析。 (关于 Dump 映像文件分析方面的内容,可参见本文第三章《JVM 内存管理:深入 JVM 内存异常分析与调优》) 。 清单 1:Java 堆 OOM 测试 /** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * @author zzm */ public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } } 运行结果: java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid3404.hprof ... Heap dump file created [22045981 bytes in 0.663 secs]

VM 栈和本地方法栈 Hotspot 虚拟机并不区分 VM 栈和本地方法栈,因此-Xoss 参数实际上是无效的,栈容量 只由-Xss 参数设定。 关于 VM 栈和本地方法栈在 VM Spec 描述了两种异常: StackOverflowError 与 OutOfMemoryError,当栈空间无法继续分配分配时,到底是内存太小还是栈太大其实某 种意义上是对同一件事情的两种描述而已,在笔者的实验中,对于单线程应用尝试下面 3 种方法均无法让虚拟机产生 OOM,全部尝试结果都是获得 SOF 异常。 1.使用-Xss 参数削减栈内存容量。结果:抛出 SOF 异常时的堆栈深度相应缩小。 2.定义大量的本地变量,增大此方法对应帧的长度。结果:抛出 SOF 异常时的堆栈深度 相应缩小。 3.创建几个定义很多本地变量的复杂对象,打开逃逸分析和标量替换选项,使得 JIT 编

译器允许对象拆分后在栈中分配。结果:实际效果同第二点。 清单 2:VM 栈和本地方法栈 OOM 测试(仅作为第 1 点测试程序) /** * VM Args:-Xss128k * @author zzm */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } } 运行结果: stack length:2402 Exception in thread "main" java.lang.StackOverflowError at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:20) at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21) at org.fenixsoft.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:21) 如果在多线程环境下,不断建立线程倒是可以产生 OOM 异常,但是基本上这个异常和 VM 栈空间够不够关系没有直接关系,甚至是给每个线程的 VM 栈分配的内存越多反而越容 易产生这个 OOM 异常。 原因其实很好理解,操作系统分配给每个进程的内存是有限制的,譬如 32 位 Windows 限制为 2G,Java 堆和方法区的大小 JVM 有参数可以限制最大值,那剩余的内存为 2G(操作 系统限制)-Xmx(最大堆)-MaxPermSize(最大方法区) ,程序计数器消耗内存很小,可以 忽略掉,那虚拟机进程本身耗费的内存不计算的话,剩下的内存就供每一个线程的 VM 栈和 本地方法栈瓜分了,那自然每个线程中 VM 栈分配内存越多,就越容易把剩下的内存耗尽。

清单 3:创建线程导致 OOM 异常 /** * VM Args:-Xss2M (这时候不妨设大些) * @author zzm */ public class JavaVMStackOOM { private void dontStop() { while (true) { } } public void stackLeakByThread() { while (true) { Thread thread = new Thread(new Runnable() { @Override public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) throws Throwable { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } } 特别提示一下,如果读者要运行上面这段代码,记得要存盘当前工作,上述代码执行时 有很大令操作系统卡死的风险。 运行结果: Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread

运行时常量池 要在常量池里添加内容, 最简单的就是使用 String.intern()这个 Native 方法。 由于常量池 分配在方法区内, 我们只需要通过-XX:PermSize 和-XX:MaxPermSize 限制方法区大小即可限制 常量池容量。实现代码如下: 清单 4:运行时常量池导致的 OOM 异常

/** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * @author zzm */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用 List 保持着常量池引用,压制 Full GC 回收常量池行为 List<String> list = new ArrayList<String>(); // 10M 的 PermSize 在 integer 范围内足够产生 OOM 了 int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } 运行结果: Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.lang.String.intern(Native Method) at org.fenixsoft.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)

方法区 上文讲过,方法区用于存放 Class 相关信息,所以这个区域的测试我们借助 CGLib 直接 操作字节码动态生成大量的 Class,值得注意的是,这里我们这个例子中模拟的场景其实经 常会在实际应用中出现:当前很多主流框架,如 Spring、Hibernate 对类进行增强时,都会 使用到 CGLib 这类字节码技术,当增强的类越多,就需要越大的方法区用于保证动态生成的 Class 可以加载入内存。 清单 5:借助 CGLib 使得方法区出现 OOM 异常 /** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * @author zzm */ public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() {

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject { } } 运行结果: Caused by: java.lang.OutOfMemoryError: PermGen space at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632) at java.lang.ClassLoader.defineClass(ClassLoader.java:616) ... 8 more 本机直接内存 DirectMemory 容量可通过-XX:MaxDirectMemorySize 指定,不指定的话默认与 Java 堆 (-Xmx 指定)一样,下文代码越过了 DirectByteBuffer,直接通过反射获取 Unsafe 实例进行 内存分配 (Unsafe 类的 getUnsafe()方法限制了只有引导类加载器才会返回实例, 也就是基本 上只有 rt.jar 里面的类的才能使用) ,因为 DirectByteBuffer 也会抛 OOM 异常,但抛出异常时 实际上并没有真正向操作系统申请分配内存, 而是通过计算得知无法分配既会抛出, 真正申 请分配的方法是 unsafe.allocateMemory()。 /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm */ public class DirectMemoryOOM { private static final int _1MB = 1024 * 1024; public static void main(String[] args) throws Exception { Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); while (true) { unsafe.allocateMemory(_1MB);

} } } 运行结果: Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at org.fenixsoft.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)

总结
到此为止,我们弄清楚虚拟机里面的内存是如何划分的,哪部分区域,什么样的代码、 操作可能导致 OOM 异常。虽然 Java 有垃圾收集机制,但 OOM 仍然离我们并不遥远,本章 内容我们只是知道各个区域 OOM 异常出现的原因, 下一章我们将看看 Java 垃圾收集机制为 了避免 OOM 异常出现,做出了什么样的努力。

java 线程安全总结
最近想将 java 基础的一些东西都整理整理,写下来,这是对知识的总结,也是 一种乐趣。已经拟好了提纲,大概分为这几个主题: java 线程安全,java 垃圾 收集,java 并发包详细介绍,java profile 和 jvm 性能调优 。慢慢写吧。本人 jameswxx 原创文章,转载请注明出处,我费了很多心血,多谢了。关于 java 线 程安全,网上有很多资料,我只想从自己的角度总结对这方面的考虑,有时候写 东西是很痛苦的,知道一些东西,但想用文字说清楚,却不是那么容易。我认为 要认识 java 线程安全,必须了解两个主要的点:java 的内存模型,java 的线程 同步机制。 特别是内存模型, java 的线程同步机制很大程度上都是基于内存模型 而设定的。后面我还会写 java 并发包的文章,详细总结如何利用 java 并发包编 写高效安全的多线程并发程序。暂时写得比较仓促,后面会慢慢补充完善。

浅谈 java 内存模型
不同的平台,内存模型是不一样的,但是 jvm 的内存模型规范是统一的。 其实 java 的多线程并发问题最终都会反映在 java 的内存模型上,所谓线程安全 无非是要控制多个线程对某个资源的有序访问或修改。总结 java 的内存模型, 要解决两个主要的问题: 可见性和有序性。 我们都知道计算机有高速缓存的存在, 处理器并不是每次处理数据都是取内存的。JVM 定义了自己的内存模型,屏蔽 了底层平台内存管理细节,对于 java 开发人员,要清楚在 jvm 内存模型的基础 上,如果解决多线程的可见性和有序性。 那么,何谓可见性? 多个线程之间是不能互相传递数据通信的,它们之间 的沟通只能通过共享变量来进行。Java 内存模型(JMM)规定了 jvm 有主内存, 主内存是多个线程共享的。当 new 一个对象的时候,也是被分配在主内存中,

每个线程都有自己的工作内存, 工作内存存储了主存的某些对象的副本,当然线 程的工作内存大小是有限制的。当线程操作某个对象时,执行顺序如下: (1) 从主存复制变量到当前工作内存 (read and load) (2) 执行代码,改变共享变量值 (use and assign) (3) 用工作内存数据刷新主存相关内容 (store and write) JVM 规范定义了线程对主存的操作指令: read, load, use, assign, store, write。 当一个共享变量在多个线程的工作内存中都有副本时, 如果一个线程修改了这个 共享变量, 那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见 性问题。 那么,什么是有序性呢 ?线程在引用变量时不能直接从主内存中引用,如果 线程工作内存中没有该变量,则会从主内存中拷贝一个副本到工作内存中,这个过 程为 read-load,完成后线程会引用该副本。 当同一线程再度引用该字段时,有可能 重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本(use), 也就是说 read,load,use 顺序可以由 JVM 实现系统决定。 线程不能直接为主存中中字段赋值,它会将值指定给工作内存中的变量副 本(assign),完成后这个变量副本会同步到主存储区(store-write),至于何时同步 过去, 根据 JVM 实现系统决定.有该字段,则会从主内存中将该字段赋值到工作内 存中,这个过程为 read-load,完成后线程会引用该变量副本, 当同一线程多次重复 对字段赋值时,比如:
Java 代码 1. for(int i=0;i<10;i++) 2. a++;

线程有可能只对工作内存中的副本进行赋值,只到最后一次赋值后才同步到主存 储区, 所以 assign,store,weite 顺序可以由 JVM 实现系统决定。假设有一个共享 变量 x, 线程 a 执行 x=x+1。 从上面的描述中可以知道 x=x+1 并不是一个原子操 作,它的执行过程如下: 1 从主存中读取变量 x 副本到工作内存 2 给x加1 3 将 x 加 1 后的值写回主 存 如果另外一个线程 b 执行 x=x-1,执行过程如下: 1 从主存中读取变量 x 副本到工作内存 2 给x减1 3 将 x 减 1 后的值写回主存 那么显然,最终的 x 的值是不可靠的。假设 x 现在为 10,线程 a 加 1,线程 b 减 1,从表面上看,似乎最终 x 还是为 10,但是多线程情况下会有这种情况发 生: 1:线程 a 从主存读取 x 副本到工作内存,工作内存中 x 值为 10 2:线程 b 从主存读取 x 副本到工作内存,工作内存中 x 值为 10 3:线程 a 将工作内存中 x 加 1,工作内存中 x 值为 11 4:线程 a 将 x 提交主存中,主存中 x 为 11 5:线程 b 将工作内存中 x 值减 1,工作内存中 x 值为 9 6:线程 b 将 x 提交到中主存中,主存中 x 为 9

同样,x 有可能为 11,如果 x 是一个银行账户,线程 a 存款,线程 b 扣款,显 然这样是有严重问题的,要解决这个问题,必须保证线程 a 和线程 b 是有序执 行的,并且每个线程执行的加 1 或减 1 是一个原子操作。看看下面代码:
Java 代码 1. public class Account { 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. "); 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. } public AddThread(Account account, int amount) { this.account = account; this.amount = amount; static class AddThread implements Runnable { Account account; int amount; } a.start(); b.start(); a.join(); b.join(); System.out.println(account.getBalance()); public static void main(String[] args) throws InterruptedException { Account account = new Account(1000); Thread a = new Thread(new AddThread(account, 20), "add"); Thread b = new Thread(new WithdrawThread(account, 20), "withdraw } public void withdraw(int num) { balance = balance - num; } public void add(int num) { balance = balance + num; } public int getBalance() { return balance; } public Account(int balance) { this.balance = balance; private int balance;

40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. } } } } public void run() { for (int i = 0; i < 100000; i++) { account.withdraw(amount); } public WithdrawThread(Account account, int amount) { this.account = account; this.amount = amount; static class WithdrawThread implements Runnable { Account account; int amount; } } } public void run() { for (int i = 0; i < 200000; i++) { account.add(amount);

第一次执行结果为 10200,第二次执行结果为 1060,每次执行的结果都是不确 定的,因为线程的执行顺序是不可预见的。这是 java 同步产生的根源, synchronized 关键字保证了多个线程对于同步块是互斥的,synchronized 作为 一种同步手段,解决 java 多线程的执行有序性和内存可见性,而 volatile 关键字 之解决多线程的内存可见性问题。后面将会详细介绍。

synchronized 关键字
上面说了, java 用 synchronized 关键字做为多线程并发环境的执行有序性 的保证手段之一。 当一段代码会修改共享变量, 这一段代码成为互斥区或临界区, 为了保证共享变量的正确性,synchronized 标示了临界区。典型的用法如下:
Java 代码 1. synchronized(锁){ 2. 3. } 临界区代码

为了保证银行账户的安全,可以操作账户的方法如下:
Java 代码 1. public synchronized void add(int num) { 2. 3. } 4. public synchronized void withdraw(int num) { 5. 6. } balance = balance - num; balance = balance + num;

刚才不是说了 synchronized 的用法是这样的吗:
Java 代码 1. synchronized(锁){ 2. 临界区代码 3. }

那么对于 public synchronized void add(int num)这种情况, 意味着什么呢?其实 这种情况,锁就是这个方法所在的对象。同理,如果方法是 public static synchronized void add(int num),那么锁就是这个方法所在的 class。 理论上,每个对象都可以做为锁,但一个对象做为锁时,应该被多个线程 共享,这样才显得有意义,在并发环境下,一个没有共享的对象作为锁是没有意 义的。假如有这样的代码:
Java 代码 1. public class ThreadTest{ 2. 3. 4. 5. 6. 7. 8. } } } public void test(){ Object lock=new Object(); synchronized (lock){ //do something

lock 变量作为一个锁存在根本没有意义, 因为它根本不是共享对象, 每个线程进 来都会执行 Object lock=new Object();每个线程都有自己的 lock,根本不存在锁 竞争。 每个锁对象都有两个队列,一个是就绪队列,一个是阻塞队列,就绪队列 存储了将要获得锁的线程, 阻塞队列存储了被阻塞的线程,当一个被线程被唤醒 (notify)后,才会进入到就绪队列,等待 cpu 的调度。当一开始线程 a 第一次执 行 account.add 方法时,jvm 会检查锁对象 account 的就绪队列是否已经有线程 在等待, 如果有则表明 account 的锁已经被占用了, 由于是第一次运行, account

的就绪队列为空,所以线程 a 获得了锁,执行 account.add 方法。如果恰好在这 个时候,线程 b 要执行 account.withdraw 方法,因为线程 a 已经获得了锁还没 有释放,所以线程 b 要进入 account 的就绪队列,等到得到锁后才可以执行。 一个线程执行临界区代码过程如下: 1 获得同步锁 2 清空工作内存 3 从主存拷贝变量副本到工作内存 4 对这些变量计算 5 将变量从工作内存写回到主存 6 释放锁 可见,synchronized 既保证了多线程的并发有序性,又保证了多线程的内存可 见性。

生产者/消费者模式
生产者/消费者模式其实是一种很经典的线程同步模型,很多时候,并不是 光保证多个线程对某共享资源操作的互斥性就够了, 往往多个线程之间都是有协 作的。 假设有这样一种情况,有一个桌子,桌子上面有一个盘子,盘子里只能放 一颗鸡蛋,A 专门往盘子里放鸡蛋,如果盘子里有鸡蛋,则一直等到盘子里没鸡 蛋,B 专门从盘子里拿鸡蛋,如果盘子里没鸡蛋,则等待直到盘子里有鸡蛋。其 实盘子就是一个互斥区,每次往盘子放鸡蛋应该都是互斥的,A 的等待其实就是 主动放弃锁,B 等待时还要提醒 A 放鸡蛋。 如何让线程主动释放锁 很简单,调用锁的 wait()方法就好。wait 方法是从 Object 来的,所以任意对象 都有这个方法。看这个代码片段:
Java 代码 1. Object lock=new Object();//声明了一个对象作为锁 2. 3. 4. 5. 6. } synchronized (lock) { balance = balance - num; //这里放弃了同步锁,好不容易得到,又放弃了 lock.wait();

如果一个线程获得了锁 lock,进入了同步块,执行 lock.wait(),那么这个线程会 进入到 lock 的阻塞队列。如果调用 lock.notify()则会通知阻塞队列的某个线程进 入就绪队列。 声明一个盘子,只能放一个鸡蛋
Java 代码 1. package com.jameswxx.synctest; 2. public class Plate{ 3. List<Object> eggs=new ArrayList<Object>();

4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. } 17.

public synchronized try{ wait();

Object getEgg(){

if(eggs.size()==0){

}catch(InterruptedException e){ } } Object egg=eggs.get(0); eggs.clear();//清空盘子 notify();//唤醒阻塞队列的某线程到就绪队列 return egg;

18. public synchronized 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. } } } } try{ wait();

void putEgg(Object egg){

If(eggs.size()>0){

}catch(InterruptedException e){

eggs.add(egg);//往盘子里放鸡蛋 notify();//唤醒阻塞队列的某线程到就绪队列

声明一个 Plate 对象为 plate,被线程 A 和线程 B 共享,A 专门放鸡蛋,B 专门 拿鸡蛋。假设 1 开始,A 调用 plate.putEgg 方法,此时 eggs.size()为 0,因此顺利将鸡蛋放到 盘子,还执行了 notify()方法,唤醒锁的阻塞队列的线程,此时阻塞队列还没有 线程。 2 又有一个 A 线程对象调用 plate.putEgg 方法,此时 eggs.size()不为 0,调用 wait()方法,自己进入了锁对象的阻塞队列。 3 此时,来了一个 B 线程对象,调用 plate.getEgg 方法,eggs.size()不为 0,顺 利的拿到了一个鸡蛋,还执行了 notify()方法,唤醒锁的阻塞队列的线程,此时 阻塞队列有一个 A 线程对象,唤醒后,它进入到就绪队列,就绪队列也就它一 个, 因此马上得到锁, 开始往盘子里放鸡蛋, 此时盘子是空的, 因此放鸡蛋成功。 4 假设接着来了线程 A,就重复 2;假设来料线程 B,就重复 3。 整个过程都保证了放鸡蛋,拿鸡蛋,放鸡蛋,拿鸡蛋。

volatile 关键字

volatile 是 java 提供的一种同步手段,只不过它是轻量级的同步,为什么这 么说,因为 volatile 只能保证多线程的内存可见性,不能保证多线程的执行有序 性。 而最彻底的同步要保证有序性和可见性, 例如 synchronized。 任何被 volatile 修饰的变量,都不拷贝副本到工作内存,任何修改都及时写在主存。因此对于 Valatile 修饰的变量的修改,所有线程马上就能看到,但是 volatile 不能保证对 变量的修改是有序的。什么意思呢?假如有这样的代码:
Java 代码 1. public class VolatileTest{ 2. 3. 4. 5. 6. } } public volatile int a; public void add(int count){ a=a+count;

当一个 VolatileTest 对象被多个线程共享,a 的值不一定是正确的,因为 a=a+count 包含了好几步操作,而此时多个线程的执行是无序的,因为没有任何 机制来保证多个线程的执行有序性和原子性。volatile 存在的意义是,任何线程 对 a 的修改,都会马上被其他线程读取到,因为直接操作主存,没有线程对工作 内存和主存的同步。所以,volatile 的使用场景是有限的,在有限的一些情形下 可以使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同 时满足下面两个条件: 1)对变量的写操作不依赖于当前值。 2)该变量没有包含在具有其他变量的不变式中 volatile 只保证了可见性,所以 Volatile 适合直接赋值的场景,如
Java 代码 1. public class VolatileTest{ 2. 3. 4. 5. 6. } } public volatile int a; public void setA(int a){ this.a=a;

在没有 volatile 声明时, 多线程环境下, 的最终值不一定是正确的, a 因为 this.a=a; 涉及到给 a 赋值和将 a 同步回主存的步骤, 这个顺序可能被打乱。 如果用 volatile 声明了, 读取主存副本到工作内存和同步 a 到主存的步骤, 相当于是一个原子操 作。所以简单来说,volatile 适合这种场景:一个变量被多个线程共享,线程直 接给这个变量赋值。这是一种很简单的同步场景,这时候使用 volatile 的开销将 会非常小。

JVM 内存管理: 深入垃圾收集器与内存 分配策略
Java 与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却 想出来。

概述:
说起垃圾收集(Garbage Collection,下文简称 GC),大部分人都把这项技术当做 Java 语言的伴生 产物。事实上 GC 的历史远远比 Java 来得久远,在 1960 年诞生于 MIT 的 Lisp 是第一门真正使用内存动 态分配和垃圾收集技术的语言。当 Lisp 还在胚胎时期,人们就在思考 GC 需要完成的 3 件事情:哪些内存 需要回收?什么时候回收?怎么样回收? 经过半个世纪的发展, 目前的内存分配策略与垃圾回收技术已经相当成熟, 一切看起来都进入―自动化‖ 的时代,那为什么我们还要去了解 GC 和内存分配?答案很简单:当需要排查各种内存溢出、泄漏问题时, 当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些―自动化‖的技术有必要的监控、调节手 段。 把时间从 1960 年拨回现在,回到我们熟悉的 Java 语言。本文第一章中介绍了 Java 内存运行时区域 的各个部分,其中程序计数器、VM 栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的帧随着方 法进入、退出而有条不紊的进行着出栈入栈操作;每一个帧中分配多少内存基本上是在 Class 文件生成时 就已知的(可能会由 JIT 动态晚期编译进行一些优化,但大体上可以认为是编译期可知的),因此这几个 区域的内存分配和回收具备很高的确定性, 因此在这几个区域不需要过多考虑回收的问题。 Java 堆和方 而 法区(包括运行时常量池)则不一样,我们必须等到程序实际运行期间才能知道会创建哪些对象,这部分 内存的分配和回收都是动态的,我们本文后续讨论中的―内存‖分配与回收仅仅指这一部分内存。

对象已死?
在堆里面存放着 Java 世界中几乎所有的对象, 在回收前首先要确定这些对象之中哪些还在存活, 哪些 已经―死去‖了,即不可能再被任何途径使用的对象。 引用计数算法(Reference Counting) 最初的想法,也是很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,当 有一个地方引用它,计数器加 1,当引用失效,计数器减 1,任何时刻计数器为 0 的对象就是不可能再被 使用的。 客观的说,引用计数算法实现简单,判定效率很高,在大部分情况下它都是一个不错的算法,但引用 计数算法无法解决对象循环引用的问题。 举个简单的例子: 对象 A 和 B 分别有字段 b、 令 A.b=B 和 B.a=A, a, 除此之外这 2 个对象再无任何引用,那实际上这 2 个对象已经不可能再被访问,但是引用计数算法却无法

回收他们。 根搜索算法(GC Roots Tracing) 在实际生产的语言中(Java、C#、甚至包括前面提到的 Lisp),都是使用根搜索算法判定对象是否存 活。 算法基本思路就是通过一系列的称为―GC Roots‖的点作为起始进行向下搜索, 当一个对象到 GC Roots 没有任何引用链(Reference Chain)相连,则证明此对象是不可用的。在 Java 语言中,GC Roots 包括: 1.在 VM 栈(帧中的本地变量)中的引用 2.方法区中的静态引用 3.JNI(即一般说的 Native 方法)中的引用 生存还是死亡? 判定一个对象死亡,至少经历两次标记过程:如果对象在进行根搜索后,发现没有与 GC Roots 相连 接的引用链,那它将会被第一次标记,并在稍后执行他的 finalize()方法(如果它有的话)。这里所谓的―执 行‖是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这点是必须的,否则一个对象在 finalize() 方法执行缓慢,甚至有死循环什么的将会很容易导致整个系统崩溃。finalize()方法是对象最后一次逃脱死 亡命运的机会,稍后 GC 将进行第二次规模稍小的标记,如果在 finalize()中对象成功拯救自己(只要重新 建立到 GC Roots 的连接即可,譬如把自己赋值到某个引用上),那在第二次标记时它将被移除出―即将回 收‖的集合,如果对象这时候还没有逃脱,那基本上它就真的离死不远了。 需要特别说明的是,这里对 finalize()方法的描述可能带点悲情的艺术加工,并不代表笔者鼓励大家去 使用这个方法来拯救对象。相反,笔者建议大家尽量避免使用它,这个不是 C/C++里面的析构函数,它运 行代价高昂,不确定性大,无法保证各个对象的调用顺序。需要关闭外部资源之类的事情,基本上它能做 的使用 try-finally 可以做的更好。 关于方法区 方法区即后文提到的永久代,很多人认为永久代是没有 GC 的,《Java 虚拟机规范》中确实说过可以 不要求虚拟机在这区实现 GC,而且这区 GC 的―性价比‖一般比较低:在堆中,尤其是在新生代,常规应用 进行一次 GC 可以一般可以回收 70%~95%的空间, 而永久代的 GC 效率远小于此。 虽然 VM Spec 不要求, 但当前生产中的商业 JVM 都有实现永久代的 GC,主要回收两部分内容:废弃常量与无用类。这两点回收 思想与 Java 堆中的对象回收很类似, 都是搜索是否存在引用, 常量的相对很简单, 与对象类似的判定即可。 而类的回收则比较苛刻,需要满足下面 3 个条件: 1.该类所有的实例都已经被 GC,也就是 JVM 中不存在该 Class 的任何实例。 2.加载该类的 ClassLoader 已经被 GC。 3.该类对应的 java.lang.Class 对象没有在任何地方被引用, 如不能在任何地方通过反射访问该类的方 法。 是否对类进行回收可使用-XX:+ClassUnloading 参数进行控制,还可以使用-verbose:class 或者 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载、卸载信息。

在大量使用反射、动态代理、CGLib 等 bytecode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要 JVM 具备类卸载的支持以保证永久代不会溢出。

垃圾收集算法
在这节里不打算大量讨论算法实现,只是简单的介绍一下基本思想以及发展过程。最基础的搜集算法 是―标记-清除算法‖(Mark-Sweep),如它的名字一样,算法分层―标记‖和―清除‖两个阶段,首先标记出所 有需要回收的对象,然后回收所有需要回收的对象,整个过程其实前一节讲对象标记判定的时候已经基本 介绍完了。说它是最基础的收集算法原因是后续的收集算法都是基于这种思路并优化其缺点得到的。它的 主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理之后会产生 大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次 的垃圾搜集动作。 为了解决效率问题,一种称为―复制‖(Copying)的搜集算法出现,它将可用内存划分为两块,每次只 使用其中的一块,当半区内存用完了,仅将还存活的对象复制到另外一块上面,然后就把原来整块内存空 间一次过清理掉。这样使得每次内存回收都是对整个半区的回收,内存分配时也就不用考虑内存碎片等复 杂情况,只要移动堆顶指针,按顺序分配内存就可以了,实现简单,运行高效。只是这种算法的代价是将 内存缩小为原来的一半,未免太高了一点。 现在的商业虚拟机中都是用了这一种收集算法来回收新生代,IBM 有专门研究表明新生代中的对象 98%是朝生夕死的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 eden 空 间和 2 块较少的 survivor 空间,每次使用 eden 和其中一块 survivor,当回收时将 eden 和 survivor 还存活 的对象一次过拷贝到另外一块 survivor 空间上,然后清理掉 eden 和用过的 survivor。Sun Hotspot 虚拟机 默认 eden 和 survivor 的大小比例是 8:1,也就是每次只有 10%的内存是―浪费‖的。当然,98%的对象可回 收只是一般场景下的数据,我们没有办法保证每次回收都只有 10%以内的对象存活,当 survivor 空间不够 用时,需要依赖其他内存(譬如老年代)进行分配担保(Handle Promotion)。 复制收集算法在对象存活率高的时候,效率有所下降。更关键的是,如果不想浪费 50%的空间,就需 要有额外的空间进行分配担保用于应付半区内存中所有对象都 100%存活的极端情况,所以在老年代一般 不能直接选用这种算法。因此人们提出另外一种―标记-整理‖(Mark-Compact)算法,标记过程仍然一样, 但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。 当前商业虚拟机的垃圾收集都是采用―分代收集‖(Generational Collecting)算法,这种算法并没有什 么新的思想出现, 只是根据对象不同的存活周期将内存划分为几块。 一般是把 Java 堆分作新生代和老年代, 这样就可以根据各个年代的特点采用最适当的收集算法,譬如新生代每次 GC 都有大批对象死去,只有少 量存活,那就选用复制算法只需要付出少量存活对象的复制成本就可以完成收集。

垃圾收集器
垃圾收集器就是收集算法的具体实现,不同的虚拟机会提供不同的垃圾收集器。并且提供参数供用户 根据自己的应用特点和要求组合各个年代所使用的收集器。 本文讨论的收集器基于 Sun Hotspot 虚拟机 1.6 版。

图 1.Sun JVM1.6 的垃圾收集器

图 1 展示了 1.6 中提供的 6 种作用于不同年代的收集器,两个收集器之间存在连线的话就说明它们可 以搭配使用。在介绍着些收集器之前,我们先明确一个观点:没有最好的收集器,也没有万能的收集器, 只有最合适的收集器。 1.Serial 收集器 单线程收集器,收集时会暂停所有工作线程(我们将这件事情称之为 Stop The World,下称 STW), 使用复制收集算法,虚拟机运行在 Client 模式时的默认新生代收集器。 2.ParNew 收集器 ParNew 收集器就是 Serial 的多线程版本,除了使用多条收集线程外,其余行为包括算法、STW、对 象分配规则、回收策略等都与 Serial 收集器一摸一样。对应的这种收集器是虚拟机运行在 Server 模式的默 认新生代收集器,在单 CPU 的环境中,ParNew 收集器并不会比 Serial 收集器有更好的效果。 3.Parallel Scavenge 收集器 Parallel Scavenge 收集器(下称 PS 收集器)也是一个多线程收集器,也是使用复制算法,但它的对 象分配规则与回收策略都与 ParNew 收集器有所不同,它是以吞吐量最大化(即 GC 时间占总运行时间最 小)为目标的收集器实现,它允许较长时间的 STW 换取总吞吐量最大化。 4.Serial Old 收集器 Serial Old 是单线程收集器,使用标记-整理算法,是老年代的收集器,上面三种都是使用在新生代收 集器。 5.Parallel Old 收集器 老年代版本吞吐量优先收集器,使用多线程和标记-整理算法,JVM 1.6 提供,在此之前,新生代使

用了 PS 收集器的话,老年代除 Serial Old 外别无选择,因为 PS 无法与 CMS 收集器配合工作。

6.CMS(Concurrent Mark Sweep)收集器 CMS 是一种以最短停顿时间为目标的收集器,使用 CMS 并不能达到 GC 效率最高(总体 GC 时间最 小),但它能尽可能降低 GC 时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来 说至关重要,这类应用对于长时间 STW 一般是不可容忍的。CMS 收集器使用的是标记-清除算法,也就 是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启 CMS 收集结束后再进行一次内存压缩。 内存分配与回收策略 了解 GC 其中很重要一点就是了解 JVM 的内存分配策略:即对象在哪里分配和对象什么时候回收。 关于对象在哪里分配,往大方向讲,主要就在堆上分配,但也可能经过 JIT 进行逃逸分析后进行标量 替换拆散为原子类型在栈上分配,也可能分配在 DirectMemory 中(详见本文第一章)。往细节处讲,对 象主要分配在新生代 eden 上,也可能会直接老年代中,分配的细节决定于当前使用的垃圾收集器类型与 VM 相关参数设置。 我们可以通过下面代码来验证一下 Serial 收集器 (ParNew 收集器的规则与之完全一致) 的内存分配和回收的策略。 读者看完 Serial 收集器的分析后, 不妨自己根据 JVM 参数文档写一些程序去实 践一下其它几种收集器的分配策略。 清单 1:内存分配测试代码 Java 代码 1. public class YoungGenGC { 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. } /** * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:Su rvivorRatio=8 */ @SuppressWarnings("unused") public static void testAllocation() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation4 = new byte[4 * _1MB]; // 出现一次 Minor GC } public static void main(String[] args) { // testAllocation(); testHandlePromotion(); // testPretenureSizeThreshold(); // testTenuringThreshold(); // testTenuringThreshold2(); private static final int _1MB = 1024 * 1024;

24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. } /** * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:Su rvivorRatio=8 -XX:MaxTenuringThreshold=15 * -XX:+PrintTenuringDistribution */ @SuppressWarnings("unused") public static void testTenuringThreshold2() { byte[] allocation1, allocation2, allocation3, allocation4; allocation1 = new byte[_1MB / 4]; rvivo 空间一半 allocation2 = new byte[_1MB / 4]; allocation3 = new byte[4 * _1MB]; allocation4 = new byte[4 * _1MB]; allocation4 = null; allocation4 = new byte[4 * _1MB]; // allocation1+allocation2 大于 su } /** * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:Su rvivorRatio=8 -XX:MaxTenuringThreshold=1 * -XX:+PrintTenuringDistribution */ @SuppressWarnings("unused") public static void testTenuringThreshold() { byte[] allocation1, allocation2, allocation3; allocation1 = new byte[_1MB / 4]; nuringThreshold 设置 allocation2 = new byte[4 * _1MB]; allocation3 = new byte[4 * _1MB]; allocation3 = null; allocation3 = new byte[4 * _1MB]; // 什么时候进入老年代决定于 XX:MaxTe } /** * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:Su rvivorRatio=8 * -XX:PretenureSizeThreshold=3145728 */ @SuppressWarnings("unused") public static void testPretenureSizeThreshold() { byte[] allocation; allocation = new byte[4 * _1MB]; //直接分配在老年代中

63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. } } /** * VM 参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:Su rvivorRatio=8 -XX:-HandlePromotionFailure */ @SuppressWarnings("unused") public static void testHandlePromotion() { byte[] allocation1, allocation2, allocation3, allocation4, allocatio n5, allocation6, allocation7; allocation1 = new byte[2 * _1MB]; allocation2 = new byte[2 * _1MB]; allocation3 = new byte[2 * _1MB]; allocation1 = null; allocation4 = new byte[2 * _1MB]; allocation5 = new byte[2 * _1MB]; allocation6 = new byte[2 * _1MB]; allocation4 = null; allocation5 = null; allocation6 = null; allocation7 = new byte[2 * _1MB];

规则一:通常情况下,对象在 eden 中分配。当 eden 无法分配时,触发一次 Minor GC。 执行 testAllocation()方法后输出了 GC 日志以及内存分配状况。-Xms20M -Xmx20M -Xmn10M 这 3 个参数确定了 Java 堆大小为 20M,不可扩展,其中 10M 分配给新生代,剩下的 10M 即为老年代。 -XX:SurvivorRatio=8 决定了新生代中 eden 与 survivor 的空间比例是 1:8,从输出的结果也清晰的看到 ―eden space 8192K、from space 1024K、to space 1024K‖的信息,新生代总可用空间为 9216K(eden+1 个 survivor)。 我们也注意到在执行 testAllocation()时出现了一次 Minor GC, 的结果是新生代 6651K 变为 148K, GC 而总占用内存则几乎没有减少(因为几乎没有可回收的对象)。这次 GC 是发生的原因是为 allocation4 分 配内存的时候, eden 已经被占用了 6M, 剩余空间已不足分配 allocation4 所需的 4M 内存, 因此发生 Minor GC。GC 期间虚拟机发现已有的 3 个 2M 大小的对象全部无法放入 survivor 空间(survivor 空间只有 1M 大小),所以直接转移到老年代去。GC 后 4M 的 allocation4 对象分配在 eden 中。 清单 2:testAllocation()方法输出结果

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)

eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, 14% used [0x032d0000, 0x032f5370, 0x033d0000) to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)

tenured generation

the space 10240K, 60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) No shared spaces configured. 规则二:配置了 PretenureSizeThreshold 的情况下,对象大于设置值将直接在老年代分配。 执行 testPretenureSizeThreshold()方法后,我们看到 eden 空间几乎没有被使用,而老年代的 10M 控 件被使用了 40%, 也就是 4M 的 allocation 对象直接就分配在老年代中, 则是因为 PretenureSizeThreshold 被设置为 3M,因此超过 3M 的对象都会直接从老年代分配。 清单 3:

Heap def new generation eden space 8192K, from space 1024K, to space 1024K, total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000) 8% used [0x029d0000, 0x02a77e98, 0x031d0000) 0% used [0x031d0000, 0x031d0000, 0x032d0000) 0% used [0x032d0000, 0x032d0000, 0x033d0000) total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)

tenured generation

the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) compacting perm gen total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000) No shared spaces configured. 规则三:在 eden 经过 GC 后存活,并且 survivor 能容纳的对象,将移动到 survivor 空间内,如果对象在 survivor 中继续熬过若干次回收 (默认为 15 次) 将会被移动到老年代中。 回收次数由 MaxTenuringThreshold 设置。 分 别 以 -XX:MaxTenuringThreshold=1 和 -XX:MaxTenuringThreshold=15 两 种 设 置 来 执 行 testTenuringThreshold() , 方 法 中 allocation1 对 象 需 要 256K 内 存 , survivor 空 间 可 以 容 纳 。 当 MaxTenuringThreshold=1 时,allocation1 对象在第二次 GC 发生时进入老年代,新生代已使用的内存 GC 后非常干净的变成 0KB。而 MaxTenuringThreshold=15 时,第二次 GC 发生后,allocation1 对象则还留 在新生代 survivor 空间,这时候新生代仍然有 404KB 被占用。 清单 4: MaxTenuringThreshold=1

[GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) - age 1: 414664 bytes, 414664 total

: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 1) : 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)

eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) 0% used [0x032d0000, 0x032d0000, 0x033d0000) total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)

tenured generation

the space 10240K, 43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) No shared spaces configured.

MaxTenuringThreshold=15 [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 1: 414664 bytes, 414664 total

: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) - age 2: 414520 bytes, 414520 total

: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)

eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, 39% used [0x031d0000, 0x03235338, 0x032d0000) to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000) total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)

tenured generation

the space 10240K, 40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000) No shared spaces configured. 规则四:如果在 survivor 空间中相同年龄所有对象大小的累计值大于 survivor 空间的一半,大于或等于个 年龄的对象就可以直接进入老年代,无需达到 MaxTenuringThreshold 中要求的年龄。 执行 testTenuringThreshold2()方法,并将设置-XX:MaxTenuringThreshold=15 ,发现运行结果中 survivor 占用仍然为 0%,而老年代比预期增加了 6%,也就是说 allocation1、allocation2 对象都直接进入

了老年代, 而没有等待到 15 岁的临界年龄。 因为这 2 个对象加起来已经到达了 512K, 并且它们是同年的, 满足同年对象达到 survivor 空间的一半规则。我们只要注释掉其中一个对象 new 操作,就会发现另外一个 就不会晋升到老年代中去了。 清单 5: [GC [DefNew Desired survivor size 524288 bytes, new threshold 1 (max 15) - age 1: 676824 bytes, 676824 total

: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] [GC [DefNew Desired survivor size 524288 bytes, new threshold 15 (max 15) : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)

eden space 8192K, 51% used [0x029d0000, 0x02de4828, 0x031d0000) from space 1024K, to space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000) 0% used [0x032d0000, 0x032d0000, 0x033d0000) total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)

tenured generation

the space 10240K, 46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000) compacting perm gen total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000) the space 12288K, 17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000) No shared spaces configured. 规则五:在 Minor GC 触发时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间,如 果大于,改为直接进行一次 Full GC,如果小于则查看 HandlePromotionFailure 设置看看是否允许担保失 败,如果允许,那仍然进行 Minor GC,如果不允许,则也要改为进行一次 Full GC。 前面提到过,新生代才有复制收集算法,但为了内存利用率,只使用其中一个 survivor 空间来作为轮 换备份,因此当出现大量对象在 GC 后仍然存活的情况(最极端就是 GC 后所有对象都存活),就需要老 年代进行分配担保,把 survivor 无法容纳的对象直接放入老年代。与生活中贷款担保类似,老年代要进行 这样的担保,前提就是老年代本身还有容纳这些对象的剩余空间,一共有多少对象在 GC 之前是无法明确 知道的,所以取之前每一次 GC 晋升到老年代对象容量的平均值与老年代的剩余空间进行比较决定是否进 行 Full GC 来让老年代腾出更多空间。 取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次 Minor GC 存活后的对象突增, 大大高于平均值的话,依然会导致担保失败,这样就只好在失败后重新进行一次 Full GC。虽然担保失败时 做的绕的圈子是最大的,但大部分情况下都还是会将 HandlePromotionFailure 打开,避免 Full GC 过于频 繁。 清单 6: HandlePromotionFailure = false

[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs] [GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

HandlePromotionFailure = true

[GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

总结
本章介绍了垃圾收集的算法、6 款主要的垃圾收集器,以及通过代码实例具体介绍了新生代串行收集 器对内存分配及回收的影响。 GC 在很多时候都是系统并发度的决定性因素,虚拟机之所以提供多种不同的收集器,提供大量的调 节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最好的性能。没有固定收 集器、参数组合,也没有最优的调优方法,虚拟机也没有什么必然的行为。笔者看过一些文章,撇开具体 场景去谈论老年代达到 92%会触发 Full GC(92%应当来自 CMS 收集器触发的默认临界点)、98%时间在 进行垃圾收集系统会抛出 OOM 异常 (98%应该来自 parallel 收集器收集时间比率的默认临界点) 其实意义 并不太大。 因此学习 GC 如果要到实践调优阶段, 必须了解每个具体收集器的行为、 优势劣势、 调节参数。

JVM 调优总结(一)
数据类型
Java 虚拟机中,数据类型可以分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他 代表的值就是数值本身;而引用类型的变量保存引用值。―引用值‖代表了某个对象的引用,而不是对象本 身,对象本身存放在这个引用值所表示的地址的位置。 基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress 引用类型包括:类类型,接口类型和数组。

堆与栈
堆和栈是程序运行的关键,很有必要把他们的关系说清楚。

栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据 怎么放、放在哪儿。 在 Java 中一个线程就会相应有一个线程栈与之对应,这点很容易理解,因为不同的线程执行逻辑有所 不同,因此需要一个独立的线程栈。而堆则是所有线程共享的。栈因为是运行单位,因此里面存储的信息 都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存 储对象信息。

为什么要把堆和栈区分出来呢?栈中不是也可以存储数据吗?
第一,从软件设计的角度看,栈代表了处理逻辑,而堆代表了数据。这样分开,使得处理逻辑更为清晰。 分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。 第二,堆与栈的分离,使得堆中的内容可以被多个栈共享(也可以理解为多个线程访问同一个对象)。 这种共享的收益是很多的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另一方面, 堆中的共享常量和缓存可以被所有栈访问,节省了空间。 第三,栈因为运行时的需要,比如保存系统运行的上下文,需要进行地址段的划分。由于栈只能向上增 长,因此就会限制住栈存储内容的能力。而堆不同,堆中的对象是可以根据需要动态增长的,因此栈和堆 的拆分,使得动态增长成为可能,相应栈中只需记录堆中的一个地址即可。 第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与以前结构化的程序在执行上没有 任何区别。但是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于自然方式的思考。 当我们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运 行逻辑,放在栈中。我们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不 承认,面向对象的设计,确实很美。

在 Java 中,Main 函数就是栈的起始点,也是程序的起始点。
程序要运行总是有一个起点的。同 C 语言一样,java 中的 Main 就是那个起点。无论什么 java 程序,找 到 main 就找到了程序执行的入口:)

堆中存什么?栈中存什么?
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说 是可以动态变化的,但是在栈中,一个对象只对应了一个 4btye 的引用(堆栈分离的好处:))。 为什么不把基本类型放堆中呢?因为其占用的空间一般是 1~8 个字节——需要空间比较少, 而且因为是 基本类型,所以不会出现动态增长的情况——长度固定,因此栈中存储就够了,如果把他存在堆中是没有 什么意义的(还会浪费空间,后面说明)。可以这么说,基本类型和对象的引用都是存放在栈中,而且都 是几个字节的一个数,因此在程序运行时,他们的处理方式是统一的。但是基本类型、对象引用和对象本 身就有所区别了,因为一个是栈中的数据一个是堆中的数据。最常见的一个问题就是,Java 中参数传递时 的问题。

Java 中的参数传递时传值呢?还是传引用?
要说明这个问题,先要明确两点: 1. 不要试图与 C 进行类比,Java 中没有指针的概念 2. 程序运行永远都是在栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。不 会直接传对象本身。 明确以上两点后。Java 在方法调用传递参数时,因为没有指针,所以它都是进行传值调用(这点可以 参考 C 的传值调用)。因此,很多书里面都说 Java 是进行传值调用,这点没有问题,而且也简化的 C 中 复杂性。

但是传引用的错觉是如何造成的呢?在运行栈中,基本类型和引用的处理是一样的,都是传值,所以,
如果是传引用的方法调用,也同时可以理解为―传引用值‖的传值调用,即引用的处理跟基本类型是完全一 样的。但是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个 时候才对应到真正的对象。如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:修改的 是堆中的数据。所以这个修改是可以保持的了。 对象,从某种意义上说,是由基本类型组成的。可以把一个对象看作为一棵树,对象的属性如果还是对 象,则还是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值本身都 是不能进行修改的,但是,如果这个值是一个非叶子节点(即一个对象引用),则可以修改这个节点下面 的所有内容。 堆和栈中,栈是程序运行最根本的东西。程序运行可以没有堆,但是不能没有栈。而堆是为栈进行数据 存储服务,说白了堆就是一块共享的内存。不过,正是因为堆和栈的分离的思想,才使得 Java 的垃圾回收 成为可能。 Java 中,栈的大小通过-Xss 来设置,当栈中存储数据比较多时,需要适当调大这个值,否则会出现 java.lang.StackOverflowError 异常。常见的出现这个异常的是无法返回的递归,因为此时栈中保存的信息 都是方法返回的记录点。

JVM 调优总结(二)
Java 对象的大小
基本数据的类型的大小是固定的,这里就不多说了。对于非基本类型的 Java 对象,其大小就值得商榷。

在 Java 中, 一个空

Object 对象的大小是 8byte,这个大小只是保存堆中一个没有任何属性

的对象的大小。看下面语句: Object ob = new Object(); 这样在程序中完成了一个 Java 对象的生命,但是它所占的空间为:4byte+8byte。4byte 是上面部 分所说的 Java 栈中保存引用的所需要的空间。而那 8byte 则是 Java 堆中对象的信息。因为所有的 Java 非基本类型的对象都需要默认继承 Object 对象, 因此不论什么样的 Java 对象, 其大小都必须是大于 8byte。 有了 Object 对象的大小,我们就可以计算其他对象的大小了。 Class NewObject { int count; boolean flag; Object ob; } 其大小为:空对象大小(8byte)+int 大小(4byte)+Boolean 大小(1byte)+空 Object 引用的大小 (4byte)=17byte。 但是因为 Java 在对对象内存分配时都是以 8 的整数倍来分, 因此大于 17byte 的最接近 8 的整数倍的是 24,因此此对象的大小为 24byte。 这里需要注意一下基本类型的包装类型的大小。因为这种包装类型已经成为对象了,因此需要 把他们作为对象来看待。 包装类型的大小至少是 12byte (声明一个空 Object 至少需要的空间) 而且 12byte , 没有包含任何有效信息,同时,因为 Java 对象大小是 8 的整数倍,因此一个基本类型包装类的大

小至少是 16byte。这个内存占用是很恐怖的,它是使用基本类型的 N 倍(N>2),有些类型的内存
占用更是夸张(随便想下就知道了)。因此,可能的话应尽量少使用包装类。在 JDK5.0 以后,因为加入 了自动类型装换,因此,Java 虚拟机会在存储方面进行相应的优化。

引用类型
对象引用类型分为强引用、软引用、弱引用和虚引用。

强引用:就是我们一般声明对象是时虚拟机生成的引用,强引用环境下,垃圾回收时需要严格判断当前对
象是否被强引用,如果被强引用,则不会被垃圾回收

软引用:软引用一般被做为缓存来使用。与强引用的区别是,软引用在垃圾回收时,虚拟机会根据当前系
统的剩余内存来决定是否对软引用进行回收。如果剩余内存比较紧张,则虚拟机会回收软引用所引用的空 间;如果剩余内存相对富裕,则不会进行回收。换句话说,虚拟机在发生 OutOfMemory 时,肯定是没有 软引用存在的。

弱引用:弱引用与软引用类似,都是作为缓存来使用。但与软引用不同,弱引用在进行垃圾回收时,是一
定会被回收掉的,因此其生命周期只存在于一个垃圾回收周期内。 强引用不用说,我们系统一般在使用时都是用的强引用。而―软引用‖和―弱引用‖比较少见。他们一般被 作为缓存使用,而且一般是在内存大小比较受限的情况下做为缓存。因为如果内存足够大的话,可以直接 使用强引用作为缓存即可,同时可控性更高。因而,他们常见的是被使用在桌面应用系统的缓存。

JVM 调优总结(三)-基本垃圾回收算 法
可以从不同的的角度去划分垃圾回收算法:

按照基本回收策略分
引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾 回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用的问题。

标记-清除(Mark-Sweep):

此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,把未标 记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。

复制(Copying):

此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把 正在使用中的对象复制到另外一个区域中。次算法每次只处理正在使用中的对象,因此复制成本比较小, 同时复制过去以后还能进行相应的内存整理,不会出现―碎片‖问题。当然,此算法的缺点也是很明显的, 就是需要两倍内存空间。

标记-整理(Mark-Compact):

此算法结合了―标记-清除‖和―复制‖两个算法的优点。也是分两阶段,第一阶段从根节点开始标记所有被引 用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象―压缩‖到堆的其中一块,按顺序排放。 此算法避免了―标记-清除‖的碎片问题,同时也避免了―复制‖算法的空间问题。

按分区对待的方式分
增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾
回收。不知道什么原因 JDK5.0 中的收集器没有使用这种算法的。

分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把
对象分为年青代、年老代、持久代,对不同生命周期的对象使用不同的算法(上述方式中的一个)进行回 收。现在的垃圾回收器(从 J2SE1.2 开始)都是使用此算法的。

按系统线程分
串行收集:串行收集使用单线程处理所有垃圾回收工作,因为无需多线程交互,实现容易,而且效率比
较高。但是,其局限性也比较明显,即无法使用多处理器的优势,所以此收集适合单处理器机器。当然, 此收集器也可以用在小数据量(100M 左右)情况下的多处理器机器上。

并行收集:并行收集使用多线程处理垃圾回收工作,因而速度快,效率高。而且理论上 CPU 数目越多,
越能体现出并行收集器的优势。

并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工作时,需要暂停整个运行环境,
而只有垃圾回收程序在运行,因此,系统在垃圾回收时会有明显的暂停,而且暂停时间会因为堆越大而越 长。

JVM 调优总结(四)-垃圾回收面临的 问题
如何区分垃圾
上面说到的―引用计数‖法,通过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计 数为 0 的对象即可。但是这种方法无法解决循环引用。所以,后来实现的垃圾判断算法中,都是从程序运 行的根节点出发,遍历整个对象引用,查找存活的对象。那么在这种方式的实现中,垃圾回收从哪儿开始 的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别,其中栈是真正 进行程序执行地方,所以要获取哪些对象正在被使用,则需要从 Java 栈开始。同时,一个栈是与一个线程 对应的,因此,如果有多个线程的话,则必须对这些线程对应的所有的栈进行检查。

同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引 用为起点,我们可以找到堆中的对象,又从这些对象找到对堆中其他对象的引用,这种引用逐步扩展,最 终以 null 引用或者基本类型结束,这样就形成了一颗以 Java 栈中引用所对应的对象为根节点的一颗对象 树,如果栈中有多个引用,则最终会形成多颗对象树。在这些对象树上的对象,都是当前系统运行所需要 的对象,不能被垃圾回收。而其他剩余对象,则可以视为无法被引用到的对象,可以被当做垃圾进行回收。 因此,垃圾回收的起点是一些根对象(java 栈, 静态变量, 寄存器...)。而最简单的 Java 栈就是 Java 程 序执行的 main 函数。这种回收方式,也是上面提到的―标记-清除‖的回收方式

如何处理碎片
由于不同 Java 对象存活时间是不一定的,因此,在程序运行一段时间以后,如果不进行内存整理,就 会出现零散的内存碎片。碎片最直接的问题就是会导致无法分配大块的内存空间,以及程序运行效率降低。 所以,在上面提到的基本垃圾回收算法中,―复制‖方式和―标记-整理‖方式,都可以解决碎片的问题。

如何解决同时存在的对象创建和对象回收问题
垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内 存,从这点看,两者是矛盾的。因此,在现有的垃圾回收方式中,要进行垃圾回收前,一般都需要暂停整 个应用(即:暂停内存的分配),然后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接, 而且最有效的解决二者矛盾的方式。 但是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大, 对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,比如最大暂停时间要求是几百毫 秒,那么当堆空间大于几个 G 时,就很有可能超过这个限制,在这种情况下,垃圾回收将会成为系统运行 的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同 时运行。在这种方式下,解决了暂停的问题,但是因为需要在新生成对象的同时又要回收对象,算法复杂 性会大大增加,系统的处理能力也会相应降低,同时,―碎片‖问题将会比较难解决。

JVM 调优总结(五)-分代垃圾回收详 述1
为什么要分代
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不 同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象, 主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类 的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。 试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对 会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历 是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想, 进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。

如何分代

如图所示:

Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是 Java 类
的类信息,与垃圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。

虚拟机中的共划分为三个代:年轻代(Young

年轻代:
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对 象。年轻代分三个区。一个 Eden 区,两个 Survivor 区(一般而言)。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时,此区的存活对 象将被复制到另外一个 Survivor 区,当这个 Survivor 去也满了的时候,从第一个 Survivor 区复制过来的并 且此时还存活的对象,将被复制―年老区(Tenured)‖。需要注意,Survivor 的两个区是对称的,没先后关系, 所以同一个区中可能同时存在从 Eden 复制过来 对象,和从前一个 Survivor 复制过来的对象,而复制到年 老区的只有从第一个 Survivor 去过来的对象。而且,Survivor 区总有一个是空的。同时,根据程序需要, Survivor 区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年 老代的可能。

年老代:
在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存 放的都是一些生命周期较长的对象。

持久代:

用于存放静态文件,如今 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态 生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运 行过程中新增的类。持久代大小通过-XX:MaxPermSize=<N>进行设置。

什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC 有两种类型:Scavenge 和 Full

GC

GC。

Scavenge GC
一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进 行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种 方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同 时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率 高的算法,使 Eden 去能尽快空闲出来。

Full GC
对整个堆进行整理,包括 Young、Tenured 和 Perm。Full GC 因为需要对整个对进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就 是对于 FullGC 的调节。有如下原因可能导致 Full GC: · 年老代(Tenured)被写满 · 持久代(Perm)被写满 ·System.gc()被显示调用 · 上一次 GC 之后 Heap 的各域分配策略动态变化

JVM 调优总结(六)-分代垃圾回收详 述2

分代垃圾回收流程示意

选择合适的垃圾收集算法
串行收集器

用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的 优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M 左右)情况下的多 处理器机器上。可以使用-XX:+UseSerialGC 打开。

并行收集器

对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用 -XX:+UseParallelGC.打开。并行收集器在 J2SE5.0 第六 6 更新上引入,在 Java SE6.0 中进行了增强--可 以对年老代进行并行收集。如果年老代不使用并发收集的话,默认是使用单线程进行垃圾回收,因此会制 约扩展能力。使用-XX:+UseParallelOldGC 打开。 使用-XX:ParallelGCThreads=<N>设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。 此收集器可以进行如下配置:

最大垃圾回收暂停:指定垃圾回收时的最长暂停时间,通过-XX:MaxGCPauseMillis=<N>指定。<N>
为毫秒.如果指定了此值的话,堆大小和垃圾回收相关参数会进行调整以达到指定值。设定此值可能会减少 应用的吞吐量。

吞吐量:吞吐量为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=<N>来设定,公式为
1/(1+N)。例如,-XX:GCTimeRatio=19 时,表示 5%的时间用于垃圾回收。默认情况为 99,即 1%的时 间用于垃圾回收。

并发收集器
可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间 要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC 打开。

并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达 对象。在每个年老代垃圾回收周期中,在收集初期并发收集器 会对整个应用进行简短的暂停,在收集中还 会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。 并发收集器使用处理器换来短暂的停顿时间。在一个 N 个处理器的系统上,并发收集部分使用 K/N 个 可用处理器进行回收,一般情况下 1<=K<=N/4。 在只有一个处理器的主机上使用并发收集器,设置为 incremental mode 模式也可获得较短的停顿时间。

浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,
这样就造成了―Floating Garbage‖,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一 般需要 20%的预留空间用于这些浮动垃圾。

Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收
的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生―并 发模式失败‖,此时整个应用将会暂停,进行垃圾回收。

启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内
存空间供程序使用,否则会出现―Concurrent Mode Failure‖。通过设置 -XX:CMSInitiatingOccupancyFraction=<N>指定还有多少剩余堆时开始执行并发收集

小结
串行处理器:

--适用情况:数据量比较小(100M 左右);单处理器下并且对响应时间无要求的应用。 --缺点:只能用于小型应用

并行处理器:
--适用情况:―对吞吐量有高要求‖,多 CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、 科学计算。 --缺点:垃圾收集过程中应用响应时间可能加长

并发处理器:
--适用情况:―对响应时间有高要求‖,多 CPU、对应用响应时间有较高要求的中、大型应用。举例:Web 服务器/应用服务器、电信交换、集成开发环境。

JVM 调优总结(七)-典型配置举例 1
以下配置主要针对分代垃圾回收算法而言。

堆大小设置
年轻代的设置很关键 JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt 还是 64-bit)限制;系统的可用虚拟内 存限制;系统的可用物理内存限制。32 位系统下,一般限制在 1.5G~2G;64 为操作系统对内存无限制。 在 Windows Server 2003 系统,3.5G 物理内存,JDK5.0 下测试,最大可设置为 1478m。

典型设置: java -Xmx3550m -Xms3550m -Xmn2g –Xss128k -Xmx3550m:设置 JVM 最大可用内存为 3550M。 -Xms3550m:设置 JVM 促使内存为 3550m。此值可以设置与-Xmx 相同,以避免每次垃圾回收完成
后 JVM 重新分配内存。

-Xmn2g:设置年轻代大小为 2G。整个堆大小=年轻代大小
置为整个堆的 3/8。

+ 年老代大小 + 持久代大小。持久代一般

固定大小为 64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun 官方推荐配

-Xss128k:设置每个线程的堆栈大小。JDK5.0 以后每个线程堆栈大小为 1M,以前每个线程堆栈大小
为 256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但 是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在 3000~5000 左右。 java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4

-XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0 -XX:NewRatio=4:设置年轻代(包括 Eden 和两个 Survivor 区)与年老代的比值(除去持久代)。设
置为 4,则年轻代与年老代所占比值为 1:4,年轻代占整个堆栈的 1/5

-XX:SurvivorRatio=4: 设置年轻代中 Eden 区与 Survivor 区的大小比值。 设置为 4, 则两个 Survivor
区与一个 Eden 区的比值为 2:4,一个 Survivor 区占整个年轻代的 1/6

-XX:MaxPermSize=16m:设置持久代大小为 16m。

-XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经
过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大 值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代 即被回收的概论。

回收器选择
JVM 给了三种选择:串行收集器、并行收集器、并发收集器,但是串行收集器只适用于小数 据量的情况,所以这里的选择主要针对并行收集器和并发收集器。默认情况下,JDK5.0 以前都是使用串行 收集器,如果想使用其他收集器需要在启动时加入相应参数。JDK5.0 以后,JVM 会根据当前系统配置进 行判断。

吞吐量优先的并行收集器
如上文所述,并行收集器主要以到达一定的吞吐量为目标,适用于科学技术和后台处理等。

典型配置:
java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC

-XX:ParallelGCThreads=20 -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述 配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。 -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回
收。此值最好配置与处理器数目相等。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0 支持对年老代并行收
集。 java -Xmx3550m -Xms3550m -Xmn2g -Xss128k

-XX:MaxGCPauseMillis=100 -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间, 如果无法满足此时间, JVM
-XX:+UseParallelGC 会自动调整年轻代大小,以满足此值。 n java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的
Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一 直打开。

响应时间优先的并发收集器
如上文所述,并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务器、 电信领域等。

典型配置:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC

-XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,
-XX:NewRatio=4 的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn 设置。

-XX:+UseParNewGC: 设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM 会
根据系统配置自行设置,所以无需再设置此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k

-XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所
-XX:+UseConcMarkSweepGC 以运行一段时间以后会产生―碎片‖,使得运行效率降低。此值设置运行多少次 GC 以后对内存空间进行压 缩、整理。

-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是
可以消除碎片

辅助信息
JVM 提供了大量命令行参数,打印信息,供调试使用。主要有以下一些:

-XX:+PrintGC:输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC
121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails:输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs]
118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps 可与上面两个混合使用
输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:打印每次垃圾回收前,程序未中断的执行时
间。可与上面混合使用。输出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间。可与上面混
合使用。输出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 打印 GC 前后的详细堆栈信息。输出形式:
34.702: [GC {Heap before gc invocations=7: def new generation total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K, 99% used [0x1ebd0000, 0x21bce430, 0x21bd0000) from space 6144K, 55% used [0x221d0000, 0x22527e10, 0x227d0000) to space 6144K, 0% used [0x21bd0000, 0x21bd0000, 0x221d0000) total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000) 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)

tenured generation the space 69632K,

compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000) the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000) ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000) rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000) 34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8: def new generation eden space 49152K, total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000) 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)

from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000) to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000) total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000) 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)

tenured generation the space 69632K,

compacting perm gen total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000) the space 8192K, 35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000) ro space 8192K, 66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000) rw space 12288K, 46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000) } , 0.0757599 secs]

-Xloggc:filename:与上面几个配合使用,把相关日志信息记录到文件以便分析。

JVM 调优总结(八)-典型配置举例 2
常见配置汇总
堆设置 -Xms:初始堆大小 -Xmx:最大堆大小 -XX:NewSize=n:设置年轻代大小 -XX:NewRatio=n:设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:3,年轻
代占整个年轻代年老代和的 1/4

-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。 Survivor 区有两个。 注意 如:
3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5

-XX:MaxPermSize=n:设置持久代大小 收集器设置 -XX:+UseSerialGC:设置串行收集器 -XX:+UseParallelGC:设置并行收集器 -XX:+UseParalledlOldGC:设置并行年老代收集器 -XX:+UseConcMarkSweepGC:设置并发收集器 垃圾回收统计信息 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:filename 并行收集器设置 -XX:ParallelGCThreads=n:设置并行收集器收集时使用的 CPU 数。并行收集线程数。 -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间 -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n) 并发收集器设置 -XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况。 -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的 CPU 数。
并行收集线程数。

调优总结
年轻代大小选择 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在
此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集
可以并行进行,一般适合 8CPU 以上的应用。

年老代大小选择 响应时间优先的应用: 年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话 率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而
使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据 获得: 1. 并发垃圾收集信息 2. 持久代并发收集次数 3. 传统 GC 信息 4. 花在年轻代和年老代回收上的时间比例 减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用
一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部 分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题
因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的 空间进行合并, 这样可以分配给较大的对象。 但是, 当堆空间较小时, 运行一段时间以后, 就会出现―碎片‖, 如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。 如果出现―碎片‖,可能需要进行如下配置: 1. -XX:+UseCMSCompactAtFullCollection: 使用并发收集器时, 开启对年老代的压缩。 2. -XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC 后,对年老代进行压缩

JVM 调优总结(九)-新一代的垃圾回 收算法

垃圾回收的瓶颈
传统分代垃圾回收方式,已经在一定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量 推到了一个极限。但是他无法解决的一个问题,就是 Full GC 所带来的应用暂停。在一些对实时性要求很 高的应用场景下,GC 暂停所带来的请求堆积和请求失败是无法接受的。这类应用可能要求请求的返回时 间在几百甚至几十毫秒以内,如果分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相 对较小范围内,但是这样有限制了应用本身的处理能力,同样也是不可接收的。 分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,但是受限 于分代垃圾回收的内存划分模型,其效果也不是很理想。 为了达到实时性的要求(其实 Java 语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼 之欲出,它既支持短的暂停时间,又支持大的内存空间分配。可以很好的解决传统分代方式带来的问题。

增量收集的演进
增量收集的方式在理论上可以解决传统分代方式带来的问题。增量收集把对堆空间划分成一系列内存 块,使用时,先使用其中一部分(不会全部用完),垃圾收集时把之前用掉的部分中的存活对象再放到后 面没有用的空间中,这样可以实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的 回收的情况。 当然,传统分代收集方式也提供了并发收集,但是他有一个很致命的地方,就是把整个堆做为一个内存 块,这样一方面会造成碎片(无法压缩),另一方面他的每次收集都是对整个堆的收集,无法进行选择, 在暂停时间的控制上还是很弱。而增量方式,通过内存空间的分块,恰恰可以解决上面问题。

Garbage Firest(G1)
这部分的内容主要参考这里,这篇文章算是对 G1 算法论文的解读。我也没加什么东西了。

目标 从设计目标看 G1 完全是为了大型应用而准备的。 支持很大的堆 高吞吐量 --支持多 CPU 和垃圾回收线程 --在主线程暂停的情况下,使用并行收集 --在主线程运行的情况下,使用并发收集 实时目标:可配置在 N 毫秒内最多只占用 M 毫秒的时间进行垃圾回收 当然 G1 要达到实时性的要求,相对传统的分代回收算法,在性能上会有一些损失。

算法详解

G1 可谓博采众家之长,力求到达一种完美。他吸取了增量收集优点,把整个堆划分为一个一个等大小 的区域(region)。内存的回收和划分都以 region 为单位;同时,他也吸取了 CMS 的特点,把这个垃圾 回收过程分为几个阶段,分散一个垃圾回收过程;而且,G1 也认同分代垃圾回收的思想,认为不同对象的 生命周期不同,可以采取不同收集方式,因此,它也支持分代的垃圾回收。为了达到对回收时间的可预计 性,G1 在扫描了 region 以后,对其中的活跃对象的大小进行排序,首先会收集那些活跃对象小的 region, 以便快速回收空间(要复制的活跃对象少了),因为活跃对象小,里面可以认为多数都是垃圾,所以这种 方式被称为 Garbage First(G1)的垃圾回收算法,即:垃圾优先的回收。

回收步骤: 初始标记(Initial Marking) G1 对于每个 region 都保存了两个标识用的 bitmap,一个为 previous marking bitmap,一个为 next marking bitmap,bitmap 中包含了一个 bit 的地址信息来指向对象的起始点。 开始 Initial Marking 之前,首先并发的清空 next marking bitmap,然后停止所有应用线程,并扫描标识 出每个 region 中 root 可直接访问到的对象,将 region 中 top 的值放入 next top at mark start(TAMS)中, 之后恢复所有应用线程。 触发这个步骤执行的条件为: G1 定义了一个 JVM Heap 大小的百分比的阀值,称为 h,另外还有一个 H,H 的值为(1-h)*Heap Size, 目前这个 h 的值是固定的,后续 G1 也许会将其改为动态的,根据 jvm 的运行情况来动态的调整,在分代 方式下,G1 还定义了一个 u 以及 soft limit,soft limit 的值为 H-u*Heap Size,当 Heap 中使用的内存超过 了 soft limit 值时,就会在一次 clean up 执行完毕后在应用允许的 GC 暂停时间范围内尽快的执行此步骤; 在 pure 方式下,G1 将 marking 与 clean up 组成一个环, 以便 clean up 能充分的使用 marking 的信息, 当 clean up 开始回收时,首先回收能够带来最多内存空间的 regions,当经过多次的 clean up,回收到没 多少空间的 regions 时,G1 重新初始化一个新的 marking 与 clean up 构成的环。 并发标记(Concurrent Marking) 按照之前 Initial Marking 扫描到的对象进行遍历,以识别这些对象的下层对象的活跃状态,对于在此期 间应用线程并发修改的对象的以来关系则记录到 remembered set logs 中,新创建的对象则放入比 top 值 更高的地址区间中,这些新创建的对象默认状态即为活跃的,同时修改 top 值。

最终标记暂停(Final Marking Pause) 当应用线程的 remembered set logs 未满时,是不会放入 filled RS buffers 中的,在这样的情况下,这 些 remebered set logs 中记录的 card 的修改就会被更新了,因此需要这一步,这一步要做的就是把应用线

程中存在的 remembered set logs 的内容进行处理,并相应的修改 remembered sets,这一步需要暂停应 用,并行的运行。

存活对象计算及清除(Live Data Counting and Cleanup) 值得注意的是,在 G1 中,并不是说 Final Marking Pause 执行完了,就肯定执行 Cleanup 这步的,由 于这步需要暂停应用, 为了能够达到准实时的要求, G1 需要根据用户指定的最大的 GC 造成的暂停时间来 合理的规划什么时候执行 Cleanup,另外还有几种情况也是会触发这个步骤的执行的: G1 采用的是复制方法来进行收集,必须保证每次的‖to space‖的空间都是够的,因此 G1 采取的策略是 当已经使用的内存空间达到了 H 时,就执行 Cleanup 这个步骤; 对于 full-young 和 partially-young 的分代模式的 G1 而言, 则还有情况会触发 Cleanup 的执行, full-young 模式下, 根据应用可接受的暂停时间、 G1 回收 young regions 需要消耗的时间来估算出一个 yound regions 的数量值,当 JVM 中分配对象的 young regions 的数量达到此值时,Cleanup 就会执行;partially-young 模式下,则会尽量频繁的在应用可接受的暂停时间范围内执行 Cleanup,并最大限度的去执行 non-young regions 的 Cleanup。

展望
以后 JVM 的调优或许跟多需要针对 G1 算法进行调优了。

JVM 调优总结(十)-调优方法
JVM 调优工具
Jconsole,jProfile,VisualVM Jconsole : jdk 自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细
的跟踪。详细说明参考这里

JProfiler:商业软件,需要付费。功能强大。详细说明参考这里 VisualVM:JDK 自带,功能强大,与 JProfiler 类似。推荐。

如何调优
观察内存释放情况、集合类检查、对象树 上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能

堆信息查看

可查看堆空间大小分配(年轻代、年老代、持久代分配) 提供即时的垃圾回收功能 垃圾监控(长时间监控回收情况)

查看堆内类、对象信息查看:数量、类型等

对象引用情况查看 有了堆信息查看方面的功能,我们一般可以顺利解决以下问题: --年老代年轻代大小划分是否合理 --内存泄漏 --垃圾回收算法设置是否合理

线程监控

线程信息监控:系统线程数量。 线程状态监控:各个线程都处在什么样的状态下

Dump 线程详细信息:查看线程内部运行情况 死锁检查

热点分析

CPU 热点:检查系统哪些方法占用的大量 CPU 时间 内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的进行系统的瓶颈查找和进行 系统优化,而不是漫无目的的进行所有代码的优化。

快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用眼睛去跟踪所有系统变化, 依赖快照功能,我们就可以进行系统两个不同运行时刻,对象(或类、线程等)的不同,以便快速找到问 题 举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下来的了。那么,我可以在进 行垃圾回收前后,分别进行一次堆情况的快照,然后对比两次快照的对象情况。

内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一下,而线程、热点方面的问 题则是具体问题具体分析了。 内存泄漏一般可以理解为系统资源(各方面的资源,堆、栈、线程等)在错误使用的情况下,导致使用 完毕的资源无法回收(或没有回收),从而导致新的资源分配请求无法完成,引起系统错误。 内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。 需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最终结果是一样的。内存泄漏 是用完的资源没有回收引起错误,而系统超负荷则是系统确实没有那么多资源可以分配了(其他的资源都 在使用)。

年老代堆空间被占满 异常: java.lang.OutOfMemoryError: Java heap space 说明:

这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对象占满,虚拟机无法再在分 配新空间。 如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都是一次垃圾回收点,所有谷 底部分表示是一次垃圾回收后剩余的内存。连接所有谷底的点,可以发现一条由底到高的线,这说明,随

时间的推移,系统的堆空间被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存 泄漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小时或者几天)

解决:
这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的 集合对象引用)分析,基本都可以找到泄漏点。

持久代被占满 异常:java.lang.OutOfMemoryError: PermGen space 说明:
Perm 空间被占满。 无法为新的 class 分配存储空间而引发的异常。 这个异常以前是没有的, 但是在 Java 反射大量使用的今天这个异常比较常见了。主要原因就是大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。 更可怕的是,不同的 classLoader 即便使用了相同的类,但是都会对其进行加载,相当于同一个东西, 如果有 N 个 classLoader 那么他将会被加载 N 次。因此,某些情况下,这个问题基本视为无解。当然,存 在大量 classLoader 和大量反射类的情况其实也不多。

解决:
1. -XX:MaxPermSize=16m 2. 换用 JDK。比如 JRocket。

堆栈溢出 异常:java.lang.StackOverflowError 说明:这个就不多说了,一般就是递归没返回,或者循环调用造成

线程堆栈满 异常:Fatal: Stack size too small 说明:java 中一个线程的空间大小是有限制的。JDK5.0 以后这个值是 1M。与这个线程相关的数据将会
保存在其中。但是当线程空间满了以后,将会出现上面异常。

解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看代码部分是否有造成泄漏的部
分。

系统内存被占满 异常:java.lang.OutOfMemoryError: unable to create new native thread 说明:
这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系统创建线程时,除了要在 Java 堆 中分配内存外,操作系统本身也需要分配资源来创建线程。因此,当线程数量大到一定程度以后,堆中或 许还有空间,但是操作系统分配不出资源来了,就出现这个异常了。 分配给 Java 虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内存固定时,分配给 Java 虚拟机 的内存越多,那么,系统总共能够产生的线程也就越少,两者成反比的关系。同时,可以通过修改-Xss 来 减少分配给单个线程的空间,也可以增加系统总共内生产的线程数。

解决:

1. 重新设计系统减少线程数量。 2. 线程数量不能减少的情况下,通过-Xss 减小单个线程大小。以便能生产更多的线程。

JVM 几个重要的参数
<本文提供的设置仅仅是在高压力, 多 CPU, 高内存环境下设置> 最近对 JVM 的参数重新看了下, 把应用的 JVM 参数调整了下。 几个重要的 参数 -server -Xmx3g -Xms3g -XX:MaxPermSize=128m -XX:NewRatio=1 eden/old 的比例 -XX:SurvivorRatio=8 s/e 的比例 -XX:+UseParallelGC -XX:ParallelGCThreads=8 -XX:+UseParallelOldGC 这个是 JAVA 6 出现的参数选项 -XX:LargePageSizeInBytes=128m 内存页的大小, 不可设置过大, 会影响 Perm 的大小。 -XX:+UseFastAccessorMethods 原始类型的快速优化 -XX:+DisableExplicitGC 关闭 System.gc()

另外 -Xss 是线程栈的大小, 这个参数需要严格的测试, 一般小的应用, 如 果栈不是很深, 应该是 128k 够用的, 不过,我们的应用调用深度比较大, 还 需要做详细的测试。 这个选项对性能的影响比较大。 建议使用 256K 的大小.
例子: -server -Xmx3g -Xms3g -Xmn=1g -XX:MaxPermSize=128m -Xss256k -XX:MaxTenuringThreshold=10 -XX:+DisableExplicitGC -XX:+UseParallelGC -XX:+UseParallelOld GC -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+AggressiveOpts -XX:+UseBiasedLocking -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintGCDetails 打印参数 ================================================================= 另外对于大内存设置的要求: Linux : Large page support is included in 2.6 kernel. Some vendors have backported the code to their 2.4 based releases. To check if your system can support large page memory, try the following: # cat /proc/meminfo | grep Huge HugePages_Total: 0 HugePages_Free: 0 Hugepagesize: 2048 kB #

If the output shows the three "Huge" variables then your system can support large page memory, but it needs to be configured. If the command doesn't print out anything, then large page support is not available. To configure the system to use large page memory, one must log in as root, then: 1. Increase SHMMAX value. It must be larger than the Java heap size. On a system with 4 GB of physical RAM (or less) the following will make all the memory sharable:

# echo 4294967295 > /proc/sys/kernel/shmmax 2. Specify the number of large pages. In the following example 3 GB of a 4 GB system are reserved for large pages (assuming a large page size of 2048k, then 3g = 3 x 1024m = 3072m = 3072 * 1024k = 3145728k, and 3145728k / 2048k = 1536):

# echo 1536 > /proc/sys/vm/nr_hugepages Note the /proc values will reset after reboot so you may want to set them in an init script (e.g. rc.local or sysctl.conf).

============================================= 这个设置, 目前观察下来的结果是 EDEN 区域收集明显速度比较快, 最多几 个 ms, 但是,对于 FGC, 大约需要 0。9, 但是发生时间非常的长, 应该是 影响不大。 但是对于非 web 应用的中间件服务, 这个设置很要不得, 可能导致 很严重延迟效果. 因此, CMS 必然需要被使用, 下面是 CMS 的重要参数介绍 关于 CMS 的设置:
使用 CMS 的前提条件是你有比较的长生命对象, 比如有 200M 以上的 OLD 堆占用。 那么这个威力非常 猛, 可以极大的提高的 FGC 的收集能力。 如果你的 OLD 占用非常的少, 别用了, 绝对降低你性能, 因为 CMS 收集有 2 个 STOP WORLD 的行为。 OLD 少的清情况, 根据我的测试, 使用并行收集参数 会比较好。

-XX:+UseConcMarkSweepGC 使用 CMS 内存收集 -XX:+AggressiveHeap 特别说明下:(我感觉对于做 java cache 应用有帮助) ? 试图是使用大量的物理内存
? ? ?

长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要 256MB 内存 大量的 CPU/内存, (在 1.4.1 在 4CPU 的机器上已经显示有提升)

-XX:+UseParNewGC 允许多线程收集新生代 -XX:+CMSParallelRemarkEnabled 降低标记停顿 -XX+UseCMSCompactAtFullCollection 在 FULL GC 的时候, 压缩内存, CMS 是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。

压力测试下合适结果:

-server -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xmx2g -Xms2g -Xmn256m -XX:PermSize=128m -Xss256k -XX:MaxTenuringThreshold=31 -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods 由于 Jdk1.5.09 及之前的 bug, 因此, CMS 下的 GC, 在这些版本的表现是十分糟糕的。 需要另外 2 个参数来控制 cms 的启动时间: -XX:+UseCMSInitiatingOccupancyOnly 仅仅使用手动定义初始化定义开始 CMS 收集 -XX:CMSInitiatingOccupancyFraction=70 CMS 堆上, 使用 70%后开始 CMS 收集。 使用 CMS 的好处是用尽量少的新生代、,我的经验值是 128M-256M, 然后老生代利用 CMS 并行收集, 这样能保证系统低延迟的吞吐效率。 实际上 cms 的收集停顿时间非常的短,2G 的内存, 大约 20-80ms 的应用程序停顿时间。

=========系统情况介绍======================== 这个例子是测试系统 12 小时运行后的情况: $uname -a 2.4.21-51.EL3.customsmp #1 SMP Fri Jun 27 10:44:12 CST 2008 i686 i686 i386 GNU/Linux

$ free -m total Mem: 3995 used 3910 2479 0 free shared 85 1515 2047 0 buffers 162 cached 1267

-/+ buffers/cache: Swap: 2047

$ jstat -gcutil 23959 1000 S0 59.06 S1 E O P YGC YGCT FGC FGCT 66 66 66 66 66 66 66 66 66 GCT

0.00 45.77 44.45 56.88 15204 324.023

1.668 325.691 1.668 325.715 1.668 325.741 1.668 325.762 1.668 325.786 1.668 325.816 1.668 325.840 1.668 325.840 1.668 325.866

0.00 39.66 27.53 44.73 56.88 15205 324.046 53.42 0.00 22.80 44.73 56.88 15206 324.073

0.00 44.90 13.73 44.76 56.88 15207 324.094 51.70 0.00 19.03 44.76 56.88 15208 324.118

0.00 61.62 19.44 44.98 56.88 15209 324.148 53.03 53.03 0.00 14.00 45.09 56.88 15210 324.172 0.00 87.87 45.09 56.88 15210 324.172

0.00 50.49 72.00 45.22 56.88 15211 324.198 GC 参数配置:

JAVA_OPTS=" -server -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xmx2g -Xms2g -Xmn256m -XX:PermSize=128m -Xss256k -XX:MaxTenuringThreshold=31 -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection

-XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 " 实际上我们可以看到并行 young gc 执行时间是: 324.198s/15211=20ms, cms 的执行时间是 1.668/ 66=25ms. 当然严格来说, 这么算是不对的, 世界停顿的时间要比这是数据稍微大 5-10ms. 对我们来 说如果不输出日志, 对我们是有参考意义的。 32 位系统下, 设置成 2G, 非常危险, 除非你确定你的应用占用的 native 内存很少, 不然可能导致 jvm 直接 crash。 -XX:+AggressiveOpts 加快编译 -XX:+UseBiasedLocking 锁机制的性能改善。

JVM 调优总结(十二)-参考资料
能整理出上面一些东西,也是因为站在巨人的肩上。下面是一些参考资料,供大家学习,大家有更好的, 可以继续完善:) ·Java ·Java

理论与实践: 垃圾收集简史 SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning

·Improving

Java Application Performance and Scalability by Reducing Garbage Collection Times and Sizing Memory Using JDK 1.4.1
·Hotspot ·Java

memory management whitepaper

Tuning White Paper a Garbage Collection problem

·Diagnosing ·Java ·A

HotSpot VM Options

Collection of JVM Options Garbage Collection

·Garbage-First ·Frequently

Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine ·JProfiler 试用手记

·Java6

JVM 参数选项大全 Java 虚拟机》。虽然过去了很多年,但这本书依旧是经典。

·《深入

猜你喜欢

转载自dannyhz.iteye.com/blog/2396660