Java虚拟机(四)-JVM内存机制

来源:互联网 发布:ubuntu怎么编辑文件 编辑:程序博客网 时间:2024/06/05 04:54

        内存作为系统中重要的资源,对于系统稳定运行和高效运行起到了关键的作用,Java和C之类的语言不同,不需要开发人员来分配内存和回收内存,而是由JVM来管理对象内存的分配以及对象内存的回收(又称为垃圾回收、GC),这对于开发人员来说确实大大降低了编写程序的难度,但带来的一个副作用就是,当系统运行过程中出现JVM抛出的内存异常(例如OutOfMemoryError)的时候,很难知道原因是什么,另外一方面,要编写高性能的程序,通常需要借助内存来提升性能,因此如何才能合理的使用内存以及让JVM合理的进行内存的回收是必须掌握的。

首先我们来了解JVM specification中的JVM整体架构。如下图:

        主要包括两个子系统和两个组件: Class loader(类装载器)子系统,Execution engine(执行引擎)子系统;Runtime data area (运行时数据区域)组件, Native interface(本地接口)组件。

        Class loader子系统的作用:根据给定的全限定名类名(如 java.lang.Object)来装载class文件的内容到 Runtime data area中的method area(方法区域)。Javsa程序员可以extends java.lang.ClassLoader类来写自己的Class loader。

        Execution engine子系统的作用:执行classes中的指令。任何JVM specification实现(JDK)的核心是Execution engine,换句话说:Sun的JDK和IBM的JDK好坏主要取决于他们各自实现的Execution engine的好坏。每个运行中的线程都有一个Execution engine的实例。

Native interface组件:与native libraries交互,是其它编程语言交互的接口。

       Runtime data area组件:这个组件就是JVM中的内存。下面对这个部分进行详细介绍。回收内存,而是由JVM来管理对象内存的分配以及对象内存的回收(又称为垃圾回收、GC),这对于开发人员来说确实大大降低了编写程序的难度,但带来的一个副作用就是,当系统运行过程中出现JVM抛出的内存异常(例如OutOfMemoryError)的时候,很难知道原因是什么,另外一方面,要编写高性能的程序,通常需要借助内存来提升性能,因此如何才能合理的使用内存以及让JVM合理的进行内存的回收是必须掌握的。

       Runtime data area主要包括六个部分:Heap (堆), Method Area(方法区域), Java Stack(java的栈), Program Counter(程序计数器), Native method stack(本地方法栈),运行时常量池(Runtime Constant Pool)。Heap和Method Area是被所有线程的共享使用的;而Java stack, Program counter 和Native method stack是以线程为粒度的,每个线程独自拥有。

Heap(堆)

      Heap是大家最为熟悉的区域,它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收,Heap在32位的操作系统上最大为2G,在64位的操作系统上则没有限制,其大小通过-Xms和-Xmx来控制,-Xms为JVM启动时申请的最小Heap内存,默认为物理内存的1/64但小于1G,-Xmx为JVM可申请的最大Heap内存,默认为物理内存的1/4但小于1G,默认当空余堆内存小于40%时,JVM会增大Heap的大小到-Xmx指定的大小,可通过-XX:MinHeapFreeRatio=来指定这个比例,当空余堆内存大于70%时,JVM会将Heap的大小往-Xms指定的大小调整,可通过-XX:MaxHeapFreeRatio=来指定这个比例,但对于运行系统而言,为了避免频繁的Heap Size的大小,通常都会将-Xms和-Xmx的值设成一样,因此这两个用于调整比例的参数通常是没用的,Java 1.2以上的版本对jvm内存进行了分代管理。

     JVM将Heap分为NewGeneration和Old Generation(或Tenured Generation)两块来进行管理:

   1、New Generation

   又称为新生代,程序中新建的对象都将分配到新生代中,新生代又由Eden Space和两块Survivor Space构成,可通过-Xmn参数来指定其大小,Eden Space的大小和两块Survivor Space的大小比例默认为8,即当New Generation的大小为10M时,Eden Space的大小为8M,两块Survivor Space各占1M,这个比例可通过-XX:SurvivorRatio来指定。

   2、Old Generation

   又称为旧生代,用于存放程序中经过几次垃圾回收还存活的对象,例如缓存的对象等,旧生代所占用的内存大小即为-Xmx指定的大小减去-Xmn指定的大小。

   堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的,鉴于这样的原因,Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间,这块空间又称为TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配,TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效,但这种方法同时也带来了两个问题,一是空间的浪费,二是对象内存的回收上仍然没法做到像Stack那么高效,同时也会增加回收时的资源的消耗,可通过在启动参数上增加-XX:+PrintTLAB来查看TLAB这块的使用情况。

   当堆中需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

