JVM简介及Java内存区域与内存溢出异常

一、JVM简介

1.1 JVM概念

(1) 虚拟机简介:指通过软件模拟的具有完整硬件功能的、运行在一个安全隔离的环境中的完整计算机系统。常见的虚拟机有JVM、VMwave、Virtual Box
(2) JVM和其他两个虚拟机的区别:VMwave与Virtual Box是通过软件模拟物理CPU的指令集,物理系统中会有很多的寄存器。而JVM则是通过软件模拟Java字节码的指令集,JVM中只是主要保留了PC寄存器,其他的寄存器都进行了剪裁,JVM是一台被定制过的现实当中不存在的计算机。
Java的跨平台的特性正是由于JVM才得以实现,一个.java文件通过Java编译器编译形成字节码文件(.class文件),这种.class文件可以运行在JVM上,而JVM可以被安装到不同的平台,这就带来了Java的跨平台特性。

1.2 Java和JVM的发展简史

(1) 20世纪Java的发展:
1996年 SUN JDK1.0发布:Classic VM 纯解释运行,使用外挂进行JIT(编译器)
1997年 JDK1.1 发布:AWT、内部类、JDBC、RMI、反射(Java的核心)
RMI:远程方法调用(Remote Method Invocation)。能够让在某个java虚拟机上的对象像调用本地对象一样调用另一个java 虚拟机中的对象上的方法。
1998年JDK 1.2发布:Solary Exact VM(仅存在了很短的时间)
JIT和解释器混合执行Accurate Memory Management 精确内存管理,数据类型敏感提升GC性能
JDK1.2开始成为Java2(J2SE,J2EE,J2ME出现),并且加入了Swing Collection。
(2) 二零零几年
2000年 JDK 1.3:HotSpot作为默认虚拟机发布
2002年 JDK 1.4:Classic VM退出历史舞台
JDK 1.4 更新内容:Assert,正则表达式,NIO,IPV6,日志API,加密类库,异常链,XML解析器等。
2004年 JDK 1.5 即Java5发布(很重要的一个版本)
Java5更新内容:泛型,注解,装箱,枚举,可变长参数,Foreach循环。
虚拟机层面:改进了Java内存模型(JMM),提供了JUC并发包。
2006年 JDK1.6 Java6发布:
更新内容:脚本编程的支持(动态语言支持),jdbc4.0,Java编译器API,微型Http服务器API等。
虚拟机层面:锁与同步,垃圾收集,类加载等算法的改动。
(3) 二零一几年
2011年JDK1.7/Java7发布:G1收集器(Update4才正式发布) 加强对非Java语言的调用支持升级类加载器架构64位系统压缩指针NIO2.0
2014年Java8发布:Lamda表达式语法增强Java类型注释等
4).Java与JVM发展历史中的大事件
使用最广泛的JVM为HotSpot HotSpot最早为Longview Technologies开发,被SUN收购
2006年,Java开源,并建立OpenJDK HotSpot成为SUN JDK和OpenJDK中所带的虚拟机
2008年,Oracle收购BEA 得到JRockit VM
2010年Oracle收购SUN 得到HotSpot
Orcale宣布在JDK8时整合HotSpot和JRockit VM,优势互补在HotSpot的基础上移植JRockit的优秀特性

二、Java内存区域与内存溢出异常

2.1 运行时数据区域

JVM会在执行Java程序的过程中把它管理的内存划分为若干个不同的数据区域。这些数据区域各有各的用处,各有各的创建与销毁时间,有的区域随着JVM进程的启动而存在,有的区域则依赖用户线程的启动和结束而创建与销毁。一般来说,JVM所管理的内存将会包含以下几个运行时数据区域:
线程私有区域:程序计数器、Java虚拟机栈、本地方法栈
线程共享区域:Java堆、方法区、运行时常量池

什么是线程私有?
由于JVM的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现,因此在任何一个确定的时刻,一个处理器(多核处理器则指的是一个内核)都只会执行一条线程中的指令。因此为了切换线程后能恢复到正确的执行位置,每条线程都需要独立的程序计数器,各条线程之间计数器互不影响,独立存储。我们就把类似这类区域称之为”线程私有”的内存。

