学习JVM

所谓虚拟机,就是一台虚拟的机器。它是一款软件,用来执行一系列虚拟计算机指令,大体上虚拟机可以分为系统虚拟机和程序虚拟机,大名鼎鼎的Visual Box、VMware就属于系统虚拟机,他们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。程序虚拟机典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在java虚拟机中执行的指令我们成为Java字节码指令。无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中。Java发展至今,出现过很多虚拟机,最初Sun使用的一款叫做Classic的Java虚拟机,到现在引用最广泛的是HotSpot虚拟机,除了Sun以外,还有BEA的JRockit,目前JRockit和HotSpot都被Oracle收入旗下,大有整合的趋势。

       下面我们来看下Java虚拟机的基本结构,如下图所示。

       下面我们便来学习Java虚拟机的基本结构:

       1、类加载子系统:负责从文件系统或者网络中加载Class信息,加载的信息存放在一块称之为方法区的内存空间。

       2、方法区:就是存放类信息、常量信息、常量池信息、包括字符串字面量和数字常量等。

       3、Java堆:在Java虚拟机启动的时候建立Java堆,它是Java程序最主要的内存工作区域,几乎所有的对象实例都存放到java堆中,堆空间是所有线程共享的。

       4、直接内存:Java的NIO库允许Java程序使用直接内存,从而提高性能,通常直接内存速度会优于Java堆。读写频繁的场合可能会考虑使用。

       5、Java栈:每个虚拟机线程都有一个私有的栈,一个线程的Java栈在线程创建的时候被创建,Java栈中保存着局部变量、方法参数、同时Java的方法调用、返回值等。

       6、本地方法栈:和Java栈非常类似,最大不同为本地方法栈用于本地方法调用。Java虚拟机允许Java直接调用本地方法(通常使用C编写)。

       7、垃圾收集系统是Java的核心,也是必不可少的,Java有一套自己进行垃圾清理的机制,开发人员无需手工清理。

       8、PC(Program Counter)寄存器也是每个线程私有的空间,Java虚拟机会为每个线程创建PC寄存器,在任意时刻,一个Java线程总是在执行一个方法,这个方法被称为当前方法,如果当前方法不是本地方法,PC寄存器就会执行当前被执行的指令,如果是本地方法,则PC寄存器值为undefined,寄存器存放如当前执行环境指针、程序计数器、操作栈指针、计算的变量值指针等信息。

       9、虚拟机最核心的组件就是执行引擎了,它负责执行虚拟机的字节码。一般会先进行编译成机器码后执行。

      下面我们来看下堆、栈、方法区概念和联系

       堆解决的是数据存储的问题,即数据怎么放、放在哪儿。

       栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。

       方法区则是辅助堆栈的永久区(Perm),解决堆栈信息的产生,是先决条件。

       我们创建一个新的对象,User:那么User类的一些信息(类信息、静态信息、都存在于方法区中)

               而User类被实例化出来之后,被存储到Java堆中,一块内存空间。当我们去使用的时候,都是使用User对象的引用,形如User user = new User();这里user就是存放在Java栈中的,即User真实对象的一个引用。如下图所示。

         下面我们一起来详细学习下堆

         java堆是和Java应用程序关系最密切的内存空间,几乎所有的对象都存放在其中,并且Java堆完全是自动化管理化的,通过垃圾回收机制,垃圾对象会自动清理,不需要显示地释放。

         根据垃圾回收机制不同,Java堆有可能拥有不同的结构。最为常见的就是将整个Java堆分为新生代和老年代。其中新生代存放新生的对象或者年龄不大的对象,老年代则存放老年对象。

         新生代分为eden区、s0区、s1区,s0和s1也被称为from和to区域,他们是两块大小相等并且可以互换角色的空间。

         绝大多数情况下,对象首先分配在eden区,在一次新生代回收后,如果对象还存活,则会进入s0或者s1区,之后每经过一次新生代回收,如果对象存活则它的年龄就加1,当对象达到一定的年龄后,则进入老年代。如下图所示。新生代被垃圾回收的频率明显高于老年代,因为新生代存放的都是些刚创建的对象或时间比较短的对象,这些对象很不稳定,有可能有的对象一实例化出来就没有被使用过,所以GC会对新生代频繁的进行回收,而老年代存放的是那些经过很多次垃圾回收依然没有被回收的对象,这些对象相对来说已经很稳定了,GC便没有必要再频繁的去尝试回收了,低频率便可以满足要求。

          我们单独拎出来新生代来详细说明,如下图所示,当一个对象刚被创建时会存放到eden区,eden这个单词是伊甸园的意思,伊甸园是亚当、夏娃出生的地方,所以用这个单词表示对象刚出生(刚被创建)。我们知道Java虚拟机有一套垃圾回收机制GC,GC会自动帮我们去回收已经不用的对象,当被创建的对象经过GC回收一次(没有被回收,是因为它在被别人引用),这个对象便会被放到s0区或者s1区,s0和s1在同一时刻只有一个正在被使用,另一个没有被使用。假如现在s0区正在被使用,GC来回收垃圾,根据复制算法,在s0区的依然被引用的对象都会被复制到当前没有被使用的s1区,等复制完之后,会把s0区中剩余的对象全部删除,然后切换到s1区,让这个区处于被使用状态。当GC下一次进行垃圾回收的时候,会自动去s1区去回收未被引用的对象,如果s1区现在有些对象依然处于被引用状态,那么根据算法,这些被引用的对象会被复制然后放到s0区,等把所有的没有被释放的对象都复制完并放到s0区后,s1区整个空间会被GC回收。s0区又开始被使用了。等到下次GC回收的时候就会去s0区去回收,如此循环往复,这样s0和s1便会不断互换角色。复制算法的好处是可以干净彻底的清楚垃圾,避免出现空间不连续的问题。

          下面我们来学习下Java栈

          Java栈是一块线程私有的内存空间,一个栈,一般由三部分组成:局部变量表、操作数栈和帧数据区。

          局部变量表:用于报错函数的参数及局部变量。

          操作数栈:主要保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

          帧数据区:除了局部变量表和操作数栈以外,栈还需要一些数据来支撑常量池的解析,这里帧数据区保存着访问常量池的指针,方便程序访问常量池,另外,当函数返回或者出现异常时,虚拟机必须有一个异常处理表,方便发送异常的时候找到异常的代码,因此异常处理表也是帧数据区的一部分。

        下面我们来学习java方法区

        java方法区和堆一样,方法区是一块所有线程共享的内存区域,它保存系统的类信息,比如类的字段、方法、常量池等。方法区的大小决定了系统可以保存多少个类,如果系统定义太多的类,导致方法区溢出。虚拟机同样会抛出内存溢出错误。方法区可以理解为永久区(Perm)。因此我们有可能要配置方法区的空间大小的。

        下面进入最重要的部分-----虚拟机参数(JVM调优)

        在虚拟机运行的过程中,如果可以跟踪系统的运行状态,那么对于问题的故障排查会有一定的帮助,为此,虚拟机提供了一些跟踪系统状态的参数,使用给定的参数执行java虚拟机,就可以在系统运行时打印相关日志,用于分析实际问题。我们进行虚拟机参数配置,其实主要就是围绕着堆、栈、方法区进行配置。

        首先来看堆分配参数

        -XX:+PrintGC  使用这个参数,虚拟机启动后,只要遇到GC就会打印日志。

        -XX:+UseSerialGC  配置串行回收器

       -XX:+PrintGCDetails  可以查看详细信息,包括各个区的情况

       -Xms:  设置java程序启动时初始堆大小

       -Xmx:  设置java程序能获得的最大堆大小

       -Xmx20m -Xms5m -XX:+PrintCommandLineFlags:可以将隐式或者显示传给虚拟机的参数输出。

        下面可以来看个实例Test01,如下所示。

 

