JVM学习笔记系列

来源:互联网 发布:数据决策分析系统 编辑:程序博客网 时间:2024/06/04 19:12

一、前言:JVM是每一位从事Java开发工程师必须翻越的一座大山!

JVM(Java Virtual Machine)是JRE的一部分,从字面上的意思来讲就是一个虚拟的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM虚拟了一套完善的硬件架构(处理器、堆栈、寄存器等,相应的指令系统)。Java语言最重要的特点就是跨平台运行,其关键就是JVM实现了跨平台操作。

JVM是Java字节码执行的引擎,为Java程序的执行提供必要的支持,它还能优化java字节码,使之转换成效率更高的机器指令。JVM虚拟机就好比是一个舞台,我们编写的程序最终都要在这个舞台上亮相。JVM的实现为JRE的共享类库,需要遵循JRE规范(Sun采用的JVM叫做Hot Spot)。

(备注:目前比较流行的JVM有 Dalvik、Hot Spot)

JVM屏蔽了于具体操作系统平台相关的信息,使得Java程序只需要生成在Java虚拟机上运行运行的目标代码(字节码),就可以在多种平台上不加修改地运行。Java虚拟机在执行字节码,实际上是把字节码解释成具体平台上的机器指令来执行。

在Java中引入虚拟机的概念,即在机器和编译器之间加入了一层抽象的虚拟机器。这台虚拟的机器在任何平台上都提供给编译程序一个共同的接口。编译程序只需要 面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种虚拟机理解的代码叫做字节码(ByteCode),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码。字节码由虚拟机解释执行,虚拟机将每一条需要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行。

经过上面对JVM的描述,总结归纳Java代码在JVM中的执行流程图如下所示:


(JVM代码执行流程图)


二、内存处理

大多数JVM将内存区域划分为Method Area(Non-Heap),Heap,Program Counter Register,Java Method Statck,Native Method Stack和Direct Memomry(备注:Directory Memory并不属于JVM管理的内存区域)。前三者一般翻译为:方法区、堆、程序计数器。但不同的资料和书籍对于后者的翻译名不尽相同,这里将他们分别翻译为:Java方法栈、本地方法栈和直接内存区域。对于不同的JVM。内存区域划分可能会有所差异,比如Hot Spot就将Java方法栈和本地方法栈合二为一,统称为方法栈(Method Stack)

首先我们熟悉一下一个一般的Java程序的工作过程。一个Java源文件,会被编译成字节码(ByteCode),然后告知JVM程序的运行入口,再被JVM通过字节码解释器加载运行,具体请参考JVM学习笔记-基础知识。那么程序开始运行后,是如何涉及到各内存区域的呢?

概括地说,JVM每遇到一个线程,就为其分配一个程序计数器、Java方法栈和本地方法栈。当线程终止时,两者所占有的内存空间会被释放掉。栈中保存的是栈帧,可以说每个栈帧对应一个“运行现场”。如果出现一个局部对象,则它的实例数据被保存在堆中,而类数据被保存在方法区。

我们用上面这一段文字就描述完了每个内存区域的基本功能,但是这还是比较粗糙,下面就分别介绍它们的存储对象、生存周期与空间管理策略。

程序计数器

  • 线程特性:私有
  • 存储内容:字节码文件指令地址(Java Methods),或Undefined(Native Methods)
  • 生命周期:随线程而生死
  • 空间策略:占用内存很小

这个最简单,就先从它说起。程序计数器,是线程私有(与线程共享相对)的,也就是说有N个线程,JVM就会分配N个程序计数器。如果当线程在执行一个Java方法,程序计数器记录着线程所执行的字节码文件中的指令地址。线程执行的是一个Native方法,则计数器值为Undefined。

程序计数器的生存周期多长呢?显然程序计数器是伴随着线程而生,伴随线程死而死的,并且程序计数器占用的内存空间也很小。

Java方法发栈与本地方法栈

