1、JVM内存区域划分
大多数 JVM 将内存区域划分为 Method Area(Non-Heap),Heap,Program Counter Register, Java Method Stack,Native Method Stack和Direct Memomry(注意 Directory Memory 并不属于 JVM 管理的内存区域)。前三者通常译为:方法区、堆、程序计数器。但不一样的资料和书籍上对于后三者的中文译名不尽相同,这里将它们分别译做:Java 方法栈、原生方法栈和直接内存区。对于不一样的 JVM,内存区域划分可能会有所差别,好比 Hot Spot 就将 Java 方法栈和原生方法栈合二为一,咱们能够统称为方法栈(Method Stack)。
首先咱们熟悉一下一个通常性的 Java 程序的工做过程。一个 Java 源程序文件,会被编译为字节码文件(以 class 为扩展名),而后告知 JVM 程序的运行入口,再被 JVM 经过字节码解释器加载运行。那么程序开始运行后,都是如何涉及到各内存区域的呢?
归纳地说来,JVM 每遇到一个线程,就为其分配一个程序计数器、Java 方法栈和原生方法栈。当线程终止时,二者所占用的内存空间也会被释放掉。栈中存储的是栈帧,能够说每一个栈帧对应一个“运行现场”。在每一个“运行现场”中,若是出现了一个局部对象,则它的实例数据被保存在堆中,而类数据被保存在方法区。
2、指令、方法与属性
在讲各部分以前,咱们首先要搞清楚的是什么是数据以及什么是指令。而后要搞清楚对象的方法和对象的属性分别保存在哪里。
1)方法自己是指令的操做码部分,保存在Stack中;
2)方法内部变量做为指令的操做数部分,跟在指令的操做码以后,保存在Stack中(其实是简单类型保存在Stack中,对象类型在Stack中保存地址,在Heap 中保存值);上述的指令操做码和指令操做数构成了完整的Java 指令。
3)对象实例包括其属性值做为数据,保存在数据区Heap 中。
非静态的对象属性做为对象实例的一部分保存在Heap 中,而对象实例必须经过Stack中保存的地址指针才能访问到。所以可否访问到对象实例以及它的非静态属性值彻底取决于可否得到对象实例在Stack中的地址指针。
非静态方法和静态方法的区别:
非静态方法有一个和静态方法很重大的不一样:非静态方法有一个隐含的传入参数,该参数是JVM给它的,和咱们怎么写代码无关,这个隐含的参数就是对象实例在Stack中的地址指针。所以非静态方法(在Stack中的指令代码)老是能够找到本身的专用数据(在Heap 中的对象属性值)。固然非静态方法也必须得到该隐含参数,所以非静态方法在调用前,必须先new一个对象实例,得到Stack中的地址指针,不然JVM将没法将隐含参数传给非静态方法。
静态方法无此隐含参数,所以也不须要new对象,只要class文件被ClassLoader load进入JVM的Stack,该静态方法便可被调用。固然此时静态方法是存取不到Heap 中的对象属性的。
总结一下该过程:当一个class文件被ClassLoader load进入JVM后,方法指令保存在Stack中,此时Heap 区没有数据。而后程序技术器开始执行指令,若是是静态方法,直接依次执行指令代码,固然此时指令代码是不能访问Heap 数据区的;若是是非静态方法,因为隐含参数没有值,会报错。所以在非静态方法执行前,要先new对象,在Heap 中分配数据,并把Stack中的地址指针交给非静态方法,这样程序技术器依次执行指令,而指令代码此时可以访问到Heap 数据区了。
静态属性和动态属性:
前面提到对象实例以及动态属性都是保存在Heap 中的,而Heap 必须经过Stack中的地址指针才可以被指令(类的方法)访问到。所以能够推断出:静态属性是保存在Stack中的,而不一样于动态属性保存在Heap 中。正由于都是在Stack中,而Stack中指令和数据都是定长的,所以很容易算出偏移量,也所以无论什么指令(类的方法),均可以访问到类的静态属性。也正由于静态属性被保存在Stack中,因此具备了全局属性。
在JVM中,静态属性保存在Stack指令内存区,动态属性保存在Heap数据内存区。
3、Stack 栈
Stack(栈)是JVM的内存指令区。Stack管理很简单,push必定长度字节的数据或者指令,Stack指针压栈相应的字节位移;pop必定字节长度数据或者指令,Stack指针弹栈。Stack的速度很快,管理很简单,而且每次操做的数据或者指令字节长度是已知的。因此Java 基本数据类型,Java 指令代码,常量都保存在Stack中。
栈也叫栈内存,是 Java 程序的运行区,是在线程建立时建立,它的生命期是跟随线程的生命
期,线程结束栈内存也就释放,对于栈来讲不存在垃圾回收问题,只要线程一结束,该栈就 Over。
那么栈中存的是那些数据呢?又什么是格式呢?
栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是
一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F1,并
被压入到栈中,A 方法又调用了 B 方法,因而产生栈帧 F2 也被压入栈,执行完毕后,先弹出 F2
栈帧,再弹出 F1 栈帧,遵循“先进后出”原则。
那栈帧中到底存在着什么数据呢?栈帧中主要保存 3 类数据:本地变量(Local Variables),
包括输入参数和输出参数以及方法内的变量;栈操做(Operand Stack),记录出栈、入栈的操做;
栈帧数据(Frame Data),包括类文件、方法等等。光说比较枯燥,咱们画个图来理解一下 Java
栈,以下图所示:
4、Heap 堆
Heap(堆)是JVM的内存数据区。Heap 的管理很复杂,每次分配不定长的内存空间,专门用来保存对象的实例。在Heap 中分配必定的内存来保存对象实例,实际上也只是保存对象实例的属性值,属性的类型和对象自己的类型标记等,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配必定的内存保存对象实例和对象的序列化比较相似。而对象实例在Heap 中分配好之后,须要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。
Java中堆是由全部的线程共享的一块内存区域。
4.1 Generation
JVM堆通常又能够分为如下三部分:
◆Perm
Perm代主要保存class,method,filed对象,这部门的空间通常不会溢出,除非一次性加载了不少的类,不过在涉及到热部署的应用服务器的时候,有时候会遇到java.lang.OutOfMemoryError : PermGen space 的错误,形成这个错误的很大缘由就有多是每次都从新部署,可是从新部署后,类的class没有被卸载掉,这样就形成了大量的class对象保存在了perm中,这种状况下,通常从新启动应用服务器能够解决问题。
◆Tenured
Tenured区主要保存生命周期长的对象,通常是一些老的对象,当一些对象在Young复制转移必定的次数之后,对象就会被转移到Tenured区,通常若是系统中用了application级别的缓存,缓存中的对象每每会被转移到这一区间。
◆Young
Young区被划分为三部分,Eden区和两个大小严格相同的Survivor区,其中Survivor区间中,某一时刻只有其中一个是被使用的,另一个留作垃圾收集时复制对象用,在Young区间变满的时候,minor GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在通过几回垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间。
4.2 Sizing the Generations
JVM提供了相应的参数来对内存大小进行配置。正如上面描述,JVM中堆被分为了3个大的区间,同时JVM也提供了一些选项对Young,Tenured的大小进行控制。
◆Total Heap
-Xms :指定了JVM初始启动之后初始化内存
-Xmx:指定JVM堆得最大内存,在JVM启动之后,会分配-Xmx参数指定大小的内存给JVM,可是不必定所有使用,JVM会根据-Xms参数来调节真正用于JVM的内存
-Xmx -Xms之差就是三个Virtual空间的大小
◆Young Generation
-XX:NewRatio=8意味着tenured 和 young的比值8:1,这样eden+2*survivor=1/9
堆内存
-XX:SurvivorRatio=32意味着eden和一个survivor的比值是32:1,这样一个Survivor就占Young区的1/34.
-Xmn 参数设置了年轻代的大小
◆Perm Generation
-XX:PermSize=16M -XX:MaxPermSize=64M
Thread Stack
-XX:Xss=128K
5、The pc Register 程序计数器寄存器
JVM支持多个线程同时运行。每一个JVM都有本身的程序计数器。在任何一个点,每一个JVM线程执行单个方法的代码,这个方法是线程的当前方法。若是方法不是native的,程序计数器寄存器包含了当前执行的JVM指令的地址,若是方法是 native的,程序计数器寄存器的值不会被定义。 JVM的程序计数器寄存器的宽度足够保证能够持有一个返回地址或者native的指针。
6、Method Area 方法区
Object Class Data(类定义数据) 是存储在方法区的。除此以外,常量、静态变量、JIT 编译后的代码也都在方法区。正由于方法区所存储的数据与堆有一种类比关系,因此它还被称为 Non-Heap。方法区也能够是内存不连续的区域组成的,而且可设置为固定大小,也能够设置为可扩展的,这点与堆同样。
方法区内部有一个很是重要的区域,叫作运行时常量池(Runtime Constant Pool,简称 RCP)。在字节码文件中有常量池(Constant Pool Table),用于存储编译器产生的字面量和符号引用。每一个字节码文件中的常量池在类被加载后,都会存储到方法区中。值得注意的是,运行时产生的新常量也能够被放入常量池中,好比 String 类中的 intern() 方法产生的常量。
6.1 常量池 (constant pool)
常量池指的是在编译期被肯定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各类基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,好比:
◆类和接口的全限定名;
◆字段的名称和描述符;
◆方法和名称和描述符。
虚拟机必须为每一个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集和,包括直接常量(string,integer和 floating point常量)和对其余类型,字段和方法的符号引用。
对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的, 对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,注意:该表只存储文字字符串值,不存储符号引 用。说到这里,对常量池中的字符串值的存储位置应该有一个比较明了的理解了。
在程序执行的时候,常量池 会储存在Method Area,而不是堆中。
7、Java Method Stack Java 方法栈 与 Native Method Stack 原生方法栈
第七章内容来源:http://blog.csdn.net/poechant/article/details/7289093
Java 方法栈也是线程私有的,每一个 Java 方法栈都是由一个个栈帧组成的,每一个栈帧是一个方法运行期的基础数据结构,它存储着局部变量表、操做数栈、动态连接、方法出口等信息。当线程调用调用了一个 Java 方法时,一个栈帧就被压入(push)到相应的 Java 方法栈。当线程从一个 Java 方法返回时,相应的 Java 方法栈就弹出(pop)一个栈帧。 java
其中要详细介绍的是局部变量表,它保存者各类基本数据类型和对象引用(Object reference)。基本数据类型包括 boolean、byte、char、short、int、long、float、double。对象引用,本质就是一个地址(也能够说是一个“指针”),该地址是堆中的一个地址,经过这个地址能够找到相应的 Object(注意是“找到”,缘由会在下面解释)。而这个地址找到相应 Object 的方式有两种。一种是该地址存储着 Pointer to Object Instance Data 和 Pointer to Object Class Data,另外一种是该地址存储着 Object Instance Data,其中又包含有 Pointer to Object Class Data。以下两图所示。 程序员
第一种方式,Java 方法栈中有 Handler Pool 和 Instance Pool。不管哪一种方式,Object Class Data 都是存储在方法区的,Object Instance Data 都是存储在堆中的。 算法
图1 句柄方式 bootstrap
图2 直接方式 数组
原生方法栈与 Java 方法栈相相似,这里再也不赘述。 缓存
8、JVM运行原理 例子
以上都是纯理论,咱们举个例子来讲明 JVM 的运行原理,咱们来写一个简单的类,代码以下:
- public class JVMShowcase {
- //静态类常量,
- public final static String ClASS_CONST = "I'm a Const";
- //私有实例变量
- private int instanceVar=15;
- public static void main(String[] args) {
- //调用静态方法
- runStaticMethod();
- //调用非静态方法
- JVMShowcase showcase=new JVMShowcase();
- showcase.runNonStaticMethod(100);
- }
- //常规静态方法
- public static String runStaticMethod(){
- return ClASS_CONST;
- }
- //非静态方法
- public int runNonStaticMethod(int parameter){
- int methodVar=this.instanceVar * parameter;
- return methodVar;
- }
- }
这个类没有任何意义,不用猜想这个类是作什么用,只是写一个比较典型的类,而后咱们来看
看 JVM 是如何运行的,也就是输入 java JVMShow 后,咱们来看 JVM 是如何处理的:
向操做系统申请空闲内存。JVM 对操做系统说“给我 64M 空闲内存”,因而第 1 步,JVM 向操做系统申请空闲内存
做系统就查找本身的内存分配表,找了段 64M 的内存写上“Java 占用”标签,而后把内存段的起
始地址和终止地址给 JVM,JVM 准备加载类文件。
分配内存内存。
第 2 步,JVM 分配内存。JVM 得到到 64M 内存,就开始得瑟了,首先给 heap 分个内存,并
且是按照 heap 的三种不一样类型分好的,而后给栈内存也分配好。
文件。第 3 步,检查和分析 class 文件。若发现有错误即返回错误。
加载类。第 4 步,加载类。因为没有指定加载器,JVM 默认使用 bootstrap 加载器,就把 rt.jar 下的全部
类都加载到了堆类存的永久存储区,JVMShow 也被加载到内存中。咱们来看看栈内存,以下图:
Heap 是空,Stack 是空,由于尚未线程被执行。Class Loader 通知 Execution Enginer 已经加
载完毕。
执行引擎执行方法。第 5 步,执行引擎执行 main 方法。执行引擎启动一个线程,开始执行 main 方法,在 main 执
行完毕前,方法区以下图所示:
在 Method Area 加入了 CLASS_CONST 常量,它是在第一次被访问时产生的。堆内存中有两
个对象 object 和 showcase 对象,以下图所示:
为何会有 Object 对象呢?是由于它是 JVMShowcase 的父类,JVM 是先初始化父类,而后再
初始化子类,甭管有多少个父类都初始化。在栈内存中有三个栈帧,以下图所示:
于此同时,还建立了一个程序计数器指向下一条要执行的语句。
释放内存。运第 6 步,释放内存。运行结束,JVM 向操做系统发送消息,说“内存用完了,我还给你”
行结束。
9、JVM 相关问题
问:堆和栈有什么区别堆和栈有什么区别有什么
答:堆是存放对象的,可是对象内的临时变量是存在栈内存中,如例子中的 methodVar 是在运
行期存放到栈中的。
栈是跟随线程的,有线程就有栈,堆是跟随 JVM 的,有 JVM 就有堆内存。
问:堆内存中到底存在着什么东西?堆内存中到底存在着什么东西?
答:对象,包括对象变量以及对象方法。
问:类变量和实例变量有什么区别?类变量和实例变量有什么区别?有什么区别
答:静态变量是类变量,非静态变量是实例变量,直白的说,有 static 修饰的变量是静态变量,
没有 static 修饰的变量是实例变量。静态变量存在方法区中,实例变量存在堆内存中。
启动时就初始化好的,和你这说的不一样呀!
问:我据说类变量是在 JVM 启动时就初始化好的,和你这说的不一样呀!
答:那你是道听途说,信个人,没错。
的方法(函数)究竟是传值仍是传址值仍是传址?
问:Java 的方法(函数)究竟是传值仍是传址?
答:都不是,是以传值的方式传递地址,具体的说原生数据类型传递的值,引用类型传递的地
址。对于原始数据类型,JVM 的处理方法是从 Method Area 或 Heap 中拷贝到 Stack,而后运行 frame
中的方法,运行完毕后再把变量指拷贝回去。
产生?
问:为何会产生 OutOfMemory 产生?
答:一句话:Heap 内存中没有足够的可用内存了。这句话要好好理解,不是说 Heap 没有内存
了,是说新申请内存的对象大于 Heap 空闲内存,好比如今 Heap 还空闲 1M,可是新申请的内存需
要 1.1M,因而就会报 OutOfMemory 了,可能之后的对象申请的内存都只要 0.9M,因而就只出现
一次 OutOfMemory,GC 也正常了,看起来像偶发事件,就是这么回事。 但若是此时 GC 没有回
收就会产生挂起状况,系统不响应了。
问:我产生的对象很少呀,为何还会产生 OutOfMemory?我产生的对象很少呀,?
答:你继承层次忒多了,Heap 中 产生的对象是先产生 父类,而后才产生子类,明白不?
错误分几种?问:OutOfMemory 错误分几种?
答:分两种,分别是“OutOfMemoryError:java heap size”和”OutOfMemoryError: PermGen
space”,两种都是内存溢出,heap size 是说申请不到新的内存了,这个很常见,检查应用或调整
堆内存大小。
“PermGen space”是由于永久存储区满了,这个也很常见,通常在热发布的环境中出现,是
由于每次发布应用系统都不重启,长此以往永久存储区中的死对象太多致使新对象没法申请内存,
通常从新启动一下便可。
问:为何会产生 StackOverflowError??
答:由于一个线程把 Stack 内存所有耗尽了,通常是递归函数形成的。
之间能够互访吗?
问:一个机器上能够看多个 JVM 吗?JVM 之间能够互访吗?
答:能够多个 JVM,只要机器承受得了。JVM 之间是不能够互访,你不能在 A-JVM 中访问
B-JVM 的 Heap 内存,这是不可能的。在之前老版本的 JVM 中,会出现 A-JVM Crack 后影响到
B-JVM,如今版本很是少见。
要采用垃圾回收机制,的显式
问:为何 Java 要采用垃圾回收机制,而不采用 C/C++的显式内存管理?的显 内存管理?
答:为了简单,内存管理不是每一个程序员都能折腾好的。
问:为何你没有详细介绍垃圾回收机制?为何你没有详细介绍垃圾回收机制
答:垃圾回收机制每一个 JVM 都不一样,JVM Specification 只是定义了要自动释放内存,也就是
说它只定义了垃圾回收的抽象方法,具体怎么实现各个厂商都不一样,算法各异,这东西实在不必
深刻。
中到底哪些区域是共享的?哪些是私有的?
问:JVM 中到底哪些区域是共享的?哪些是私有的?
答:Heap 和 Method Area 是共享的,其余都是私有的,
问:什么是 JIT,你怎么没说?,你怎么没说?
答:JIT 是指 Just In Time,有的文档把 JIT 做为 JVM 的一个部件来介绍,有的是做为执行引
擎的一部分来介绍,这都能理解。Java 刚诞生的时候是一个解释性语言,别嘘,即便编译成了字
节码(byte code)也是针对 JVM 的,它须要再次翻译成原生代码(native code)才能被机器执行,于
是效率的担心就提出来了。Sun 为了解决该问题提出了一套新的机制,好,你想编译成原生代码,
没问题,我在 JVM 上提供一个工具,把字节码编译成原生码,下次你来访问的时候直接访问原生
码就成了,因而 JIT 就诞生了,就这么回事。
还有哪些部分是你没有提到的?
问:JVM 还有哪些部分是你没有提到的?
答:JVM 是一个异常复杂的东西,写一本砖头书都不为过,还有几个要说明的:
常量池(constant pool)按照顺序存放程序中的常量,:而且进行索引编号的区域。好比 int i =100,
这个 100 就放在常量池中。
安全管理器(Security Manager):提供 Java 运行期的安全控制,防止恶意攻击,好比指定读取
文件,写入文件权限,网络访问,建立进程等等,Class Loader 在 Security Manager 认证经过后才
能加载 class 文件的。
方法索引表(Methods table),记录的是每一个 method 的地址信息,Stack 和 Heap 中的地址指针
实际上是指向 Methods table 地址。
问:为何不建议在程序中显式的生命 System.gc()??
答:由于显式声明是作堆内存全扫描,也就是 Full GC,是须要中止全部的活动的(Stop The
World Collection),你的应用能承受这个吗?
问:JVM 有哪些调整参数?
答:很是多,本身去找,堆内存、栈内存的大小均可以定义,甚至是堆内存的三个部分、新生
代的各个比例都能调整。