[html] view plain copy
 
  1. package com.jvm.base;  
  2.   
  3. public class Test01 {  
  4.     public static void main(String[] args) {  
  5.         //-XX:+PrintGC -Xms5m -Xmx20m -XX:+UseSerialGC -XX:+PrintGCDetails  
  6.         //查看GC信息  
  7.         System.out.println("max memory:"+Runtime.getRuntime().maxMemory());  
  8.         System.out.println("free memory:"+Runtime.getRuntime().freeMemory());  
  9.         System.out.println("total memory:"+Runtime.getRuntime().totalMemory());  
  10.           
  11.         byte[] b1 = new byte[1*1024*1024];  
  12.         System.out.println("分配了1M");  
  13.         System.out.println("max memory:"+Runtime.getRuntime().maxMemory());  
  14.         System.out.println("free memory:"+Runtime.getRuntime().freeMemory());  
  15.         System.out.println("total memory:"+Runtime.getRuntime().totalMemory());  
  16.           
  17.         byte[] b2 = new byte[4*1024*1024];  
  18.         System.out.println("分配了4M");  
  19.         System.out.println("max memory:"+Runtime.getRuntime().maxMemory());  
  20.         System.out.println("free memory:"+Runtime.getRuntime().freeMemory());  
  21.         System.out.println("total memory:"+Runtime.getRuntime().totalMemory());  
  22.     }  
  23. }  

        首先来解释下上面的max memory、free memory、total memory的意思。

        1、maxMemory()这个方法返回的是Java虚拟机(这个进程)能构从操纵系统那里挖到的最大的内存
        2、totalMemory:程序运行的过程中,内存总是慢慢的从操纵系统那里挖的,基本上是用多少挖多少,直 挖到maxMemory()为止,所以totalMemory()是慢慢增大的
        3、freeMemory:挖过来而又没有用上的内存,实际上就是 freeMemory(),所以freeMemory()的值一般情况下都是很小的(totalMemory一般比需要用得多一点,剩下的一点就是freeMemory)

       运行上面的代码,结果如下图所示,可以看到最开始的时候free memory(剩余内存)是254741016byte,当分配了1M后free memory的值是253692424byte,前者减去后者得到的值%1024%1024得到的值便是1M。与我们分配了1M刚好相符,再分配4M,这时free memory的值是249498104byte,用第一个free memory减去第三个free memory的差%1024%1024值就等于5M,刚好与我们分配了5M内存相符。

          上面是在没有配置jvm参数的情况下的运行结果,下面我们设置下jvm参数,Run As的子菜单中我们点击"Run Configurations...",如下图所示。


        在下图中的VM arguments:一栏中 "-XX:+PrintGC -Xms5m -Xmx20m -XX:+UseSerialGC -XX:+PrintGCDetails -XX:+PrintCommandLineFlags",这行配置的意思是碰到GC就会打印日志,分配初始堆大小为5m,最大堆大小是20m,配置串行回收器,查看详细信息包括各个区的信息,将隐式或者显示传给虚拟机的参数输出

        运行结果如下图所示。第一行打印的是我们配置的jvm参数。第二行max memory的值20316160byte相当于19.375M,与我们在上图配置的参数"-Xmx20m"基本上是一致的(注意:我们给jvm配置的空间大小并不一定就完全一致的那么大,程序真实运行情况往往会有所偏差,但差不了多少,这里我们配置了最大20m,现在是19.375m,已经是比较接近了)。第三行free memory的值5312280byte相当于5.066m,第四行total memory的值6094848byte相当于5.81m,total memory的值一般会比free memory稍大,但比max memory要小。这与我们设置的"-Xms5m"(初始堆大小)比较接近,可以认为它俩是一致的。

       当我们分配了1M的内存后,触发了第一次垃圾回收, [GC (Allocation Failure) [DefNew: 764K->191K(1856K), 0.0010702 secs] 764K->529K(5952K), 0.0011022 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]这是打印的第一条垃圾回收的信息,[DefNew: 764K->191K(1856K), 0.0010702 secs]的意思是在GC之前新生代已使用764K,GC之后新生代使用容量为191K,说明回收了764K-191K=573K的垃圾。1856K是新生代的总容量,764K->529K(5952K)的意思是GC前java堆已使用空间764K,GC后java堆已使用空间是529K,也就是说回收了235K的堆垃圾。堆包括新生代和老年代。

        分配了1M内存后,打印的free memory的值变成了4470408byte,这个值相当于4M,初始是5M,分配了1M,现在剩4M,符合运行情况。

       当我们再分配4M的容量后,又触发了一次垃圾回收,[GC (Allocation Failure) [DefNew: 1249K->0K(1856K), 0.0009063 secs][Tenured: 1553K->1553K(4096K), 0.0017102 secs] 1586K->1553K(5952K), [Metaspace: 2636K->2636K(1056768K)], 0.0026923 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 这是打印的第二条垃圾回收信息,[DefNew: 1249K->0K(1856K), 0.0009063 secs]是指新生代原来使用的空间是1249K,垃圾回收后,新生代所用空间变为0K,说明新生代已经没有垃圾了。新生代占的空间是1856K,是1M多,[Tenured: 1553K->1553K(4096K), 0.0017102 secs]这句信息的意思是老年代没有回收任何垃圾,说明老年代中的对象十分稳定,老年代所占用的空间是4096K,也就是4M,4096%1856=2.2,这与新生代与老年代默认所占空间比例(1:2)基本上是一致的。[Metaspace: 2636K->2636K(1056768K)], 0.0026923 secs]这句信息的意思是元数据区经过垃圾回收后,没有回收任何垃圾,元数据区所占空间大小是1056768%1024%1024=1G。这里需要提醒的是,JDK1.8将永久区换成了元数据区。

        分配4M内存后,free memory的值变成了4539056byte,这个值也相当于4M,而此时total memory发生了变化,不再是我们设置的初始值5M了,这是为什么呢?其实这是由于我们第一次分配1M后,free memory的值是4470408byte,约等于4M,但是我们知道,在环境实际运行中我们配置的参数与真实jvm环境值还是有一些出入的,我们申请分配4M内存时可能jvm真实环境中total memory已经不够申请了,于是乎向max memory申请要点空间,由于max memory的值是20M,足够申请这4M空间,因此当前total memory的空间大小便由原来的5M加上4M变成现在的10358784byte(约等于9M)。

        再往下便是打印的Heap的详细信息,def new generation   total 1920K, used 69K [0x00000000fec00000, 0x00000000fee10000, 0x00000000ff2a0000)明显指的是新生代的信息,总空间是1920K,用了69K,我们看到的[0x00000000fec00000, 0x00000000fee10000, 0x00000000ff2a0000)这句信息我们只关心前两个数就行了。我们将0x00000000fec00000和0x00000000fee10000都转成int,int a = 0x00000000fec00000;和int b = 0x00000000fee10000;然后用(b-a)/1024得到的值是2112K,这与total 1920K已经很接近了,两者一般不会完全相等,但我们要知道他俩的值应该是一样的。新生代下面的信息便是具体的eden、from、to三个模块具体的信息。再往下便是老年代的信息,再往下便是元数据区的信息(方法区)。

         小结:在实际工作中,我们可以直接将初始的堆大小与最大堆大小设置相等,这样的好处是可以减少程序运行时的垃圾回收次数,从而提高性能。

        下面我们来学习新生代的配置

         -Xmn:可以设置新生代的大小,设置一个比较大的新生代会减少老年代的大小,这个参数对系统性能以及GC行为有很大的影响,新生代大小一般会设置整个堆空间的1/3到1/4左右。

        -XX:SurvivorRatio:用来设置新生代中eden空间和from/to空间的比例。含义:-XX:SurvivorRatio=eden/from=eden/to

       我们还是看个例子

 