Java方法栈也是线程私有的,每个Java方法栈都是由一个个栈帧组成的,每个栈帧是一个方法运行期的基础数据结构,它存储局部变量表,操作数栈、动态链表、方法出口等信息。当线程调用了一个Java方法时,一个栈帧就被压入(Push)到相应的Java方法栈。当线程从一个Java方法返回时,相应的Java方法栈就弹出(Pop)一个栈帧。

其中要详细介绍的是局部变量表,它保存着各种基本数据类型和对象引用(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。


图1:间接方式



图2:直接方式


第一种方式,Java方法栈中有Handler Pool和Instance Pool,无论哪种方式,Object Class Data都是存储在方法区的,Object Instance Data都是存储在堆中的。

本地方法栈和Java方法栈相类似,这里不再赘述。

堆是在启动虚拟机的时候划分出来的区域,其大小由参数或者默认参数指定。当虚拟机终止运行时,会释放堆内存 。一个JVM只有一个堆,它自然是线程共享的。堆中存储的是所有的Object Instant Data以及数组(不过随着栈上分配技术、标量替换技术等优化手段的发展,对象也不一定都存储在堆上了),这些Instance由垃圾管理器(Grabage Collector)管理,具体会在后面的章节阐述。

堆可以是由不连续的物理内存空间组成的,并且既可以固定大小,也可以设置为可扩展的(Scalable)。

方法区

通过上述Java方法栈的介绍,大家已经知道Object Class Data是存储在方法区的。除此之外,常量、静态变量、JIT编译后的代码也都都是在方法区。正因为方法去存储的数据与堆有一种类比关系,所以还被称为Non-Heap。方法区也可以是内存不连续的区域组成的,并且可设置为固定大小,也可以设置称为可扩展的,这点与堆一样。

方法区内部有一个非常重要的区域,叫做运行时常量池(Runtime Constant Pool,简称RCP)。在字节码文件中常量池(Constant Pool Table),用于存储编译器产生的字面量和符号引用。每个字节码文件中的常量池在类被加载后,都会存储到方法区中。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如String类中的intern()方法产生的常量。

直接内存区

直接内存区并不是JVM管理的内存区域的一部分,而是其之外。该区域也会在Java开发中使用到,并且存在导致内存溢出的隐患。如果对NIO有所了解,应该会知道NIO是可以使用Native Methods来使用直接内存区的。


三、JVM学习笔记-内存溢出

在实际编程过程中,会遇到一些OutOfMemory(OOM)异常。通过模拟。我们可以直接指出这些场景的本质,从而在纷繁复杂的千万行代码中避免这样去Coding。导致OOM的情况有多种,包括Java或Native Method Stack的内存不足或者栈空间溢出、Heap内存溢出、Non-heap内存溢出、Direct Memory溢出。

  1. Java Method Stack栈溢出模拟
什么时候会让Java Method Stack栈溢出?栈的基本特点就是FILO(First In Last Out),如果in的太多而out的太少,就可能overflow了。而Java Method Statck的功能就是保存每一次函数调用时的“现场”,即为入栈,函数返回对应出栈,所以函数的调用深度越大,栈就变得越大,足够大的时候就会溢出。所以模拟Java Method Stack溢出,只要不断递归调用某一函数就可以导致溢出。
[java] view plaincopy
  1. package com.jony.java;  
  2.   
  3. public class TestStackOverflow {  
  4.     private int stackLength = 0;  
  5.   
  6.     public void stackOverFlow(){  
  7.         ++stackLength;  
  8.         stackOverFlow();  
  9.     }  
  10.   
  11.     public static void main(String[] args){  
  12.         TestStackOverflow test = new TestStackOverflow();  
  13.         try {  
  14.             test.stackOverFlow();  
  15.         } catch (Throwable e) {  
  16.             System.out.println("Stack Length:" + test.stackLength);  
  17.             e.printStackTrace();  
  18.         }  
  19.     }  
  20.   
  21. }  
运行结果:
Stack Length:11477
java.lang.StackOverflowError
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:7)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)
at com.jony.java.TestStackOverflow.stackOverFlow(TestStackOverflow.java:8)

        2.  Java Method Stack内存溢出模拟-Heap内存溢出