MethodArea(方法区域)

   方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,可见方法区域的重要性,同样,方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

   在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代,默认为64M,可通过-XX:PermSize以及-XX:MaxPermSize来指定其大小。

JavaStack(java的栈)

    JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址,因此Java中基本类型的变量是值传递,而非基本类型的变量是引用传递,SunJDK的实现中JVM栈的空间是在物理内存上分配的,而不是从堆上分配。

    由于JVM栈是线程私有的,因此其在内存分配上非常高效,并且当线程运行完毕后,这些内存也就被自动回收。

    当JVM栈的空间不足时,会抛出StackOverflowError的错误,在Sun JDK中可以通过-Xss来指定栈的大小,例如如下代码:

new Thread(newRunnable(){   public void run() {      loop(0);      }      private voidloop (int i){           if(i!=1000){               i++;               loop (i);           }else{               return;           }      }   }).start();

当JVM参数设置为-Xss1K,运行后会报出类似下面的错误:Exception inthread "Thread-0" java.lang.StackOverflowError。

ProgramCounter(程序计数器)

   每个运行中的Java程序,每一个线程都有它自己的PC寄存器,也是该线程启动时创建的。PC寄存器的内容总是指向下一条将被执行指令的地址;,这里的地址可以是一个本地指针,也可以是在方法区中相对应于该方法起始指令的偏移量。

Nativemethod stack(本地方法栈)

   JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

例如有这么一段代码:

public class A {public static void main(String[] args) {String a = "a";String b = "b";String ab = "ab";System.out.println((a + b) == ab); // falseSystem.out.println(("a" + "b") == ab); // truefinal String afinal = "a";String result = afinal + "b";System.out.println(result == ab); // trueString plus = a + "b";System.out.println(plus == ab); // falseSystem.out.println(plus.intern() == ab); // true}}

分析下上面代码执行的结果,可通过javap –verbose A来辅助理解分析。

(a+b)==ab:a+b是两个变量相加,需要到运行时才能确定其值,到运行时后JVM会为两者相加后产生一个新的对象,因此a+b==ab的结果为false。

(“a”+”b”)==ab:“a”+”b”是常量,在编译时JVM已经将其变为”ab”字符串了,而ab=”ab”也是常量,这两者在常量池即为同一地址,因此(“a”+”b”)==ab为true。

result==ab:result=afinal+”b”,afinal是个final的变量,result在编译时也已经被转变为了”ab”,和”ab”在常量池中同样为同一地址,因此result==ab为true。

plus=ab:plus和a+b的情况是相同的,因此plus==ab为false。

plus.intern()==ab:这里的不同点在于调用了plus.intern()方法,这个方法的作用是获取plus指向的常量池地址,因此plus.intern()==ab为true。

运行时常量池(Runtime Constant Pool)

   类似C中的符号表,存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配。

   在掌握了JVM对象内存分配的机制后,来看看JVM是如何做到自动的对象内存回收的,这里指的的是Heap以及Method Area的回收,其他几个区域的回收都由JVM简单的按生命周期来进行管理。


1 0
原创粉丝点击