[html] view plain copy
 
  1. package com.jvm.base;  
  2.   
  3. public class Test02 {  
  4.    public static void main(String[] args) {  
  5.        //第一次配置  
  6.        //-XX:SurvivorRatio=2的意思是新生代的eden与s0或s1所占空间的比例,其中s0与s1是大小相等互相切换的两个区域  
  7.        //需要特别注意的是,下面的-Xms与-Xmn值不能相等,Xms一定要大于Xmn。因为Xms指定的是堆的初始大小,而Xmn只是新生代的大小  
  8.        //堆是包括新生代和老生代的,因此Xms要大于Xmn。  
  9.        //-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC  
  10.          
  11.        //第二次配置  
  12.        //-Xms20m -Xmx20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC  
  13.          
  14.        //第三次配置   
  15.        //-XX:NewRatio=老年代/新生代  
  16.        //-Xms20m -Xmx20m -Xmn7m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC  
  17.        byte[] b = null;  
  18.        //连续向系统申请10MB空间  
  19.        for(int i=0;i<10;i++){  
  20.            b = new byte[1*1024*1024];//一次申请1M,申请10次,就是10M  
  21.        }  
  22.    }  
  23. }  

      我们先进行第一次配置,如下图所示。

 


         运行结果如下图所示。在Heap详细信息中,def new generation   total 768K, used 490K [0x00000000fec00000, 0x00000000fed00000, 0x00000000fed00000)的意思是新生代当前起作用的空间总大小是768K,used 490K是指已经使用了490K。而紧跟这句信息的是如下三句信息。这三句的意思是eden区空间大小是512K,from区(也叫s0区)空间大小是256K,to区(也叫s1区)空间大小是256K,eden/ftom=2,并且eden/to=2。eden与from或eden与to两者之和是768K。从这儿也印证了,同一时刻from和to区只有一个区起作用。eden、from、to三个区加起来大小是1024K刚好是1M。

 

