JVM内存结构和分析

性能分析之-- JAVA Thread Dump 分析综述 http://blog.csdn.net/rachel_luo/article/details/8920596

原文 http://my.oschina.net/u/2308739/blog/415106
一、JVM内存结构
1.1 下面总体说说内存

        Java虚拟机会将内存分为几个不同的管理区,这些区域各自有各自的用途,根据不同的特点,承担不同的任务以及在垃圾回收时运用不同的算法。总体分为下面几个部分:
程序计数器(Program Counter Register)、JVM虚拟机栈(JVM Stacks)、本地方法栈(Native Method Stacks)、堆(Heap)、方法区(Method Area)


  jvm 管理的内存大致包括三种不同类型的内存区域:

Permanent Generation space(永久保存区域) 其中永久保存区域主要存放Class(类)和Meta的信息,Class第一次被 Load 的时候被放入 PermGen space区域,Class需要存储的内容主要包括方法和静态属性。
Heap space(堆区域) 堆区域用来存放Class的实例(即对象),对象需要存储的内容主要是非静态属性。每次用new创建一个对象实例后,对象实例存储在堆区域中,这部分空间也被jvm的垃圾回收机制管理。
Java Stacks(Java栈)  Java栈跟大多数编程语言包括汇编语言的栈功能相似,主要基本类型变量以及方法的输入输出参数。
Java程序的每个线程中都有一个独立的堆栈。容易发生内存溢出问题的内存空间包括:Permanent Generation space和Heap space。

1.2 下面说说具体各个结构的功能

1.2.1、程序计数器(Program Counter Register)

      这是一块比较小的内存,不在Ram上,而是直接划分在CPU上的,程序员无法直接操作它,它的作用是:JVM在解释字节码文件(.class)时( 每次执行class里面的方法其实就是调用JVM方法区里面的字节码指令),存储当前线程所执行的字节码的行号,只是一种概念模型,各种JVM所采用的方式不同,字节码解释器工作时,就是通过改变程序计数器的值来选取下一条要执行的指令,分支、循环、跳转、等基础功能都是依赖此技术区完成的。
       还有一种情况,就是我们常说的Java多线程方面的,多线程就是通过现程轮流切换而达到的,同一时刻,一个内核只能执行一个指令,所以,对于每一个程序来说,必须有一个计数器来记录程序的执行进度,这样,当现程恢复执行的时候,才能从正确的地方开始, 所以,每个线程都必须有一个独立的程序计数器,这类计数器为线程私有的内存。如果一个线程正在执行一个Java方法,则计数器记录的是字节码的指令的地址,如果执行的一个Native方法,则计数器的记录为空,此内存区是唯一一个在Java规范中没有任何 OutOfMemoryError情况的区域。


1,2.2、JVM虚拟机栈(JVM Stacks)

      JVM虚拟机栈就是我们常说的堆栈的栈(我们常常把内存粗略分为堆和栈),和程序计数器一样, 也是线程私有的,生命周期和线程一样,每个方法被执行的时候会产生一个栈帧,用于存储局部变量表、动态链接、操作数、方法出口等信息。方法的执行过程就是栈帧在JVM中出栈和入栈的过程。局部变量表中存放的是各种基本数据类型,如boolean、byte、char、等8种, 及引用类型(存放的是指向各个对象的内存地址),因此,它有一个特点:内存空间可以在编译期间就确定,运行期不在改变。这个内存区域会有两种可能的Java异常:StackOverFlowError和OutOfMemoryError。
注意:Java 栈堆的区别与联系
      当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。  
      堆内存用来存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。

 
1.2.3、本地方法栈(Native Method Stacks)

      从名字即可看出,本地方法栈就是用来处理Java中的本地方法的,Java类的祖先类Object中有众多Native方法,如hashCode()、wait()等,他们的执行很多时候是借助于操作系统,但是JVM需要对他们做一些规范,来处理他们的执行过程。此区域,可以有不同的实现方法,向我们常用的Sun的JVM就是本地方法栈和JVM虚拟机栈是同一个。


1.2.4、堆(Heap)

      堆内存是内存中最重要的一块,也是最有必要进行深究的一部分。因为Java性能的优化,主要就是针对这部分内存的。 所有的对象实例及数组都是在堆上面分配的(随着JIT技术的逐渐成熟,这句话视乎有些绝对,不过至少目前还基本是这样的),可通过-Xmx和-Xms来控制堆的大小。JIT技术的发展产生了新的技术,如栈上分配和标量替换,也许在不久的几年里,即时编译会诞生及成熟,那个时候,“所有的对象实例及数组都是在堆上面分配的”这句话就应该稍微改改了。堆内存是垃圾回收的主要区域,所以在下文垃圾回收板块会重点介绍,此处只做概念方面的解释。在32位系统上最大为2G,64位系统上无限制。可通过-Xms和-Xmx控制,-Xms为JVM启动时申请的最小Heap内存,-Xmx为JVM可申请的最大Heap内存。