2.1.1 程序计数器(线程私有)
  1. 程序计数器是一块比较小的内存空间,可以看做是当前线程所执行的字节码的行号指示器。
  2. 如果当前的线程执行的是Java方法,程序计数器记录的是正在执行的虚拟机字节码指令地址(代码行数),如果执行的是本地Native方法,计数器值为空
  3. 程序计数器是唯一一个在JVM规范中没有OOM(OutOfMemoryError,内存溢出)出现的区域
2.1.2 Java虚拟机栈(线程私有)

描述 Java 方法执行的内存模型:每个方法都会在虚拟机栈中创建一个栈帧用于存储局部变量表、操作数栈、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈和出栈的过程。声明周期与线程相同。

局部变量表: 存放了编译器可知的各种基本数据类型(8大基本数据类型)、对象引用。局部变量表所需的内存空间在
编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在执行期间
不会改变局部变量表大小。
方法的回收不是垃圾回收!!
此区域一共产生两种异常:
如果当前线程请求的栈深度 > 虚拟机栈深度(-Xss,设置栈大小),抛出 StackOverflow异常
如果虚拟机栈在动态扩展时,无法申请到足够大的内存,抛出OOM(OutOfMemoryError)异常

2.1.3 本地方法栈(线程私有)

服务的是本地方法 (native 方法),其余与虚拟机栈相同
HotSpot 中将本地方法栈与虚拟机栈合二为一

2.1.4 Java堆(GC堆,线程共享,垃圾回收的主要区域)
  1. Java堆在JVM启动时创建,存放的都是对象实例,JVM要求所有对象实例以及数组都在Java堆上存放
  2. Java堆可以处于物理上不连续的区域,Java堆一般来说都是可扩展的(-Xms:设置堆最小值,-Xmx:设置堆最大值)
  3. 如果堆中没有足够内存来完成对象实例化并且无法再扩展。抛出OOM。
2.1.5 方法区(线程共享,JDK1.8以前称之为永久代,以后称之为元空间)
  1. 存储已被JVM加载的类信息、常量、静态变量
  2. 此区域也会进行垃圾回收,主要是针对常量池的回收以及类型卸载
  3. 方法区无法满足内存分配需求时,抛出OOM
2.1.6 运行时常量池(线程共享,方法区一部分)

存放字面量以及符号引用
字面量:字符串、final常量、基本数据类型的值
符号引用:类和结构的完全限定名、字段的名称和描述符、方法的名称和描述符

2.2 Java堆溢出

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免来避免GC清除
这些对象,那么在对象数量达到最大堆容量后就会产生内存溢出异常。

范例:观察堆溢出

/**
* JVM参数为:-Xmx20m -Xms20m -XX:+HeapDumpOnOutOfMemoryError
*/

public class Test {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list =
        new ArrayList<>();
        while(true) {
            list.add(new OOMObject());
        }
    }
}

堆的OOM分为两种情况:内存泄漏、内存溢出
内存泄漏:泄漏对象无法被垃圾回收
内存溢出:该对象确实还应该存活,应该对比物理内存查看当前Jav堆是否还应扩容或者缩短对象存活时间

2.3 虚拟机栈和本地方法栈溢出

由于我们HotSpot虚拟机将虚拟机栈与本地方法栈合二为一,因此对于HotSpot来说,栈容量只需要由-Xss参数来
设置。
关于虚拟机栈会产生的两种异常:

  • 如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出StackOverFlow异常
  • 如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM异常

范例:观察StackOverFlow异常(单线程环境下)

/**
* JVM参数为:-Xss128k
*
*/
public class Test {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) {
        Test test = new Test();
        try {
            test.stackLeak();
        } catch (Throwable e) {
            System.out.println("Stack Length: "+test.stackLength);
            throw e;
        }
    }
}

出现StackOverflowError异常时有错误堆栈可以阅读,比较好找到问题所在。如果使用虚拟机默认参数,栈深度在多数情况下达到1000-2000完全没问题,对于正常的方法调用(包括递归),完全够用。
如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程。

范例:观察多线程下的栈内存溢出问题

/**
* JVM参数为:-Xss2M
*
*/
public class Test {
    private void dontStop() {
        while(true) {
    }
    }
    public void stackLeakByThread() {
        while(true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) {
        Test test = new Test();
        test.stackLeakByThread();
    }
}

运行以上代码需谨慎操作,记得提前保存好手头的工作!!!

猜你喜欢

转载自blog.csdn.net/yubujian_l/article/details/80792743