JVM内存管理学习总结(一)

I.JVM进程的生命周期

  1. JVM实例的生命周期和java程序的生命周期保持一致,即一个新的程序启动则产生一个新的JVM进程实例,程序结束则JVM进程实例伴随着消失。那么程序启动和程序终止就是JVM实例生命周期的两个边界,两个边界点可以这么理解:一个拥有程序入口(main函数)的class在执行main方法时,相应的JVM就被创建了(即JVM生命周期的起点),当由此main函数启动的所有非守护线程都终止时,JVM即退出(JVM实例生命周期的终点)。举个实例来描述一下JVM实例的生命周期:

    (1) JVMInstance.java

    public class JVMInstance {
    
        public static void main(String[] args) {
            System.out.println("hello world!");
        }
    }
    

    (2) javac JVMInstance.java 编译源码生成class文件 
    (3) java JVMInstance 
    在用java 命令执行编译好的字节码文件时,java命令会调用java launcher来创建JVM实例,而java.exe的源代码在jdk/src/share/bin/java.c定义(可以在openJDK中看到其中的源代码)。 java.c中主要包含了两个主要的函数:

    int main(int argc, char ** argv); int JNICALLJavaMain(void * _args); 其中主要完成的功能是新建JVM实例进程,实例化一些守护线程,包括监视器线程(WatcherThread),编译器线程(Compiler Thread),GC线程(GC Thread)。(具体源代码可以细扣一下,有现成的研究大家也分享一下) //TODO ps:学习一下java.c 源代码

II.JVM内存模型

在内存结构上每个JVM实例都有自己的一套内存模型(即:堆,方法区,方法栈,本地方法栈,程序技术器),当JVM实例创建时内存模型也随之创建,没猜错的话java.c的这段代码就是用来分配JVM实例内存的:

{
  int i;
  original_argv = (char**)JLI_MemAlloc(sizeof(char*)*(argc+1));
  for(i = 0; i < argc+1; i++) {
original_argv[i] = argv[i];
  }
}

CreateExecutionEnvironment(&argc, &argv,
   jrepath, sizeof(jrepath),
   jvmpath, sizeof(jvmpath),
   original_argv);
...

JVM实例的内存模型结构入下图(参考《深入理解java虚拟机》):  上图中标出了红框和绿框,其中红框代表在一个JVM进程实例内所有线程共享的内存模型区域,绿框表示线程内存数据隔离的区域,即方法区和堆中的数据JVM进程内线程共享,栈和程序计数器每个线程有自己的一套。(看过一些博客把方法区也画入了堆内存结构中,感觉不是很合理,虽然java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-heap(非堆))。方法区主要存储:类加载的信息,常量,静态变量,即时编译器编译后的代码等数据。堆内存又分为 Yong、Tenured、Perm。虚拟机栈是执行普通方法是需要用的,本地方法栈就是执行本地方法栈的时候用的。程序计数器就是辅助程序执行用的,记录程序指令指针。 JVM执行引擎是JVM实例进程中的一个线程,用来执行字节码指令的。

III.JVM内存垃圾回收机制及常用垃圾回收器

(1)分代回收:在运行的程序中,会创建大量端生命周期的对象和小部分长生命周期的对象。对于短生命周期对象,垃圾回收线程就需要频繁的检测释放,对于长周期的对象垃圾回收检测线程探测频率就可以少一点。为了满足这种需求SUN的JVM内存是分代管理,分代回收的(说白了分代管理就是为了分代回收,哪天如果不需要分代回收了,那分代管理机制也就不需要了,我个人觉得哈)。

(2)常用的垃圾回收算法:

标记清除算法: 概述:回收过程分两个阶段,即先“标记” 再 “清除”,首先GC线程根据根搜索算法以及回收判定策略,判断对象是否需要被回收,将需要被回收的对象全都标记出来,然后统一回收掉。这种算法是比较基础的算法,后续的算法基本是基于这种算法的。 主要缺点:一个是效率问题,标记和清除过程效率都不高;另外一个是空间问题,标记清除之后会产生不连续的内存碎片(如果空间碎片太多,当分配一个大对象的时候,因为找不到一块合适的内存区域,就会提前出发一次GC操作)。

复制算法: 复制算法基本思想就是,将内存分成大小相等的两块,每次只使用其中的一块。当其中一块内存用完了,就将活着的对象按序摆放到另外一块内存中,然后将另一块内存清理掉。 优缺点:优点就是与标记清除算法相比,效率明显提高了,并且不会产生内存碎片。缺点也是显而易见就是内存需求更大了,每次都需要有一块内存闲置。(ps:现在的商业虚拟机都采用这种收集算法回收新生代,例如:SUN JDK1.3.1版本启用的HotSpot虚拟机)

