深入理解Java虚拟机——自动内存管理机制

特此申明:

前段时间找工作所以看了这本书,整理的时候除了参考网上已有的笔记贴,加上自己整合的,可能和别人有雷同之处。不过无所谓啦,写出来自己看看,需要的朋友参考下,仅此而已。


一:Java内存区域与内存溢出异常

       在运行Java程序时,Java虚拟机会把管理的内存划分为若干个不同的数据区域。

Java虚拟机运行时数据区

数据区域图中,除了方法区和堆区是线程共享区外,其他三个是线程隔离的数据区(private)

程序计数器(Program Counter Register):属于线程私有的,占用的内存空间较少,可以看成是当前线程所执行字节码的行号指示器,字节码解释器工作时就是通过改变这个计数器的值来选择下一条,需要执行的字节码指令,分支,循环,跳转,异常处理,线程恢复等基础功能需要依赖这个计数器来完成,这个区域是jvm规范中没有规定任何OutOfMemoryError情况区域。

虚拟机栈:和程序计数器一样,都属于线程私有,生命周期与线程相同,描述的是java方法执行的内存模型,每个方法执行都会创建一个栈帧,用于存储局部变量表,操作栈,动态链接,方法出口等信息,每一个方法被调用直至执行完成的过程,就对应一个栈帧在jvm stack 从入栈到出栈的过程.局部变量表存放了编译期可知的各种数据基本类型(Boolean,byte,char,short,int,float,long,double),以及对象的引用。这个区域中定义了2种异常情况,如果线程请求的栈深度大于jvm所允许的深度,将抛出StackOverflowError异常,如果jvm可以动态扩张,当扩张无法申请到足够的内存空间是会抛出OutOfMemoryError异常。(这些数据区域异常将在下面的例子都讲到)。

         本地方法栈:与虚拟机栈比较相似。其区别:虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用Native方法服务。

         堆(Heap):jvm中内存占用最大的一块,是所有线程共享的一块内存区域.在jvm启动时创建,存放的是所有对象实例(或数组),所有的对象实例都在这里进行动态分配,当类空间无法再扩张会抛出OutOfMemoryError异常。Java堆是垃圾收集器管理的主要区域,而收集器采用分代收集算法。

         方法区(Method Area):与堆类似,也是各个线程共享的内存区域,主要用来存储加载的类信息,常量,静态变量,即时编译器编译后的代码等数据,当方法区无法满足内存分配时,也抛出OutOfMemoryError异常。运行时常量池是方法区的一部分,用于存放编译期生成的各种字面量和符号引用相对于Class文件常量池的重要特征是具备动态性(常量并非强制编译期产生,运行期间也可以新增,例如String类的intern()方法)。

         直接内存(DirectMemort):并不属于数据区,也不属于java定义的内存区域。由于NIO(New Input/Output)类,引入了一种基于通道与缓冲区(Buffer)的I/O方式。

对象访问

Object object = new Object(); 

Object object 这部分存储在java栈的本地变量表中,作为一个引用(reference)类型存在。

new Object() 这部分存储在java堆中,形成了一块存储了Object类型所有的实例数据值的结构化内存,动态变化,长度不固定。

方法区:在java堆中,必须要找到此对象类型数据,比如,对象类型,基类,实现的接口,方法等都存放在方法区。

       对象访问方式有两种:句柄和直接指针。

              句柄:reference中存储是对象的句柄地址,而句柄包含了对象实例数据和类型数据各自具体地址信息。好处:在对象移动时只需改变句柄中的实例数据指针,reference本身不需要修改。

              直接指针:reference中直接存储的就是对象地址。好处:速度快,它节省了一次指针定位的时间开销。

实战:OutOfMemoryError异常

1.      Java堆溢出

调整虚拟机最小值(-Xms)和最大值(-Xmx),并通过参数-XX:+HeapDumpOnOutOfMemoryError生成快照。要解决这个区域的异常,通过内存映像分析工具对快照分析,确认内存中的对象是否是必要的,分清楚出现了内存泄露还是内存溢出。若是内存泄露,通过工具查看泄露对象到GCRoots引用链,找到泄露对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收。若不存在泄露,则检查虚拟机堆参数与机器物理内存对比看是否还能调大或从代码上检查某些对象生命周期是否过长,尝试减少程序运行期的内存消耗。

2.      虚拟机栈和本地方法栈溢出

调节栈容量大小(-Xss)。如果线程请求的栈深度大于虚拟机所允许的最大深度,将会抛出StackOverflowError异常。使用-Xss参数减小栈内存容量或者增加此方法帧中本地变量表的程度都使栈深度缩小。

3.      运行时常量池溢出

调节参数-XX:PermSize和-XX:MaxPermSize限制方法区的大小,然后使用String.intern()这个Native方法向常量池中添加内容。运行时常量池溢出,在OutOfMemoryError后面跟随提示信息是“PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机的永久代)的一部分。

