JVM所管理的内存分为以下几个运行时数据区:程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区。
程序计数器(Program Counter Register)
一块较小的内存空间,它是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变该计数器的值来选择下一条需要执行的字节码指令,分支、跳转、循环等基础功能都要依赖它来实现。每条线程都有一个独立的的程序计数器,各线程间的计数器互不影响,因此该区域是线程私有的。
当线程在执行一个Java方法时,该计数器记录的是正在执行的虚拟机字节码指令的地址,当线程在执行的是Native方法(调用本地操作系统方法)时,该计数器的值为空。另外,该内存区域是唯一一个在Java虚拟机规范中么有规定任何OOM(内存溢出:OutOfMemoryError)情况的区域。
Java虚拟机栈(Java Virtual Machine Stacks)
该区域也是线程私有的,它的生命周期也与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧,栈它是用于支持续虚拟机进行方法调用和方法执行的数据结构。对于执行引擎来讲,活动线程中,只有栈顶的栈帧是有效的,称为当前栈帧,这个栈帧所关联的方法称为当前方法,执行引擎所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译程序代码时,栈帧中需要多大的局部变量表、多深的操作数栈都已经完全确定了,并且写入了方法表的Code属性之中。因此,一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。
本地方法栈(Native Method Stacks)
该区域与虚拟机栈所发挥的作用非常相似,只是虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为使用到的本地操作系统(Native)方法服务。
Java堆(Java Heap)
Java Heap是Java虚拟机所管理的内存中最大的一块,它是所有线程共享的一块内存区域。几乎所有的对象实例和数组都在这类分配内存。Java Heap是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。
根据Java虚拟机规范的规定,Java堆可以处在物理上不连续的内存空间中,只要逻辑上是连续的即可。如果在堆中没有内存可分配时,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。
方法区(Method Area)
方法区也是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区域又被称为“永久代”,但这仅仅对于Sun HotSpot来讲,JRockit和IBM J9虚拟机中并不存在永久代的概念。Java虚拟机规范把方法区描述为Java堆的一个逻辑部分,而且它和Java Heap一样不需要连续的内存,可以选择固定大小或可扩展,另外,虚拟机规范允许该区域可以选择不实现垃圾回收。相对而言,垃圾收集行为在这个区域比较少出现。该区域的内存回收目标主要针是对废弃常量的和无用类的回收。运行时常量池是方法区的一部分,Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Class文件常量池),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。运行时常量池相对于Class文件常量池的另一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入Class文件中的常量池的内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的是String类的intern()方法。
根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。
内存泄漏和内存溢出的差别
内存泄露是指分配出去的内存没有被回收回来,由于失去了对该内存区域的控制,因而造成了资源的浪费。Java中一般不会产生内存泄露,因为有垃圾回收器自动回收垃圾,但这也不绝对,当我们new了对象,并保存了其引用,但是后面一直没用它,而垃圾回收器又不会去回收它,这边会造成内存泄露,
内存溢出是指程序所需要的内存超出了系统所能分配的内存(包括动态扩展)的上限。
GC回收器有哪些?
1 Serial收集器
1.1 Serial收集器运行过程
从名字可以看出,这个收集器是一个单线程的收集器。但是,它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是,在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。
"Stop The World"是在用户不可见的情况下,把用户正常工作的线程全部停掉,对于"Stop The World"给用户带来的不良体验,但从JDK1.3开始,HotSpot虚拟机开发团队为消除或减少工作线程因内存回收而导致停顿的努力一直进行着。从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)乃至GC收集器的最前沿成功Garbage First(G1)收集器,用户线程停顿时间不短缩短,但是仍然无法完全消除!
1.2 Serial收集器应用场景
虽然Serial收集器看起来“老而无用、食之无味弃之可惜”,但实际上到目前为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。它有着优于其他收集器的地方:简单高效(与其他收集器的单线程比)。
对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。
在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,只要不是频繁发生,这点停顿还是可以接受的,所以Serial收集器对应运行Client模式下的虚拟机来说是一个很好的选择。
2 ParNew 收集器
2.1 ParNew 收集器运行过程
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SruvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在现实上,这两种收集器也共用了相当多代码。
1.2 ParNew收集器应用场景
ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器。其中一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果。甚至由于存在线程交互的开销,该收集器在通过超线程技术实现两个CPU环境中都不能百分百地保证可以超越Serial收集器。当然,随着CPU数量增加,它对于GC时,系统资源的有效利用还是很有好处。它默认开启的收集线程数与CPU数量相同,在CPU非常多(例如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)环境下,可以使用-XX:ParalleGCThreads参数来限制垃圾收集的线程数。
3 Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器....看上去和ParNew都一样,那它有啥特别的地方呢?
Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能第缩短垃圾收集时用户线程停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。所谓的吞吐量就是:
CPU用于运行用户代码的时间与CPU总消耗时间的比值,即:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
虚拟机总共运行100分钟,其中垃圾收集化掉1分钟,那吞吐量就是99%。
4 Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法,这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它主要还有两大用途:
在JDK1.5以及之前版本中与Parallel Scavenge收集器搭配使用
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
5 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK1.6中才开始提供。
6 CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目的的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统服务器上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户最好的体验。CMS收集器就非常符合这类应用的需求。
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
初始标记
并发标记
重新标记
并发清除
其中,初始标记、重新标记着两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。并发标记阶段就是进行GC Roots Tracing的过程。而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间会比初始标记稍长一些,但远比并发标记时间短。
CMS是一款优秀的收集器,它的主要优点从名字上体现出来:并发收集、低停顿。但是CMS还远达不到完美程度,它有以下3个明显的缺点:
CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。
CMS收集器无法处理浮动垃圾,可能出现Concurrent Mode Failure失败而导致另一次Full GC产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,治好留待下一次GC时再清理掉。这部分垃圾就称为“浮动垃圾”。因此,CMS不能像其他收集器那样等到老年代几乎完全被填满再进行手机,CMS需要预留一部分空间。
由于CMS基于“标记-清除”算法,意味着收集结束时会有大量空间碎片产生。
7 G1 收集器
G1(Garbage First)收集器是当今收集器技术发展的最前沿成果之一。G1是面向服务端应用的垃圾收集器,与其他GC收集器相比,G1具备如下特点:
并行与并发:充分利用多CPU、多核环境下的硬件优势,使用多个CPU(CPU或CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器需要停顿Java线程执行的GC动作,G1仍然能通过并发方式让Java程序继续执行。
分代收集:与其他收集器一样,分代概念在G1中依然得以保存。
空间整合:与CMS的“标记-清理”算法不同,G1从整体上看是基于“标记-整理”算法实现的收集器,从局部上看是基于“复制”算法实现的,这两种算法意味着G1运作期间不会产生内存空间碎片。
可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得从超出N毫秒,这几乎已经是实时Java的垃圾收集器的特征了。
G1运作大致可划分为以下几个步骤:
初始标记
并发标记
最终标记
筛选回收
类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名空间。简单说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义
否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要他们的类加载器不同,那这两个类就必定不等。这里指的“相等”,包括代表Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,而包括使用instanceof关键字做对象所属关系判定等情况。
双亲委派模型:Bootstrap ClassLoader、Extension ClassLoader、ApplicationClassLoader。
启动类加载器,负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即时放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用。
扩展类加载器:负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用该类加载器。
应用程序类加载器:负责加载用户路径上所指定的类库,开发者可以直接使用这个类加载器,也是默认的类加载器。 三种加载器的关系:启动类加载器->扩展类加载器->应用程序类加载器->自定义类加载器。
这种关系即为类加载器的双亲委派模型。其要求除启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不以继承关系实现,而是用组合的方式来复用父类的代码。
双亲委派模型的工作过程:如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
好处:java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都会委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果用户自己写了一个名为java.lang.Object的类,并放在程序的Classpath中,那系统中将会出现多个不同的Object类,java类型体系中最基础的行为也无法保证,应用程序也会变得一片混乱。
实现:在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。