标记整理算法: 标记整理算法主要分为算个阶段,首先将要清理的对象标记出来和标记清除的第一阶段一样,然后将有效的对象向一端移动(类似于我们有win7优化到时整理内存碎片的效果),最后将有效对象边界以外的对象全部回收掉。 优缺点:有点就是和标记清除算法相比减少了内存碎片,和复制算法相比提高内存利用率。 缺点:效率较低。

(3)常用的垃圾收集器:

  1. Serial收集器:单线程收集器,只会启动一条线程进行内存回收工作,在回收的过程中,必须暂停其他所有的工作线程。serial是一个比较原始的收集器,运行在Server端的JVM已经不再使用Serial,不过对于运行在Client端的JVM是一个很好的选择(单线程嘛,意味着没有线程切换开销)。Serial对于Yong区采用的是复制算法,对于Tenured采用的是标记整理算法。
  2. ParNew收集器: ParNew收集器是Serial收集器的多线程版本,对于收集算法以及控制参数Serial和ParNew一样,ParNew是运行在Server模式下的JVM的首选的新生代收集器,主要原因是除了ParNew能和Serial配合使用外,只有ParNew能和CMS协同工作。
  3. Parallel Scavenge收集器:多线程的新生代收集器,采用的复制算法,看上去和ParNew一样,但专注点不同,ParNew专注于在收集过程中缩短用户线程的停顿时间,而它专注于达到一个可控的吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。
  4. Serial Old收集器:Serial的老年代版本,采用的是标记整理算法,现在跑Server端JVM进程实例不经常使用的收集器,比较古老。
  5. Parallel Old收集器:Parallel Scavenge的老年代版本,使用多线程和标记-整理算法。Paralle Old出现之后就可以和Parallel Scavenge配合使用了。
  6. CMS收集器:CMS(Concurrent Mark Sweep)收集器是一种以获取最短停顿时间为目标的收集器,目前在市场上使用的占比很大。它是一款老年代收集器,采用的是标记-清除算法。它在清理过程中主要包含4个阶段,分别是初始标记,并发标记,重新标记,并发清除。其中只有初始标记和重新标记需要停顿用户线程,在并发标记和并发清除阶段,用户线程和回收器线程可以共同运行(将整个回收过程分成好多段,以达到减少停顿用户线程的目的)。
  7. G1收集器是JDK1.7提供的收集器,G1基于标记-整理算法(没有内存碎片),试用于新生代和老年代。它改变了收集的策略,不在针对新生代和老年代整个区域进行收集,而是把新生代或老年代划分成多个单元格,针对每个单元格的使用程度,对每个单元格进行收集,属于算法的优化和突破。

下面这个图不错的表达了各个收集器使用的分代区域: 

IV.JVM垃圾回收日志结构分析及常用的命令

我们在我们Server的gc log上经常看到下面的这个日志。 总结一下各个字段表达的是什么意思: Young GC: 

Full GC: 

阅读更多

JVM内存管理总结【分享】

06-02