4.      方法区溢出

同样使用参数-XX:PermSize和-XX:MaxPermSize限制方法区的大小,然后不断产生大量的class来加载到内存,从而出现OutOfMemoryError。所以在经常动态生成大量Class的应用中,需要特别注意类的回收状况。

5.      本机直接内存溢出

通过参数-XX:MaxDirectMemorySize指定DirectMemory容量,若不指定则与Java堆最大值一样。可以直接通过反射获取Unsafe实例并进行内存分配,使用unsafe.allocateMemory()申请分配内存。不足时会出现OutOfMemoryError。


二.垃圾收集器与内存分配策略

概述

       Java内存运行时区域的各个部分,其中程序计数器、VM栈、本地方法栈三个区域随线程而生,随线程而灭;栈中的帧随着方法进入、退出而有条不紊的进行着出栈入栈操作。而Java堆和方法区(包括运行时常量池)则不一样,我们必须等到程序实际运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。

判断对象已死

       1)引用计数算法(对象中添加一个引用计数器,当有一个地方引用它,计数器加1,当引用失效,计数器减1,任何时刻计数器为0的对象就是不可能再被使用的),但引用计数算法无法解决对象循环引用的问题。

       根搜索算法(通过一系列的称为“GCRoots”的点作为起始进行向下搜索,当一个对象到GCRoots没有任何引用链(ReferenceChain)相连,则证明此对象是不可用的),主流程序语言Java,c#都使用此算法。Java语言中,GC Roots包括:  

1.VM栈(帧中的本地变量)中的引用。
2.方法区中的静态引用 和常量引用的对象。
3.JNI(即一般说的Native方法)中的引用。

2)生存还是死亡? 

       判定一个对象死亡,至少经历两次标记过程:如果对象在进行根搜索后,发现没有与GC Roots相连接的引用链,那它将会被第一次标记,并在稍后执行他的finalize()方法(如果它有的话)。这里所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这点是必须的,否则一个对象在finalize()方法执行缓慢,甚至有死循环什么的将会很容易导致整个系统崩溃。 finalize()方法是对象最后一次逃脱死亡命运的机会,稍后GC将进行第二次规模稍小的标记,如果在finalize()中对象成功拯救自己(只要重新建立到GC Roots的连接即可,譬如把自己赋值到某个引用上),那在第二次标记时它将被移除出即将回收的集合,如果对象这时候还没有逃脱,那基本上它就真的离死不远了。 需要关闭外部资源之类的事情,基本上它能做的使用try-finally可以做的更好。      

3)回收方法区

       方法区即后文提到的永久代,很多人认为永久代是没有GC的,这区GC性价比一般比较低:在堆中,尤其是在新生代,进行一次GC可以一般可以回收70%~95%的空间,而永久代的GC效率远小于此。但是目前方法区主要回收两部分内容:废弃常量与无用类。需要满足下面3个条件:  
1.该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。  
2.加载该类的ClassLoader已经被GC  
3.该类对应的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

垃圾收集算法

1.标记-清除算法(Mark-Sweep

算法分成标记清除两个阶段,首先标记出所有需要回收的对象,然后回收所有需要回收的对象。主要缺点有两个,一是效率问题,标记和清理两个过程效率都不高,二是空间问题,标记清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续使用中无法找到足够的连续内存而提前触发另一次的垃圾搜集动作。

2.复制算法(Copying

将内存分为一块较大的eden空间和2块较少的survivor空间,每次使用eden和其中一块survivor,当回收时将eden survivor还存活的对象一次过拷贝到另外一块survivor空间上,然后清理掉eden和用过的survivor。复制收集算法在对象存活率高的时候,效率有所下降。

3.标记-整理(Mark-Compact)算法

标记过程仍然一样,但后续步骤不是进行直接清理,而是令所有存活的对象一端移动,然后直接清理掉这端边界以外的内存。

4.分代收集(Generational Collection)算法

此算法只是根据对象不同的存活周期将内存划分为几块。一般是把Java堆分作新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。

垃圾收集器

没有最好的收集器,也没有万能的收集器,只有最合适的收集器。

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.CMSConcurrent Mark Sweep)收集器  
CMS是一种以最短停顿时间为目标的收集器,使用CMS并不能达到GC效率最高(总体GC时间最小),但它能尽可能降低GC时服务的停顿时间,这一点对于实时或者高交互性应用(譬如证券交易)来说至关重要,这类应用对于长时间STW一般是不可容忍的。CMS收集器使用的是标记-清除算法,也就是说它在运行期间会产生空间碎片,所以虚拟机提供了参数开启CMS收集结束后再进行一次内存压缩。

内存分配与回收策略 

       分析实验数据与结果。

总结

       GC 在很多时候都是系统并发度的决定性因素,虚拟机之所以提供多种不同的收集器,提供大量的调节参数,是因为只有根据实际应用需求、实现方式选择最优的收集方式才能获取最好的性能。没有固定收集器、参数组合,也没有最优的调优方法,虚拟机也没有什么必然的行为。


三虚拟机性能监控与故障处理工具

概述

       给一个系统问题定位问题的时候,知识、经验是关键基础,数据是依据,工具是运用知识处理数据的手段。这里说的数据包括:运行日志、异常堆栈、GC日志、线程快照(threaddump/javacore)、堆转储快照(headdump/hprof)等。经常使用适当的虚拟机监控和分析的工具可以加快我们分析数据和定位解决问题的速度。

JDK的命令行工具

       jdk的命令行工具都放置在jdk/bin目录下,其中包括了我们很熟悉的javajavac等工具。这些工具大多都是jdk/lib/tools.jar类库的一层包装而已,它们真正的主要功能都是在tools类库中实现的。下面将介绍几个常用的虚拟机监控工具。

1.jps: JVM Process Status Tool ,显示指定系统内所有的HotSpot虚拟机进程

可以列出正在运行的虚拟机进程,并显示虚拟机执行主类的名称,以及这些进程的本地虚拟机的唯一ID(LVMIDLocal Virtual Machine Identifier)。虽然功能单一,但是它是使用最频繁的工具。LVMID与系统中的进程IDPID)是一样的。如果同时启动了多个虚拟机进程,无法根据进程名称定位时,那就只能依赖jps命令显示主类的功能才能区分。

    命令格式:jps [option] [hostid]

