|
|
51CTO旗下网站
|
|
移动端

浅谈虚拟机内存区

我们在编写程序时,经常会遇到OOM(out of Memory)以及内存泄漏等问题。为了避免出现这些问题,我们首先必须对JVM的内存划分有个具体的认识。JVM将内存主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。

作者:Wooola来源:今日头条|2019-04-09 15:56

【大咖·来了 第7期】10月24日晚8点观看《智能导购对话机器人实践》

 1. Java 虚拟机内存区概述

我们在编写程序时,经常会遇到OOM(out of Memory)以及内存泄漏等问题。为了避免出现这些问题,我们首先必须对JVM的内存划分有个具体的认识。JVM将内存主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。

2. Java 虚拟机运行时数据区

2.1. 运行时数据区划分

2.1.1 运行时数据区图

2.1.2 运行时数据区包括

  • 方法区(Method Area)
  • 虚拟机栈(VM Stack)
  • 本地方法栈(Native Method Stack)
  • 堆(Heap)
  • 程序计算器(Program Counter Register)

2.2. 方法区(Method Area)

2.2.1 方法区的概念

方法区又叫静态区,存放的是已加载的类的基本信息、常量、静态变量等。它是各个线程共享区域。

比方说我们在写Java代码时,每个线程度可以访问同一个类的静态变量对象。由于使用反射机制的原因,虚拟机很难推测哪那个类信息不再使用,因此这块区域的回收很难。

2.2.1.1 静态块和非静态块有什么区别?

  • 类(Class)和对象(Object)的区别与联系?
  • 为什么静态块中不能使用this、super关键字?
  • 为什么java的静态方法可以直接用类名调用?

2.2.2 方法区的特点

线程间共享区域

2.2.3 方法区的异常

对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。制造方法区内存溢出,注意,必须在JDK1.6及之前版本才会导致方法区溢出,原因后面解释,执行之前,可以把虚拟机的参数-XXpermSize和-XX:MaxPermSize限制方法区大小。

代码清单如下:

  1. public static void printOOM() { 
  2.  List<String> list = new ArrayList<String>(); 
  3.  int i = 0; 
  4.  while (true) { 
  5.  list.add(String.valueOf(i).intern()); 
  6.  } 

输出异常结果:

  1. Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 
  2.  at java.util.Arrays.copyOf(Arrays.java:2245) 
  3.  at java.util.Arrays.copyOf(Arrays.java:2219) 
  4.  at java.util.ArrayList.grow(ArrayList.java:242) 
  5.  at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216) 
  6.  at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208) 
  7.  at java.util.ArrayList.add(ArrayList.java:440) 
  8.  at com.vprisk.knowledgeshare.MethodAreExample.main(MethodAreExample.java:15) 
  9.  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
  10.  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) 
  11.  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 
  12.  at java.lang.reflect.Method.invoke(Method.java:606) 
  13.  at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147) 

备注:网上的例子运行后会抛出java.lang.OutOfMemoryError:PermGen space异常。

2.2.3.1 关于String的intern()函数

intern()的作用:

如果当前的字符串在常量池中不存在,则放入到常量池中。