[html] view plain copy
 
  1. eden space 512K,  45% used [0x00000000fec00000, 0x00000000fec3ab78, 0x00000000fec80000)  
  2. from space 256K,  99% used [0x00000000fecc0000, 0x00000000fecffff8, 0x00000000fed00000)  
  3. to   space 256K,   0% used [0x00000000fec80000, 0x00000000fec80000, 0x00000000fecc0000)  

            tenured generation   total 19456K, used 10413K [0x00000000fed00000, 0x0000000100000000, 0x0000000100000000)这句信息的意思是老年代所占空间大小是19456K,刚好等于19M,加上新生代的大小1M,刚好等于20M。

 

         下面我们来修改下参数,修改成第二次配置所指定的参数,其实只修改了新生代的空间大小,原来是1M,现在要分配7M。如下图所示。

        运行结果如下图所示,新生代分配的空间变大了,GC反而回收的次数增加了,但是也不全是,当我们把新生代的空间配置成10m时,GC的回收次数就变成了2次,当我们把新生代的空间配置成15m时,GC回收的次数就变成了1次。可见GC回收的触发机制并不全是新生代空间的大小,肯定还有其它因素的作用,只不过我不太清楚。下图的eden区与s0或s1区的比例是2:1,三者所占空间大小总和是7M,刚好就是我们分配的新生代的总大小。def new generation   total 11520K, used 5875K 这句信息中的total 11520K 很明显不是eden、from、to三者之和,而是eden与from或者to两者之和。

        

          下面我们来配置老年代与新生代的比例(对应代码中第三次配置),如下图所示。

         运行结果如下图所示:可以看到,当前新生代总大小是5760K+704K+704K=7M。tenured generation   total 13312K, used 2575K [0x00000000ff300000, 0x0000000100000000, 0x0000000100000000)这句信息中指明了老年代的大小是13312K=13M,老年代/新生代约等于2。

        小结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置,基本策略是尽可能将对象预留在新生代,减少老年代的GC次数。除了可以设置新生代的绝对大小(-Xmn),还可以使用(-XX:NewRatio)设置新生代和老年代的比例:-XX:NewRatio=老年代/新生代。

         下面我们来学习一下如何用内存分析工具来分析内存溢出。

         首先我们先来造一个内存溢出的例子

 

[html] view plain copy
 
  1. package com.jvm.base;  
  2.   
  3. import java.util.Vector;  
  4.   
  5. public class Test03 {  
  6.    public static void main(String[] args) {  
  7.       //-Xms2m -Xmx2m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:/Test03.dump  
  8.       //堆内存溢出  
  9.       Vector v = new Vector<>();  
  10.       for(int i=0;i<5;i++){  
  11.           v.add(new Byte[1*1024*1024]);  
  12.       }  
  13.    }  
  14. }  

          我们按照代码中的JVM参数进行配置,可以看到我们配置的最大内存才2M,而程序却想要获取5M内存,肯定会发生内存溢出。如下图进行配置(这里有个地方需要说一下,就是我们在Test03类的main方法处右键------>Run As----->Run Configuration,弹出框中可能没有Test03,这时我们可以点击"Java Application"---->New,就会出现Test03了)。

 


          运行结果如下图所示。

posted @ 2017-08-30 15:29  林加欣  阅读(265)  评论(0编辑  收藏  举报