执行样例:C:\Documents andSettings\Administrator>jps –lmv

2. jstat: JVM Statistics Monitoring Tool, 用于收集HotSpot虚拟机各方面的运行数据

用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,它是将运行期定位虚拟机性能问题的首选工具。

命令格式:jstat [option vmid [interval [s|ms] [count]]]

执行样例:C:\Documents andSettings\Administrator>jstat -gc 6820 1000 3代表在进程6820,查询间隔1000毫秒,次数3,查询参数为-gc

3. jinfo: Configuration Info for Java , 显示虚拟机配置信息

jinfo的作用是实时地查看和调整虚拟机的各项参数。使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表,在JDK1.6之后,jinfo还加入了运行期修改参数的能力,可以使用-flag [+|-] name 或者 -flag name=value

     命令格式:jinfo [option] pid

4. jmap: Memeory Map for Java, 生成虚拟机的内存转储快照(headdump文件)

jmap一般用于生成堆转储快照。当然jmap的作用也不仅仅是为了获取dump文件,它还可以查询finalize执行队列,Java堆和永久代得详细信息,如空间使用率、当前使用收集器等。

      命令格式:jmap [option] vmid

5.jstack: Stack Trace for Java,显示虚拟机的线程快照

       此命令用于生成虚拟机当前时刻的线程快照。它就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因:线程间死锁、死循环、请求外部资源导致的长时间等待等。

       命令格式:jstack [option] vmid

6.JConsoleJava监视与管理控制台

JConsole是一个基于JMX的GUI工具,用于连接正在运行的JVM,不过此JVM需要使用可管理的模式启动。如果要把一个应用以可管理的形式启动,可以在启动是设置com.sun.management.jmxremote。除此之外,还可以用JConsole监控tomacat。

JConsole可以以三种方式连接正在运行的JVM:

  • Local:使用JConsole连接一个正在本地系统运行的JVM,并且执行程序的和运行JConsole的需要是同一个用户。JConsole使用文件系统的授权通过RMI连接器连接到平台的MBean服务器上。这种从本地连接的监控能力只有Sun的JDK具有
  • Remote:使用下面的URL通过RMI连接器连接到一个JMX代理:
    service:jmx:rmi:///jndi/rmi://hostName:portNum/jmxrmi。hostName填入主机名称,portNum为JMX代理启动时指定的端口。JConsole为建立连接,需要在环境变量中设置mx.remote.credentials来指定用户名和密码从而进行授权。
  • Advanced:使用一个特殊的URL连接JMX代理。一般情况使用自己定制的连接器而不是RMI提供的连接器来连接JMX代理,或者是一个使用JDK1.4的实现了JMX和JMX Rmote的应用。

JConsole成功建立连接,它从连接上的JMX代理处获取信息,并且以下面几个标签页呈现信息。

  • Summary tab. 监控JVM和一些监控变量的信息。
  • Memory tab. 内存使用信息
  • Threads tab. 线程使用信息
  • Classes tab. 类调用信息
  • VM tab. JVM的信息
MBeans tab. 所有 MBeans 的信息

你可能感兴趣的:(Java,书籍笔记)