堆是用来存储对象的,当然对象不一定都存在堆里(栈上分配、标量替换技术)。那么堆如果溢出了,一定是不能被杀掉的对象太多了。模拟Heap内存溢出,只要不断的创建对象并保存对象引用存在即可。
[java] view plaincopy
  1. package com.jony.java;  
  2.   
  3. import java.util.ArrayList;  
  4.   
  5. public class TestHeapOverflow {  
  6.   
  7.     static class TestOomHeap {}  
  8.   
  9.     public static void main(String[] args) {  
  10.         ArrayList<TestOomHeap> list = new ArrayList<TestHeapOverflow.TestOomHeap>();  
  11.   
  12.         while (true) {  
  13.             list.add(new TestOomHeap());  
  14.         }  
  15.     }  
  16.   
  17. }  
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.jony.java.TestHeapOverflow.main(TestHeapOverflow.java:13)


       3.  Method Area内存溢出
Method Area内存溢出,也就是Non-heap,是用来存储Object Class Data、变量、静态常量、JIT编译后的代码。如果该区域溢出,则说明某种数据创建的实在是太多了,模拟该异常,可以不断创建新的class,知道溢出为止。下面实例中会用到asm-all-3.0.jar和cglib2.2.jar 下载
[java] view plaincopy
  1. package com.jony.java;  
  2.   
  3. import java.lang.reflect.Method;  
  4. import net.sf.cglib.proxy.Enhancer;  
  5. import net.sf.cglib.proxy.MethodInterceptor;  
  6. import net.sf.cglib.proxy.MethodProxy;  
  7.   
  8. public class TestMethodAreaOverflow {  
  9.   
  10.     static class MethodAreaOom {}  
  11.   
  12.     public static void main(String[] args) {  
  13.         while (true) {  
  14.             Enhancer enhancer = new Enhancer();  
  15.             enhancer.setSuperclass(MethodAreaOom.class);  
  16.             enhancer.setUseCache(false);  
  17.             enhancer.setCallback(new MethodInterceptor() {  
  18.                   
  19.                 @Override  
  20.                 public Object intercept(Object arg0, Method arg1, Object[] arg2,  
  21.                         MethodProxy proxy) throws Throwable {  
  22.                     return proxy.invoke(arg0, arg2);  
  23.                 }  
  24.             });  
  25.             enhancer.create();  
  26.         }  
  27.     }  
  28.   
  29. }  
运行结果:
Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:237)
at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:285)
at com.jony.java.TestMethodAreaOverflow.main(TestMethodAreaOverflow.java:26)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:616)
at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:384)
at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:219)
... 3 more
Caused by: java.lang.OutOfMemoryError: PermGen space
at java.lang.ClassLoader.defineClass1(Native Method)
at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
... 8 more

4.Runtime Constant Pool In Method  Area溢出
在运行时产生大量常量就可以让Method Area溢出,运行时常量可以用String类的intern方法,不断产生新的常量。
[java] view plaincopy
  1. package com.jony.java;  
  2.   
  3. import java.util.ArrayList;  
  4.   
  5. public class RCPverflow {  
  6.   
  7.     public static void main(String[] args) {  
  8.         ArrayList<String> list = new ArrayList<String>();  
  9.         int i = 0;  
  10.         while (true) {  
  11.             list.add(String.valueOf(i++).intern());  
  12.         }  
  13.     }  
  14.   
  15. }  
运行结果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at com.jony.java.RCPverflow.main(RCPverflow.java:11)


总结
在实际编程中要尽量避免此类错误,不过大多数程序设计的结构要比实例复杂很多,使得问题被应藏,但JVM内存溢出问题本质上就是以上几种问题,因此在实际编程中应该避免JVM内存溢出情况。

0 0
原创粉丝点击