深刻理解JVMhtml
提及Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成: Java编程语言、Java类文件格式、Java虚拟机和Java应用程序接口(Java API)。它们的关系以下图所示:java
图1 Java四个方面的关系程序员
运行期环境表明着Java平台,开发人员编写Java代码(.java文件),而后将之编译成字节码(.class文件)。最后字节码被装入内存,一旦字节码进入虚拟机,它就会被解释器解释执行,或者是被即时代码发生器有选择的转换成机器码执行。从上图也能够看出Java平台由Java虚拟机和 Java应用程序接口搭建,Java语言则是进入这个平台的通道,用Java语言编写并编译的程序能够运行在这个平台上。这个平台的结构以下图所示:web
在Java平台的结构中, 能够看出,Java虚拟机(JVM) 处在核心的位置,是程序与底层操做系统和硬件无关的关键。它的下方是移植接口,移植接口由两部分组成:适配器和Java操做系统, 其中依赖于平台的部分称为适配器;JVM 经过移植接口在具体的平台和操做系统上实现;在JVM 的上方是Java的基本类库和扩展类库以及它们的API, 利用Java API编写的应用程序(application) 和小程序(Java applet) 能够在任何Java平台上运行而无需考虑底层平台, 就是由于有Java虚拟机(JVM)实现了程序与操做系统的分离,从而实现了Java 的平台无关性。算法
那么到底什么是Java虚拟机(JVM)呢?一般咱们谈论JVM时,咱们的意思多是:编程
1. 对JVM规范的的比较抽象的说明;小程序
2. 对JVM的具体实现;数组
3. 在程序运行期间所生成的一个JVM实例。缓存
对JVM规范的的抽象说明是一些概念的集合,它们已经在书《The Java Virtual Machine Specification》(《Java虚拟机规范》)中被详细地描述了;对JVM的具体实现要么是软件,要么是软件和硬件的组合,它已经被许多生产厂商所实现,并存在于多种平台之上;运行Java程序的任务由JVM的运行期实例单个承担。在本文中咱们所讨论的Java虚拟机(JVM)主要针对第三种状况而言。它能够被当作一个想象中的机器,在实际的计算机上经过软件模拟来实现,有本身想象中的硬件,如处理器、堆栈、寄存器等,还有本身相应的指令系统。安全
JVM在它的生存周期中有一个明确的任务,那就是运行Java程序,所以当Java程序启动的时候,就产生JVM的一个实例;当程序运行结束的时候,该实例也跟着消失了。下面咱们从JVM的体系结构和它的运行过程这两个方面来对它进行比较深刻的研究。
刚才已经提到,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. pc: Java程序计数器;
2. optop: 指向操做数栈顶端的指针;
3. frame: 指向当前执行方法的执行环境的指针;。
4. 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语言的栈相同。
上面对虚拟机的各个部分进行了比较详细的说明,下面经过一个具体的例子来分析它的运行过程。
虚拟机经过调用某个指定类的方法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:虚拟机的运行过程
本文经过对JVM的体系结构的深刻研究以及一个Java程序执行时虚拟机的运行过程的详细分析,意在剖析清楚Java虚拟机的机理。
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是一个内存中的虚拟机,那它的存储就是内存了,咱们写的全部类、常量、变量、方法都在内存中,这决定着咱们程序运行的是否健壮、是否高效,接下来的部分就是重点介绍之。
咱们先把JVM这个虚拟机画出来,以下图所示:
从这个图中能够看到,JVM是运行在操做系统之上的,它与硬件没有直接的交互。咱们再来看下JVM有哪些组成部分,以下图所示:
该图参考了网上广为流传的JVM构成图,你们看这个图,整个JVM分为四部分:
q 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负责的。
q Execution Engine 执行引擎
执行引擎也叫作解释器(Interpreter),负责解释命令,提交操做系统执行。
q Native Interface本地接口
本地接口的做用是融合不一样的编程语言为Java所用,它的初衷是融合C/C++程序,Java诞生的时候是C/C++横行的时候,要想立足,必须有一个聪明的、睿智的调用C/C++程序,因而就在内存中专门开辟了一块区域处理标记为native的代码,它的具体作法是Native Method Stack中登记native方法,在Execution Engine执行时加载native libraies。目前该方法使用的是愈来愈少了,除非是与硬件有关的应用,好比经过Java程序驱动打印机,或者Java系统管理生产设备,在企业级应用中已经比较少见,由于如今的异构领域间的通讯很发达,好比能够使用Socket通讯,也能够使用Web Service等等,很少作介绍。
q Runtime data area运行数据区
运行数据区是整个JVM的重点。咱们全部写的程序都被加载到这里,以后才开始运行,Java生态系统如此的繁荣,得益于该区域的优良自治,下一章节详细介绍之。
整个JVM框架由加载器加载文件,而后执行器在内存中处理数据,须要与异构系统交互是能够经过本地接口进行,瞧,一个完整的系统诞生了!
全部的数据和程序都是在运行数据区存放,它包括如下几部分:
q 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,线程结束,栈释放。
q 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对象,通常池对象都在这个区域活跃。 三个区的示意图以下:
q Method Area 方法区
方法区是被全部线程共享,该区域保存全部字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。
q PC Register 程序计数器
每一个线程都有一个程序计数器,就是一个指针,指向方法区中的方法字节码,由执行引擎读取下一条指令。
q Native Method Stack 本地方法栈
问:堆和栈有什么区别
答:堆是存放对象的,可是对象内的临时变量是存在栈内存中,如例子中的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有哪些调整参数?
答:很是多,本身去找,堆内存、栈内存的大小均可以定义,甚至是堆内存的三个部分、新生代的各个比例都能调整。
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
对于从事C、C++程序开发的开发人员来讲,在内存管理领域,他们便是拥有最高权力的皇帝又是执行最基础工做的劳动人民——拥有每个对象的“全部权”,又担负着每个对象生命开始到终结的维护责任。
对于Java程序员来讲,不须要在为每个new操做去写配对的delete/free,不容易出现内容泄漏和内存溢出错误,看起来由JVM管理内存一切都很美好。不过,也正是由于Java程序员把内存控制的权力交给了JVM,一旦出现泄漏和溢出,若是不了解JVM是怎样使用内存的,那排查错误将会是一件很是困难的事情。
JVM执行Java程序的过程当中,会使用到各类数据区域,这些区域有各自的用途、建立和销毁时间。根据《Java虚拟机规范(第二版)》(下文称VM Spec)的规定,JVM包括下列几个运行时数据区域:
每个Java线程都有一个程序计数器来用于保存程序执行到当前方法的哪个指令,对于非Native方法,这个区域记录的是正在执行的VM原语的地址,若是正在执行的是Natvie方法,这个区域则为空(undefined)。此内存区域是惟一一个在VM Spec中没有规定任何OutOfMemoryError状况的区域。
与程序计数器同样,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异常。
本地方法栈与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异常。
叫“方法区”可能认识它的人还不太多,若是叫永久代(Permanent Generation)它的粉丝也许就多了。它还有个别名叫作Non-Heap(非堆),可是VM Spec上则描述方法区为堆的一个逻辑部分(原文:the method area is logically part of the heap),这个名字的问题还真容易使人产生误解,咱们在这里就不纠结了。
方法区中存放了每一个Class的结构信息,包括常量池、字段描述、方法描述等等。VM Space描述中对这个区域的限制很是宽松,除了和Java堆同样不须要连续的内存,也能够选择固定大小或者可扩展外,甚至能够选择不实现垃圾收集。相对来讲,垃圾收集行为在这个区域是相对比较少发生的,但并非某些描述那样永久代不会发生GC(至少对当前主流的商业JVM实现来讲是如此),这里的GC主要是对常量池的回收和对类的卸载,虽然回收的“成绩”通常也比较差强人意,尤为是类卸载,条件至关苛刻。
Class文件中除了有类的版本、字段、方法、接口等描述等信息外,还有一项信息是常量表(constant_pool table),用于存放编译期已可知的常量,这部份内容将在类加载后进入方法区(永久代)存放。可是Java语言并不要求常量必定只有编译期预置入Class的常量表的内容才能进入方法区常量池,运行期间也可将新内容放入常量池(最典型的String.intern()方法)。
运行时常量池是方法区的一部分,天然受到方法区内存的限制,当常量池没法在申请到内存时会抛出OutOfMemoryError异常。
直接内存并非虚拟机运行时数据区的一部分,它根本就是本机内存而不是VM直接管理的区域。可是这部份内存也会致使OutOfMemoryError异常出现,所以咱们放到这里一块儿描述。
在JDK1.4中新加入了NIO类,引入一种基于渠道与缓冲区的I/O方式,它能够经过本机Native函数库直接分配本机内存,而后经过一个存储在Java堆里面的DirectByteBuffer对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在Java对和本机堆中来回复制数据。
显然本机直接内存的分配不会受到Java堆大小的限制,可是即然是内存那确定仍是要受到本机物理内存(包括SWAP区或者Windows虚拟内存)的限制的,通常服务器管理员配置JVM参数时,会根据实际内存设置-Xmx等参数信息,但常常忽略掉直接内存,使得各个内存区域总和大于物理内存限制(包括物理的和操做系统级的限制),而致使动态扩展时出现OutOfMemoryError异常。
上述区域中,除了程序计数器,其余在VM Spec中都描述了产生OutOfMemoryError(下称OOM)的情形,那咱们就实战模拟一下,经过几段简单的代码,令对应的区域产生OOM异常以便加深认识,同时初步介绍一些与内存相关的虚拟机参数。下文的代码都是基于Sun Hotspot虚拟机1.6版的实现,对于不一样公司的不一样版本的虚拟机,参数与程序运行结果可能结果会有所差异。
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] |
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 profile和jvm性能调优 。慢慢写吧。本人jameswxx原创文章,转载请注明出处,我费了不少心血,多谢了。关于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. private int balance;
4.
5. public Account(int balance) {
6. this.balance = balance;
7. }
8.
9. public int getBalance() {
10. return balance;
11. }
12.
13. public void add(int num) {
14. balance = balance + num;
15. }
16.
17. public void withdraw(int num) {
18. balance = balance - num;
19. }
20.
21. public static void main(String[] args) throws InterruptedException {
22. Account account = new Account(1000);
23. Thread a = new Thread(new AddThread(account, 20), "add");
24. Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");
25. a.start();
26. b.start();
27. a.join();
28. b.join();
29. System.out.println(account.getBalance());
30. }
31.
32. static class AddThread implements Runnable {
33. Account account;
34. int amount;
35.
36. public AddThread(Account account, int amount) {
37. this.account = account;
38. this.amount = amount;
39. }
40.
41. public void run() {
42. for (int i = 0; i < 200000; i++) {
43. account.add(amount);
44. }
45. }
46. }
47.
48. static class WithdrawThread implements Runnable {
49. Account account;
50. int amount;
51.
52. public WithdrawThread(Account account, int amount) {
53. this.account = account;
54. this.amount = amount;
55. }
56.
57. public void run() {
58. for (int i = 0; i < 100000; i++) {
59. account.withdraw(amount);
60. }
61. }
62. }
63. }
第一次执行结果为10200,第二次执行结果为1060,每次执行的结果都是不肯定的,由于线程的执行顺序是不可预见的。这是java同步产生的根源,synchronized关键字保证了多个线程对于同步块是互斥的,synchronized做为一种同步手段,解决java多线程的执行有序性和内存可见性,而volatile关键字之解决多线程的内存可见性问题。后面将会详细介绍。
上面说了,java用synchronized关键字作为多线程并发环境的执行有序性的保证手段之一。当一段代码会修改共享变量,这一段代码成为互斥区或临界区,为了保证共享变量的正确性,synchronized标示了临界区。典型的用法以下:
Java代码
1. synchronized(锁){
2. 临界区代码
3. }
为了保证银行帐户的安全,能够操做帐户的方法以下:
Java代码
1. public synchronized void add(int num) {
2. balance = balance + num;
3. }
4. public synchronized void withdraw(int num) {
5. balance = balance - num;
6. }
刚才不是说了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. public void test(){
3. Object lock=new Object();
4. synchronized (lock){
5. //do something
6. }
7. }
8. }
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. synchronized (lock) {
3. balance = balance - num;
4. //这里放弃了同步锁,好不容易获得,又放弃了
5. lock.wait();
6. }
若是一个线程得到了锁lock,进入了同步块,执行lock.wait(),那么这个线程会进入到lock的阻塞队列。若是调用lock.notify()则会通知阻塞队列的某个线程进入就绪队列。
声明一个盘子,只能放一个鸡蛋
Java代码
1. package com.jameswxx.synctest;
2. public class Plate{
3. List<Object> eggs=new ArrayList<Object>();
4. public synchronized Object getEgg(){
5. if(eggs.size()==0){
6. try{
7. wait();
8. }catch(InterruptedException e){
9. }
10. }
11.
12. Object egg=eggs.get(0);
13. eggs.clear();//清空盘子
14. notify();//唤醒阻塞队列的某线程到就绪队列
15. return egg;
16. }
17.
18. public synchronized void putEgg(Object egg){
19. If(eggs.size()>0){
20. try{
21. wait();
22. }catch(InterruptedException e){
23. }
24. }
25. eggs.add(egg);//往盘子里放鸡蛋
26. notify();//唤醒阻塞队列的某线程到就绪队列
27. }
28. }
声明一个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是java提供的一种同步手段,只不过它是轻量级的同步,为何这么说,由于volatile只能保证多线程的内存可见性,不能保证多线程的执行有序性。而最完全的同步要保证有序性和可见性,例如synchronized。任何被volatile修饰的变量,都不拷贝副本到工做内存,任何修改都及时写在主存。所以对于Valatile修饰的变量的修改,全部线程立刻就能看到,可是volatile不能保证对变量的修改是有序的。什么意思呢?假若有这样的代码:
Java代码
1. public class VolatileTest{
2. public volatile int a;
3. public void add(int count){
4. a=a+count;
5. }
6. }
当一个VolatileTest对象被多个线程共享,a的值不必定是正确的,由于a=a+count包含了好几步操做,而此时多个线程的执行是无序的,由于没有任何机制来保证多个线程的执行有序性和原子性。volatile存在的意义是,任何线程对a的修改,都会立刻被其余线程读取到,由于直接操做主存,没有线程对工做内存和主存的同步。因此,volatile的使用场景是有限的,在有限的一些情形下能够使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时知足下面两个条件:
1)对变量的写操做不依赖于当前值。
2)该变量没有包含在具备其余变量的不变式中
volatile只保证了可见性,因此Volatile适合直接赋值的场景,如
Java代码
1. public class VolatileTest{
2. public volatile int a;
3. public void setA(int a){
4. this.a=a;
5. }
6. }
在没有volatile声明时,多线程环境下,a的最终值不必定是正确的,由于this.a=a;涉及到给a赋值和将a同步回主存的步骤,这个顺序可能被打乱。若是用volatile声明了,读取主存副本到工做内存和同步a到主存的步骤,至关因而一个原子操做。因此简单来讲,volatile适合这种场景:一个变量被多个线程共享,线程直接给这个变量赋值。这是一种很简单的同步场景,这时候使用volatile的开销将会很是小。
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,令A.b=B和B.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. private static final int _1MB = 1024 * 1024;
4.
5. public static void main(String[] args) {
6. // testAllocation();
7. testHandlePromotion();
8. // testPretenureSizeThreshold();
9. // testTenuringThreshold();
10. // testTenuringThreshold2();
11. }
12.
13. /**
14. * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
15. */
16. @SuppressWarnings("unused")
17. public static void testAllocation() {
18. byte[] allocation1, allocation2, allocation3, allocation4;
19. allocation1 = new byte[2 * _1MB];
20. allocation2 = new byte[2 * _1MB];
21. allocation3 = new byte[2 * _1MB];
22. allocation4 = new byte[4 * _1MB]; // 出现一次Minor GC
23. }
24.
25. /**
26. * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
27. * -XX:PretenureSizeThreshold=3145728
28. */
29. @SuppressWarnings("unused")
30. public static void testPretenureSizeThreshold() {
31. byte[] allocation;
32. allocation = new byte[4 * _1MB]; //直接分配在老年代中
33. }
34.
35. /**
36. * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
37. * -XX:+PrintTenuringDistribution
38. */
39. @SuppressWarnings("unused")
40. public static void testTenuringThreshold() {
41. byte[] allocation1, allocation2, allocation3;
42. allocation1 = new byte[_1MB / 4]; // 何时进入老年代决定于XX:MaxTenuringThreshold设置
43. allocation2 = new byte[4 * _1MB];
44. allocation3 = new byte[4 * _1MB];
45. allocation3 = null;
46. allocation3 = new byte[4 * _1MB];
47. }
48.
49. /**
50. * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
51. * -XX:+PrintTenuringDistribution
52. */
53. @SuppressWarnings("unused")
54. public static void testTenuringThreshold2() {
55. byte[] allocation1, allocation2, allocation3, allocation4;
56. allocation1 = new byte[_1MB / 4]; // allocation1+allocation2大于survivo空间一半
57. allocation2 = new byte[_1MB / 4];
58. allocation3 = new byte[4 * _1MB];
59. allocation4 = new byte[4 * _1MB];
60. allocation4 = null;
61. allocation4 = new byte[4 * _1MB];
62. }
63.
64. /**
65. * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
66. */
67. @SuppressWarnings("unused")
68. public static void testHandlePromotion() {
69. byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
70. allocation1 = new byte[2 * _1MB];
71. allocation2 = new byte[2 * _1MB];
72. allocation3 = new byte[2 * _1MB];
73. allocation1 = null;
74. allocation4 = new byte[2 * _1MB];
75. allocation5 = new byte[2 * _1MB];
76. allocation6 = new byte[2 * _1MB];
77. allocation4 = null;
78. allocation5 = null;
79. allocation6 = null;
80. allocation7 = new byte[2 * _1MB];
81. }
82. }
规则一:一般状况下,对象在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,GC的结果是新生代6651K变为148K,而总占用内存则几乎没有减小(由于几乎没有可回收的对象)。此次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)
tenured generation total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
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 total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)
eden space 8192K, 8% used [0x029d0000, 0x02a77e98, 0x031d0000)
from space 1024K, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
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, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)
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)
tenured generation total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
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%,也就是说allocation一、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, 0% used [0x031d0000, 0x031d0000, 0x032d0000)
to space 1024K, 0% used [0x032d0000, 0x032d0000, 0x033d0000)
tenured generation total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)
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若是要到实践调优阶段,必须了解每一个具体收集器的行为、优点劣势、调节参数。
Java虚拟机中,数据类型能够分为两类:基本类型和引用类型。基本类型的变量保存原始值,即:他表明的值就是数值自己;而引用类型的变量保存引用值。“引用值”表明了某个对象的引用,而不是对象自己,对象自己存放在这个引用值所表示的地址的位置。
基本类型包括:byte,short,int,long,char,float,double,Boolean,returnAddress
引用类型包括:类类型,接口类型和数组。
堆和栈是程序运行的关键,颇有必要把他们的关系说清楚。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据;堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
在Java中一个线程就会相应有一个线程栈与之对应,这点很容易理解,由于不一样的线程执行逻辑有所不一样,所以须要一个独立的线程栈。而堆则是全部线程共享的。栈由于是运行单位,所以里面存储的信息都是跟当前线程(或程序)相关信息的。包括局部变量、程序运行状态、方法返回值等等;而堆只负责存储对象信息。
第一,从软件设计的角度看,栈表明了处理逻辑,而堆表明了数据。这样分开,使得处理逻辑更为清晰。分而治之的思想。这种隔离、模块化的思想在软件设计的方方面面都有体现。
第二,堆与栈的分离,使得堆中的内容能够被多个栈共享(也能够理解为多个线程访问同一个对象)。这种共享的收益是不少的。一方面这种共享提供了一种有效的数据交互方式(如:共享内存),另外一方面,堆中的共享常量和缓存能够被全部栈访问,节省了空间。
第三,栈由于运行时的须要,好比保存系统运行的上下文,须要进行地址段的划分。因为栈只能向上增加,所以就会限制住栈存储内容的能力。而堆不一样,堆中的对象是能够根据须要动态增加的,所以栈和堆的拆分,使得动态增加成为可能,相应栈中只需记录堆中的一个地址便可。
第四,面向对象就是堆和栈的完美结合。其实,面向对象方式的程序与之前结构化的程序在执行上没有任何区别。可是,面向对象的引入,使得对待问题的思考方式发生了改变,而更接近于天然方式的思考。当咱们把对象拆开,你会发现,对象的属性其实就是数据,存放在堆中;而对象的行为(方法),就是运行逻辑,放在栈中。咱们在编写对象的时候,其实即编写了数据结构,也编写的处理数据的逻辑。不得不认可,面向对象的设计,确实很美。
程序要运行老是有一个起点的。同C语言同样,java中的Main就是那个起点。不管什么java程序,找到main就找到了程序执行的入口:)
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用。一个对象的大小是不可估计的,或者说是能够动态变化的,可是在栈中,一个对象只对应了一个4btye的引用(堆栈分离的好处:))。
为何不把基本类型放堆中呢?由于其占用的空间通常是1~8个字节——须要空间比较少,并且由于是基本类型,因此不会出现动态增加的状况——长度固定,所以栈中存储就够了,若是把他存在堆中是没有什么意义的(还会浪费空间,后面说明)。能够这么说,基本类型和对象的引用都是存放在栈中,并且都是几个字节的一个数,所以在程序运行时,他们的处理方式是统一的。可是基本类型、对象引用和对象自己就有所区别了,由于一个是栈中的数据一个是堆中的数据。最多见的一个问题就是,Java中参数传递时的问题。
要说明这个问题,先要明确两点:
1. 不要试图与C进行类比,Java中没有指针的概念
2. 程序运行永远都是在栈中进行的,于是参数传递时,只存在传递基本类型和对象引用的问题。不会直接传对象自己。
明确以上两点后。Java在方法调用传递参数时,由于没有指针,因此它都是进行传值调用(这点能够参考C的传值调用)。所以,不少书里面都说Java是进行传值调用,这点没有问题,并且也简化的C中复杂性。
可是传引用的错觉是如何形成的呢?在运行栈中,基本类型和引用的处理是同样的,都是传值,因此,若是是传引用的方法调用,也同时能够理解为“传引用值”的传值调用,即引用的处理跟基本类型是彻底同样的。可是当进入被调用方法时,被传递的这个引用的值,被程序解释(或者查找)到堆中的对象,这个时候才对应到真正的对象。若是此时进行修改,修改的是引用对应的对象,而不是引用自己,即:修改的是堆中的数据。因此这个修改是能够保持的了。
对象,从某种意义上说,是由基本类型组成的。能够把一个对象看做为一棵树,对象的属性若是仍是对象,则仍是一颗树(即非叶子节点),基本类型则为树的叶子节点。程序参数传递时,被传递的值自己都是不能进行修改的,可是,若是这个值是一个非叶子节点(即一个对象引用),则能够修改这个节点下面的全部内容。
堆和栈中,栈是程序运行最根本的东西。程序运行能够没有堆,可是不能没有栈。而堆是为栈进行数据存储服务,说白了堆就是一块共享的内存。不过,正是由于堆和栈的分离的思想,才使得Java的垃圾回收成为可能。
Java中,栈的大小经过-Xss来设置,当栈中存储数据比较多时,须要适当调大这个值,不然会出现java.lang.StackOverflowError异常。常见的出现这个异常的是没法返回的递归,由于此时栈中保存的信息都是方法返回的记录点。
基本数据的类型的大小是固定的,这里就很少说了。对于非基本类型的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时,确定是没有软引用存在的。
弱引用:弱引用与软引用相似,都是做为缓存来使用。但与软引用不一样,弱引用在进行垃圾回收时,是必定会被回收掉的,所以其生命周期只存在于一个垃圾回收周期内。
强引用不用说,咱们系统通常在使用时都是用的强引用。而“软引用”和“弱引用”比较少见。他们通常被做为缓存使用,并且通常是在内存大小比较受限的状况下作为缓存。由于若是内存足够大的话,能够直接使用强引用做为缓存便可,同时可控性更高。于是,他们常见的是被使用在桌面应用系统的缓存。
能够从不一样的的角度去划分垃圾回收算法:
引用计数(Reference Counting):
比较古老的回收算法。原理是此对象有一个引用,即增长一个计数,删除一个引用则减小一个计数。垃圾回收时,只用收集计数为0的对象。此算法最致命的是没法处理循环引用的问题。
标记-清除(Mark-Sweep):
此算法执行分两阶段。第一阶段从引用根节点开始标记全部被引用的对象,第二阶段遍历整个堆,把未标记的对象清除。此算法须要暂停整个应用,同时,会产生内存碎片。
复制(Copying):
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历当前使用区域,把正在使用中的对象复制到另一个区域中。次算法每次只处理正在使用中的对象,所以复制成本比较小,同时复制过去之后还能进行相应的内存整理,不会出现“碎片”问题。固然,此算法的缺点也是很明显的,就是须要两倍内存空间。
标记-整理(Mark-Compact):
此算法结合了“标记-清除”和“复制”两个算法的优势。也是分两阶段,第一阶段从根节点开始标记全部被引用对象,第二阶段遍历整个堆,把清除未标记对象而且把存活对象“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也避免了“复制”算法的空间问题。
增量收集(Incremental Collecting):实时垃圾回收算法,即:在应用进行的同时进行垃圾回收。不知道什么缘由JDK5.0中的收集器没有使用这种算法的。
分代收集(Generational Collecting):基于对对象生命周期分析后得出的垃圾回收算法。把对象分为年青代、年老代、持久代,对不一样生命周期的对象使用不一样的算法(上述方式中的一个)进行回收。如今的垃圾回收器(从J2SE1.2开始)都是使用此算法的。
串行收集:串行收集使用单线程处理全部垃圾回收工做,由于无需多线程交互,实现容易,并且效率比较高。可是,其局限性也比较明显,即没法使用多处理器的优点,因此此收集适合单处理器机器。固然,此收集器也能够用在小数据量(100M左右)状况下的多处理器机器上。
并行收集:并行收集使用多线程处理垃圾回收工做,于是速度快,效率高。并且理论上CPU数目越多,越能体现出并行收集器的优点。
并发收集:相对于串行收集和并行收集而言,前面两个在进行垃圾回收工做时,须要暂停整个运行环境,而只有垃圾回收程序在运行,所以,系统在垃圾回收时会有明显的暂停,并且暂停时间会由于堆越大而越长。
如何区分垃圾
上面说到的“引用计数”法,经过统计控制生成对象和删除对象时的引用数来判断。垃圾回收程序收集计数为0的对象便可。可是这种方法没法解决循环引用。因此,后来实现的垃圾判断算法中,都是从程序运行的根节点出发,遍历整个对象引用,查找存活的对象。那么在这种方式的实现中,垃圾回收从哪儿开始的呢?即,从哪儿开始查找哪些对象是正在被当前系统使用的。上面分析的堆和栈的区别,其中栈是真正进行程序执行地方,因此要获取哪些对象正在被使用,则须要从Java栈开始。同时,一个栈是与一个线程对应的,所以,若是有多个线程的话,则必须对这些线程对应的全部的栈进行检查。
同时,除了栈外,还有系统运行时的寄存器等,也是存储程序运行数据的。这样,以栈或寄存器中的引用为起点,咱们能够找到堆中的对象,又从这些对象找到对堆中其余对象的引用,这种引用逐步扩展,最终以null引用或者基本类型结束,这样就造成了一颗以Java栈中引用所对应的对象为根节点的一颗对象树,若是栈中有多个引用,则最终会造成多颗对象树。在这些对象树上的对象,都是当前系统运行所须要的对象,不能被垃圾回收。而其余剩余对象,则能够视为没法被引用到的对象,能够被当作垃圾进行回收。
所以,垃圾回收的起点是一些根对象(java栈, 静态变量, 寄存器...)。而最简单的Java栈就是Java程序执行的main函数。这种回收方式,也是上面提到的“标记-清除”的回收方式
因为不一样Java对象存活时间是不必定的,所以,在程序运行一段时间之后,若是不进行内存整理,就会出现零散的内存碎片。碎片最直接的问题就是会致使没法分配大块的内存空间,以及程序运行效率下降。因此,在上面提到的基本垃圾回收算法中,“复制”方式和“标记-整理”方式,均可以解决碎片的问题。
垃圾回收线程是回收内存的,而程序运行线程则是消耗(或分配)内存的,一个回收内存,一个分配内存,从这点看,二者是矛盾的。所以,在现有的垃圾回收方式中,要进行垃圾回收前,通常都须要暂停整个应用(即:暂停内存的分配),而后进行垃圾回收,回收完成后再继续应用。这种实现方式是最直接,并且最有效的解决两者矛盾的方式。
可是这种方式有一个很明显的弊端,就是当堆空间持续增大时,垃圾回收的时间也将会相应的持续增大,对应应用暂停的时间也会相应的增大。一些对相应时间要求很高的应用,好比最大暂停时间要求是几百毫秒,那么当堆空间大于几个G时,就颇有可能超过这个限制,在这种状况下,垃圾回收将会成为系统运行的一个瓶颈。为解决这种矛盾,有了并发垃圾回收算法,使用这种算法,垃圾回收线程与程序运行线程同时运行。在这种方式下,解决了暂停的问题,可是由于须要在新生成对象的同时又要回收对象,算法复杂性会大大增长,系统的处理能力也会相应下降,同时,“碎片”问题将会比较难解决。
分代的垃圾回收策略,是基于这样一个事实:不一样的对象的生命周期是不同的。所以,不一样生命周期的对象能够采起不一样的收集方式,以便提升回收效率。
在Java程序运行的过程当中,会产生大量的对象,其中有些对象是与业务信息相关,好比Http请求中的Session对象、线程、Socket链接,这类对象跟业务直接挂钩,所以生命周期比较长。可是还有一些对象,主要是程序运行过程当中生成的临时变量,这些对象生命周期会比较短,好比:String对象,因为其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次便可回收。
试想,在不进行对象存活时间区分的状况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,由于每次回收都须要遍历全部存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,由于可能进行了不少次遍历,可是他们依旧存在。所以,分代垃圾回收采用分治的思想,进行代的划分,把不一样生命周期的对象放在不一样代上,不一样代上采用最适合它的垃圾回收方式进行回收。
如图所示:
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
年轻代:
全部新生成的对象首先都是放在年轻代的。年轻代的目标就是尽量快速的收集掉那些生命周期短的对象。年轻代分三个区。一个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 GC和Full 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的各域分配策略动态变化
串行收集器
用单线程处理全部垃圾回收工做,由于无需多线程交互,因此效率比较高。可是,也没法使用多处理器的优点,因此此收集器适合单处理器机器。固然,此收集器也能够用在小数据量(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中最大堆大小有三方面限制:相关操做系统的数据模型(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。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代通常固定大小为64m,因此增大年轻代后,将会减少年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-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:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,若是没法知足此时间,JVM会自动调全年轻代大小,以知足此值。
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:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction:因为并发收集器不对内存空间进行压缩、整理,因此运行一段时间之后会产生“碎片”,使得运行效率下降。此值设置运行多少次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)
tenured generation total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K, 3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)
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 total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)
eden space 49152K, 0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)
from space 6144K, 55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)
to space 6144K, 0% used [0x221d0000, 0x221d0000, 0x227d0000)
tenured generation total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)
the space 69632K, 4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)
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:与上面几个配合使用,把相关日志信息记录到文件以便分析。
堆设置
-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后,对年老代进行压缩
传统分代垃圾回收方式,已经在必定程度上把垃圾回收给应用带来的负担降到了最小,把应用的吞吐量推到了一个极限。可是他没法解决的一个问题,就是Full GC所带来的应用暂停。在一些对实时性要求很高的应用场景下,GC暂停所带来的请求堆积和请求失败是没法接受的。这类应用可能要求请求的返回时间在几百甚至几十毫秒之内,若是分代垃圾回收方式要达到这个指标,只能把最大堆的设置限制在一个相对较小范围内,可是这样有限制了应用自己的处理能力,一样也是不可接收的。
分代垃圾回收方式确实也考虑了实时性要求而提供了并发回收器,支持最大暂停时间的设置,可是受限于分代垃圾回收的内存划分模型,其效果也不是很理想。
为了达到实时性的要求(其实Java语言最初的设计也是在嵌入式系统上的),一种新垃圾回收方式呼之欲出,它既支持短的暂停时间,又支持大的内存空间分配。能够很好的解决传统分代方式带来的问题。
增量收集的方式在理论上能够解决传统分代方式带来的问题。增量收集把对堆空间划分红一系列内存块,使用时,先使用其中一部分(不会所有用完),垃圾收集时把以前用掉的部分中的存活对象再放到后面没有用的空间中,这样能够实现一直边使用边收集的效果,避免了传统分代方式整个使用完了再暂停的回收的状况。
固然,传统分代收集方式也提供了并发收集,可是他有一个很致命的地方,就是把整个堆作为一个内存块,这样一方面会形成碎片(没法压缩),另外一方面他的每次收集都是对整个堆的收集,没法进行选择,在暂停时间的控制上仍是很弱。而增量方式,经过内存空间的分块,偏偏能够解决上面问题。
这部分的内容主要参考这里,这篇文章算是对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算法进行调优了。
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减少单个线程大小。以便能生产更多的线程。
<本文提供的设置仅仅是在高压力, 多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 used free shared buffers cached
Mem: 3995 3910 85 0 162 1267
-/+ buffers/cache: 2479 1515
Swap: 2047 0 2047
$ jstat -gcutil 23959 1000
S0 S1 E O P YGC YGCT FGC FGCT GCT
59.06 0.00 45.77 44.45 56.88 15204 324.023 66 1.668 325.691
0.00 39.66 27.53 44.73 56.88 15205 324.046 66 1.668 325.715
53.42 0.00 22.80 44.73 56.88 15206 324.073 66 1.668 325.741
0.00 44.90 13.73 44.76 56.88 15207 324.094 66 1.668 325.762
51.70 0.00 19.03 44.76 56.88 15208 324.118 66 1.668 325.786
0.00 61.62 19.44 44.98 56.88 15209 324.148 66 1.668 325.816
53.03 0.00 14.00 45.09 56.88 15210 324.172 66 1.668 325.840
53.03 0.00 87.87 45.09 56.88 15210 324.172 66 1.668 325.840
0.00 50.49 72.00 45.22 56.88 15211 324.198 66 1.668 325.866
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 锁机制的性能改善。
能整理出上面一些东西,也是由于站在巨人的肩上。下面是一些参考资料,供你们学习,你们有更好的,能够继续完善:)
· Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
· Hotspot memory management whitepaper
· Diagnosing a Garbage Collection problem
· Garbage-First Garbage Collection
· Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine
· 《深刻Java虚拟机》。虽然过去了不少年,但这本书依旧是经典。
这里是本系列的最后一篇了,很高兴你们可以喜欢这系列的文章。期间也提了不少问题,其中有些是我以前没有想到的或者考虑欠妥的,感谢提出这些问题的朋友,我也学到的很多东西。