近期看了看Java内存泄露的一些案例,跟原来的几个哥们讨论了一下,深入研究发现JVM里面还是有不少以前不知道的细节,这里稍微剖析一下。先看一看JVM的内部结构——rn[img=http://p.blog.csdn.net/images/p_blog_csdn_net/Ant_Yan/EntryImages/20090520/a7413588-2618-3301-9eb4-e025e5afc2d8.png][/img]rnrn如图所示,JVM主要包括两个子系统和两个组件。两个子系统分别是Class loader子系统和Execution engine(执行引擎) 子系统;两个组件分别是Runtime data area (运行时数据区域)组件和Native interface(本地接口)组件。rn rnClass loader子系统的作用:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Java程序员可以extends java.lang.ClassLoader类来写自己的Class loader。 rnrnExecution engine子系统的作用:执行classes中的指令。任何JVM specification实现(JDK)的核心都是Execution engine,不同的JDK例如Sun 的JDK 和IBM的JDK好坏主要就取决于他们各自实现的Execution engine的好坏。rnrnNative interface组件:与native libraries交互,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,所以也很容易出现JVM无法控制的native heap OutOfMemory。rnrnRuntime Data Area组件:这就是我们常说的JVM的内存了。它主要分为五个部分——rn1、Heap (堆):一个Java虚拟实例中只存在一个堆空间rn2、Method Area(方法区域):被装载的class的信息存储在Method area的内存中。当虚拟机装载某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件内容并把它传输到虚拟机中。rn3、Java Stack(java的栈):虚拟机只会直接对Java stack执行两种操作:以帧为单位的压栈或出栈rn4、Program Counter(程序计数器):每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的饿地址,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。 rn5、Native method stack(本地方法栈):保存native方法进入区域的地址rnrn以上五部分只有Heap 和Method Area是被所有线程的共享使用的;而Java stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有自己的部分。rnrn了解JVM的系统结构,再来看看JVM内存回收问题了——rnSun的JVM Generational Collecting(垃圾回收)原理是这样的:把对象分为年青代(Young)、年老代(Tenured)、持久代(Perm),对不同生命周期的对象使用不同的算法。(基于对对象生命周期分析)rn[img=http://p.blog.csdn.net/images/p_blog_csdn_net/Ant_Yan/EntryImages/20090520/39bbb07d-fe97-3d9e-b55f-2f48a6d46395.png][/img]rnrn如上图所示,为Java堆中的各代分布。 rn1. Young(年轻代)rn年轻代分三个区。一个Eden区,两个Survivor区。大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到Survivor区(两个中的一个),当这个Survivor区满时,此区的存活对象将被复制到另外一个Survivor区,当这个Survivor去也满了的时候,从第一个Survivor区复制过来的并且此时还存活的对象,将被复制年老区(Tenured。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而复制到年老区的只有从第一个Survivor去过来的对象。而且,Survivor区总有一个是空的。 rn2. Tenured(年老代)rn年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。 rn3. Perm(持久代)rn用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。rnrn举个例子:当在程序中生成对象时,正常对象会在年轻代中分配空间,如果是过大的对象也可能会直接在年老代生成(据观测在运行某程序时候每次会生成一个十兆的空间用收发消息,这部分内存就会直接在年老代分配)。年轻代在空间被分配完的时候就会发起内存回收,大部分内存会被回收,一部分幸存的内存会被拷贝至Survivor的from区,经过多次回收以后如果from区内存也分配完毕,就会也发生内存回收然后将剩余的对象拷贝至to区。等到to区也满的时候,就会再次发生内存回收然后把幸存的对象拷贝至年老区。rnrn通常我们说的JVM内存回收总是在指堆内存回收,确实只有堆中的内容是动态申请分配的,所以以上对象的年轻代和年老代都是指的JVM的Heap空间,而持久代则是之前提到的Method Area,不属于Heap。rnrn了解完这些之后,以下的转载一热衷于钻研技术的哥们Richen Wang关于内存管理的一些建议——rn1、手动将生成的无用对象,中间对象置为null,加快内存回收。rn2、对象池技术 如果生成的对象是可重用的对象,只是其中的属性不同时,可以考虑采用对象池来较少对象的生成。如果有空闲的对象就从对象池中取出使用,没有再生成新的对象,大大提高了对象的复用率。rn3、JVM调优 通过配置JVM的参数来提高垃圾回收的速度,如果在没有出现内存泄露且上面两种办法都不能保证内存的回收时,可以考虑采用JVM调优的方式来解决,不过一定要经过实体机的长期测试,因为不同的参数可能引起不同的效果。如-Xnoclassgc参数等。rnrn推荐的两款内存检测工具rn1、jconsole JDK自带的内存监测工具,路径jdk bin目录下jconsole.exe,双击可运行。连接方式有两种,第一种是本地方式如调试时运行的进程可以直接连,第二种是远程方式,可以连接以服务形式启动的进程。远程连接方式是:在目标进程的jvm启动参数中添加-Dcom.sun.management.jmxremote.port=1090 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false 1090是监听的端口号具体使用时要进行修改,然后使用IP加端口号连接即可。通过该工具可以监测到当时内存的大小,CPU的使用量以及类的加载,还提供了手动gc的功能。优点是效率高,速度快,在不影响进行运行的情况下监测产品的运行。缺点是无法看到类或者对象之类的具体信息。使用方式很简单点击几下就可以知道功能如何了,确实有不明白之处可以上网查询文档。rnrn2、JProfiler 收费的工具,但是到处都有破解办法。安装好以后按照配置调试的方式配置好一个本地的session即可运行。可以监测当时的内存、CPU、线程等,能具体的列出内存的占用情况,还可以就某个类进行分析。优点很多,缺点太影响速度,而且有的类可能无法被织入方法,例如我使用jprofiler时一直没有备份成功过,总会有一些类的错误。rnrnrnrnrn

没有更多推荐了,返回首页