下载安卓APP箭头
箭头给我发消息

客服QQ:3315713922

编程语言浅谈 Java 虚拟机内存区

作者:Wooola     来源: 架构荟萃点击数:714发布时间: 2020-03-16 09:03:48

标签: 编程语言javaJavascript

大神带你学编程,欢迎选课

浅谈 java 虚拟机内存区。在过去的几十年间,大量的编程语言被发明、被取代、被修改或组合在一起。尽管人们多次试图创造一种通用的程序设计语言,却没有一次尝试是成功的。之所以有那么多种不同的编程语言存在的原因是,编写程序的初衷其实也各不相同;新手与老手之间技术的差距非常大,而且有许多语言对新手来说太难学;还有,不同程序之间的运行成本(runtime cost)各不相同。

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

编程语言浅谈 Java 虚拟机内存区_编程语言_ java _Javascriptb _课课家

Java 虚拟机运行时数据区

运行时数据区划分

运行时数据区包括

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

一、方法区(Method Area)

方法区的概念

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

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

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

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

方法区的特点

  • 线程间共享区域

方法区的异常

对这块区域主要是针对常量池回收,值得注意的是 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) 

关于 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 

为什么了?

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

方法区的作用

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

方法区的运用

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

二、虚拟机栈(VM Stack)

虚拟机栈的概念

Java 方法执行的内存模型:

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

局部变量表

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

操作栈

操作数栈也常被称为操作栈,它是一个后入先出(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 命令相加的情况。

动态链接

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

虚拟机栈的特点

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

虚拟机栈的异常

一种是 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 异常。

一种是 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 限制为 2 GB。。

虚拟机栈的作用

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

虚拟机栈的运用

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

三、本地方法栈(Native Method Stack)

本地方法栈的概念

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

本地方法栈的特点

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

本地方法栈的异常

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

本地方法栈的作用

  • 与 java 环境外交互

有时 java 应用需要与 java 外面的环境交互。这是本地方法存在的主要原因,你可以想想 java 需要与一些底层系统如操作系统或某些硬件交换信息时的情况。

本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解 java 应用之外的繁琐的细节。

  • 与操作系统交互

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 调用。

四、Java 堆(Heap)

Java 堆的概念

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

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

  • Eden 空间
  • From Survivor
  • To Survivor

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

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

Java 堆的特点

  • 线程间共享区域,在虚拟机启动时创建
  • 是虚拟机中最大的一块内存,几乎所有的实例对象都是在这块区域中存放

Java 堆的异常

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

Java 堆的作用

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

Java 堆的运用

通过 -Xmx 和 -Xms 控制

五、程序计算器(Program Counter Register)

程序计算器的概念

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

程序计算器的特点

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

程序计算器的异常

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

程序计算器的作用

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

程序计算器的运用

通过 -Xmx 和 -Xms 控制

六、直接内存(Direct Memory)

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

根据官方文档的描述:

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 异常。

直接内存的特点

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

直接内存的异常

动态扩展时出现 OutOfMemoryError 异常

直接内存的作用

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

直接内存的运用

  1. XX:MaxDirectMemorySize=10M 

直接内存的使用场景

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

编程语言往往使程序员能够比使用机器语言更准确地表达他们所想表达的目的。对那些从事计算机科学的人来说,懂得程序设计语言是十分重要的,因为在当今所有的计算都需要程序设计语言才能完成。

 

赞(15)
踩(0)
分享到:
华为认证网络工程师 HCIE直播课视频教程