1.2.5、JVM 方法区(Method Area)

      方法区是所有线程共享的内存区域,用于存储已经被 JVM 加载的类信息、常量、静态变量等数据,一般来说,方法区属于持久代(关于持久代,会在GC部分详细介绍,除了持久代,还有新生代和旧生代),也难怪Java规范将方法区描述为堆的一个逻辑部分,但是它不是堆。方法区的垃圾回收比较棘手,就算是Sun的HotSpot VM在这方面也没有做得多么完美。此处引入方法区中一个重要的概念: 运行时常量池。主要用于存放在编译过程中产生的字面量(字面量简单理解就是常量)和引用。一般情况,常量的内存分配在编译期间就能确定,但不一定全是,有一些可能就是运行时也可将常量放入常量池中,如String类中有个Native方法intern()<关于intern()的详细说明,请看另一篇文章:http://blog.csdn.net/zhangerqing/article/details/8093919>
此处补充一个在JVM内存管理之外的一个内存区: 直接内存。在JDK1.4中新加入类NIO类,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,即我们所说的直接内存,这样在某些场景中会提高程序的性能。


1.2.6 JVM 垃圾回收(GC)

       在我们上面介绍的五大区中,有三个是不需要进行垃圾回收的:程序计数器、JVM栈、本地方法栈。因为它们的生命周期是和线程同步的,随着线程的销毁,它们占用的内存会自动释放, 所以只有方法区和堆需要进行GC。具体到哪些对象的话,简单概况一句话:如果某个对象已经不存在任何引用,那么它可以被回收。通俗解释一下就是说,如果一个对象,已经没有什么作用了,就可以被当废弃物被回收了。
       垃圾回收算法:
       根据一个经典的引用计数算法,每个对象添加一个引用计数器,每被引用一次,计数器加1,失去引用,计数器减1,当计数器在一段时间内保持为0时,该对象就认为是可以被回收得了。但是,这个算法有明显的缺陷:当两个对象相互引用,但是二者已经没有作用时,按照常规,应该对其进行垃圾回收,但是其相互引用,又不符合垃圾回收的条件,因此无法完美处理这块内存清理,因此Sun的JVM并没有采用引用计数算法来进行垃圾回收。而是采用一个叫:根搜索算法,如下图:


       基本思想就是:从一个叫GC Roots的对象开始,向下搜索,如果一个对象不能到达GC Roots对象的时候,说明它已经不再被引用,即可被进行垃圾回收(此处 暂且这样理解,其实事实还有一些不同,当一个对象不再被引用时,并没有完全“死亡”,如果类重写了finalize()方法,且没有被系统调用过,那么系统会调用一次finalize()方法,以完成最后的工作,在这期间,如果可以将对象重新与任何一个和GC Roots有引用的对象相关联,则该对象可以“重生”,如果不可以,那么就说明彻底可以被回收了),如上图中的Object5、Object6、Object7,虽然它们3个依然可能相互引用,但是总体来说,它们已经没有作用了,这样就解决了引用计数算法无法解决的问题。



1.3 字节码基础:JVM字节码初探

欢迎来到“Under The Hood“第三期。前两期我们分别介绍了JVM的基本结构和功能和Java类文件的基本结构,本期的主要内容有:字节码所操作的原始类型、类型转换的字节码,以及操作JVM栈的字节码。

字节码格式

字节码是JVM的机器语言。JVM加载类文件时,对类中的每个方法,它都会得到一个字节码流。这些字节码流保存在JVM的方法区中。在程序运行过程中,当一个方法被调用时,它的字节码流就会被执行。根据特定JVM设计者的选择,它们可以通过解释的方式,即时编译(Just-in-time compilation)的方式或其他技术的方式被执行。

方法的字节码流就是JVM的指令(instruction)序列。每条指令包含一个单字节的操作码(opcode)和0个或多个操作数(operand)。操作码指明要执行的操作。如果JVM在执行操作前,需要更多的信息,这些信息会以0个或多个操作数的方式,紧跟在操作码的后面。

每种类型的操作码都有一个助记符(mnemonic)。类似典型的汇编语言风格,Java字节码流可以用它们的助记符和紧跟在后面的操作数来表示。例如,下面的字节码流可以分解成多个助记符的形式。
// 字节码流: 03 3b 84 00 01 1a 05 68 3b a7 ff f9
// 分解后:
iconst_0      // 03
istore_0      // 3b
iinc 0, 1     // 84 00 01
iload_0       // 1a
iconst_2      // 05
imul          // 68
istore_0      // 3b
goto -7       // a7 ff f9

字节码指令集被设计的很紧凑。除了处理跳表的2条指令以外,所有的指令都以字节边界对齐。操作码的总数很少,一个字节就能搞定。这最小化了JVM加载前,通过网络传输的类文件的大小;也使得JVM可以维持很小的实现。

JVM中,所有的计算都是围绕栈(stack)而展开的。因为JVM没有存储任意数值的寄存器(register),所有的操作数在计算开始之前,都必须先压入栈中。因此,字节码指令主要是用来操作栈的。例如,在上面的字节码序列中,通过iload_0先把本地变量(local variable)入栈,然后用iconst_2把数字2入栈的方式,来计算本地变量乘以2。两个整数都入栈之后,imul指令有效的从栈中弹出它们,然后做乘法,最后把运算结果压入栈中。istore_0指令把结果从栈顶弹出,保存回本地变量。 JVM被设计成基于栈,而不是寄存器的机器,这使得它在如80486寄存器架构不佳的处理器上,也能被高效的实现。

猜你喜欢

转载自panyongzheng.iteye.com/blog/2211923