上面的代码不断将字符串添加到常量池,最终肯定会导致内存不足,抛出方法区的OOM。解释一下,为什么必须将上面的代码在JDK1.6之前运行。我们前面提到JDK1.7后,把常量池放入到堆空间中,这导致intern()函数的功能不同,代码清单如下:

  1. public static void testInternMethod(){ 
  2.  String str1 =new StringBuilder("hua").append("chao").toString(); 
  3.  System.out.println(str1.intern()==str1); 
  4.  String str2=new StringBuilder("ja").append("va").toString(); 
  5.  System.out.println(str2.intern()==str2); 

在场景jdk6,输出结果:

  1. false , false 

在场景jdk7,输出结果:

  1. true , false 

为什么了?

原因是在JDK1.6中,intern()方法会把***遇到的字符串实例复制到常量池中,返回的也是常量池中的字符串的引用,而StringBuilder创建的字符串实例是在堆上面,所以必然不是同一个引用,返回false。在JDK1.7中,intern方法不再复制实例,常量池中只保存***出现的实例的引用,因此intern()返回的引用和由StringBuilder创建的字符串实例是同一个。为什么对str2比较返回的是false呢?这是因为,JVM中内部在加载类的时候,就已经有”java”这个字符串,不符合“***出现”的原则,因此返回false。

2.2.4 方法区的作用

方法区存放的是类信息、常量、静态变量等,是各个线程共享区域

2.2.5 方法区的运用

通过过设置虚拟机的参数 -XXpermSize 以及 -XX:MaxPermSize 限制方法区大小

2.2.6 方法区的使用场景

2.3. 虚拟机栈(VM Stack)

2.3.1 虚拟机栈的概念

虚拟机栈描述的是Java方法执行的内存模型:

每个方法被执行的时候都会同时创建一个栈帧 (StackFrame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程

2.3.1.1 局部变量表

局部变量表存放了编译器克制的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(Object reference)和字节码指令地址(returnAddress类型)。

2.3.1.1 操作栈

操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out, LIFO)栈。同局部变量表一样,操作数栈的***深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的***值。

当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令向操作数栈中写入和提取内容,也就是入栈出栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。

举个例子,整数加法的字节码指令iadd在运行的时候要求操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值和并相加,然后将相加的结果入栈。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。再以上面的iadd指令为例,这个指令用于整型数加法,它在执行时,最接近栈顶的两个元素的数据类型必须为int型,不能出现一个long和一个float使用iadd命令相加的情况。

2.3.1.1 动态链接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。我们知道Class文件的常量池有存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或***次使用的时候转化为直接引用,这种转化称为静态解析。另外一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

2.3.2 虚拟机栈的特点

  • 线程私有
  • 生命周期与线程相同

2.3.3 虚拟机栈的异常

2.3.3.1 一种是StackOverflowError

当前线程如果请求的栈深度大于虚拟机所允许的深度时,则会抛出该异常。例如,将一个函数反复递归自己,最终会出现栈溢出错误(StackOverflowError)。

代码清单如下:

  1. public class StackOverflowErrorDemo { 
  2.  public static void main(String []args){ 
  3.  printStackOverflowError(); 
  4.  } 
  5.  public static void printStackOverflowError(){ 
  6.  printStackOverflowError(); 
  7.  } 

输出异常结果:

  1. Exception in thread "main" java.lang.StackOverflowError 
  2. stack length:9482 
  3.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  4.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  5.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  6.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  7.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  8.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  9.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  10.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 
  11.  at com.itech.jvm.demo.StackOverflowErrorDemo.printStackOverflowError(StackOverflowErrorDemo.java:22) 

需要说明的是,在单个线程环境下,无论是栈帧太大,还是虚拟机栈容量太小,当内存无法分配时,虚拟机都会抛出 StackOverflowError 异常。

2.3.3.2 一种是OOM异常

当虚拟机栈支持动态扩展时,如果无法申请到足够多的内存时就会抛出OOM异常。代码清单如下:

  1. public class VMOOMDemo { 
  2.  public static void main(String[] args) throws Throwable { 
  3.  VMOOMDemo demo = new VMOOMDemo(); 
  4.  demo.printVMOOM(); 
  5.  } 
  6.  public void printVMOOM() { 
  7.  while (true) { 
  8.  new Thread() { 
  9.  public void run() { 
  10.  while (true) { 
  11.  } 
  12.  } 
  13.  }.start(); 
  14.  } 
  15.  } 

这个例子慎用...

本例通过不断地建立线程的方式产生内存溢出异常。但是,这样产生的内存溢出异常与栈空间是否足够大并不存在任何联系,或者准确地说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。 其原因是操作系统分配给每个进程的内存是有限制的,如32位的Windows限制为2GB。。

2.3.4 虚拟机栈的作用

用于存储局部变量、操作栈、动态链接、方法出口

2.3.5 虚拟机栈的运用

对于32位的jvm,默认大小为256kb, 而64位的jvm, 默认大小为512kb,可以通过-Xss设置虚拟机栈的***值。不过如果设置过大,会影响到可创建的线程数量。

2.3.6 虚拟机栈的使用场景

2.4. 本地方法栈(Native Method Stack)

2.4.1 本地方法栈的概念

本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

2.4.2 本地方法栈的特点

  • 线程私有
  • 为Native方法服务

2.4.3 本地方法栈的异常

与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

2.4.4 本地方法栈的作用

2.4.4.1 与java环境外交互

有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因,你可以想想java需要与一些底层系统如操作系统或某些硬件交换信息时的情况。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解java应用之外的繁琐的细节。

2.4.4.2 与操作系统交互

JVM支持着java语言本身和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统常常是强大的操作系统。通过使用本地方法,我们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,如果我们要使用一些java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

Sun's Java Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用java实现的,它也通过一些本地方法与外界交互。例如:类java.lang.Thread

的 setPriority()方法是用java实现的,但是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库(external dynamic link library)提供,然后被JVM调用。

2.4.5 本地方法栈的运用

2.4.6 本地方法栈的使用场景

  • 与java环境外交互
  • 与操作系统交互

2.5. Java堆(Heap)

2.5.1 Java 堆的概念

Java堆可以说是虚拟机中***一块内存了。它是所有线程所共享的内存区域,几乎所有的实例对象都是在这块区域中存放。当然,随着JIT编译器的发展,所有对象在堆上分配渐渐变得不那么“绝对”了。

Java堆是垃圾收集器管理的主要区域。由于现在的收集器基本上采用的都是分代收集算法,所有Java堆可以细分为:新生代和老年代。在细致分就是把新生代分为:

  • Eden空间
  • From Survivor
  • To Survivor

根据Java 虚拟机规范的规定:

Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可,就像我们的磁盘空间一样。在实现时,既可以实现成固定大小的,也可以是可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。

2.5.2 Java 堆的特点

线程间共享区域,在虚拟机启动时创建

是虚拟机中***的一块内存,几乎所有的实例对象都是在这块区域中存放

2.5.3 Java 堆的异常

当堆无法再扩展时,会抛出OutOfMemoryError异常。

2.5.4 Java 堆的作用

唯一目的就是存放对象实例,几乎所有的对象实例都在java堆中分配内存

2.5.5 Java 堆的运用

通过 -Xmx 和 -Xms 控制

2.5.6 Java 堆的使用场景

2.6. 程序计算器(Program Counter Register)

2.6.1 程序计算器的概念

类似于PC寄存器,程序计数器是线程私有的区域,每个线程都有自己的程序计算器。可以把它看成是当前线程所执行的字节码的行号指示器。

2.6.2 程序计算器的特点

  • 线程私有
  • 占用的内存空间小
  • 此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域

2.6.3 程序计算器的异常

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOM(OutOfMemoryError)情况的区域

2.6.4 程序计算器的作用

  • 信号指示器:多线程间切换时,需恢复每一个线程的当前执行位置,通过程序计数器中的值寻找要执行的指令的字节码
  • 如果线程在执行Java方法,计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是Native方法,计数器的值为空(Undefined)。

2.6.5 程序计算器的运用

通过 -Xmx 和 -Xms 控制

2.6.6 程序计算器的使用场景

2.7. 直接内存

2.7.1 直接内存的概念

2.7.1.1 什么是直接内存与非直接内存?

根据官方文档的描述:

A byte buffer is either direct or non-direct. Given a direct byte buffer, the Java virtual machine will make a best effort to perform native I/O operations directly upon it. That is, it will attempt to avoid copying the buffer's content to (or from) an intermediate buffer before (or after) each invocation of one of the underlying operating system's native I/O operations.

byte byffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。

直接内存(Direct Memory)既不属于虚拟机运行时数据区的一部分,也不属于Java虚拟机规范中定义的内存区域,但是这部分内存却被频繁地使用,而且还可能导致OutOfMemoryError异常出现。

对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而堆内存如果要作IO操作,会先复制到直接内存,再利用本地IO处理。

从数据流的角度,非直接内存的作用链:

本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

而直接内存的作用链:

本地IO-->直接内存-->本地IO

很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。

A direct byte buffer may be created by invoking the allocateDirect factory method of this class. The buffers returned by this method typically have somewhat higher allocation and deallocation costs than non-direct buffers. The contents of direct buffers may reside outside of the normal garbage-collected heap, and so their impact upon the memory footprint(内存占用) of an application might not be obvious. It is therefore recommended that direct buffers be allocated primarily for large, long-lived buffers that are subject to the underlying system's native I/O operations. In general it is best to allocate direct buffers only when they yield a measureable gain in program performance.

但是由于直接内存使用allocateDirect创建,它比申请普通的堆内存需要耗费更高的性能。不过它不会占用应用的堆内存。所以,当你有大量数据要缓存时,并且它的生命周期又比较长,那么使用直接内存是个不错的选择。但如果该选择不能带来显著的性能提升,推荐使用堆内存。在JDK1.4的NIO中,ByteBuffer有个方法是:

  1. public static ByteBuffer allocateDirect(int capacity) { 
  2.  return new DirectByteBuffer(capacity); 
  3. DirectByteBuffer(int cap) { 
  4.  ...... 
  5.  protected static final Unsafe unsafe = Bits.unsafe(); 
  6.  unsafe.allocateMemory(size); 
  7.  ...... 
  8. public final class Unsafe { 
  9.  ...... 
  10.  public native long allocateMemory(long var1); 
  11.  ...... 

另外直接内受限于本机总内存(包括RAM及SWAP区或者分页文件)的大小及处理器寻址空间的限制。

服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OutOfMemoryError异常。

2.7.2 直接内存的特点

  • 不受Java堆大小的限制
  • 既不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,不会占用应用的内存
  • IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作
  • 它比申请普通的堆内存需要耗费更高的性能。

2.7.3 直接内存的异常

动态扩展时出现OutOfMemoryError异常

2.7.4 直接内存的作用

基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

2.7.5 直接内存的运用

XX:MaxDirectMemorySize=10M

2.7.6 直接内存的使用场景

例如在IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。

本文基于jdk1.6、1.7

【编辑推荐】

  1. 程序员从宏观、微观角度浅析JVM虚拟机!
  2. 一文带你全面了解虚拟机的四种网络模型(图文并茂)
  3. 听说又被 JVM 内存区域方面的面试题给虐了?看看这篇文章吧!
  4. Type 1和Type 2虚拟机管理程序有什么区别?
  5. 确保云计算中虚拟机安全的4个步骤
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

16招轻松掌握PPT技巧

16招轻松掌握PPT技巧

GET职场加薪技能
共16章 | 晒书包

289人订阅学习

20个局域网建设改造案例

20个局域网建设改造案例

网络搭建技巧
共20章 | 捷哥CCIE

645人订阅学习

WOT2019全球人工智能技术峰会

WOT2019全球人工智能技术峰会

通用技术、应用领域、企业赋能三大章节,13大技术专场,60+国内外一线人工智能精英大咖站台,分享人工智能的平台工具、算法模型、语音视觉等技术主题,助力人工智能落地。
共50章 | WOT峰会

0人订阅学习

读 书 +更多

框架设计(第2版)CLR Via C#

作为深受编程人员爱戴和尊敬的编程专家,微软.NET开发团队的顾问,本书作者Jeffrey Richter针对开发各种应用程序(如Web Form、Windows For...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO播客