JVM内存结构

一.内存结构

jvm的内存空间可分为5个部分:程序计数器,Java虚拟机栈,本地方法栈,方法区,堆。

1.1 程序计数器

1.1.1 定义

它是一块比较小的内存空间,是当前线程正在执行的那条字节码指令的地址。

1.1.2 作用
  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制。
  2. 在多线程的情况下,程序计数器记录的是当前线程执行的位置,从而当线程切换回来时,就知道上次线程执行到哪了。
1.1.3 特点
  1. 是一块较小的内存空间
  2. 线程私有,每条线程都有自己的程序计数器
  3. 生命周期与线程相同
  4. 是唯一一个不会出现OOM的内存区域

1.2 Java虚拟机栈

1.2.1 定义

它是描述Java方法运行过程的内存模型。Java虚拟机栈为每一个即将运行的Java方法创建一个名为 栈帧 的区域,用于存放该方法运行过程中的一些信息,如:局部变量表,操作数栈,动态链接,方法出口信息...

1.2.2 作用

从上图可以看出在局部变量表中存放了编译期可知的各种基本数据类型,对象引用,returnAddress等信息。当方法运行过程中需要创建局部变量时,就会将对应的值方法局部变量表里,Java虚拟机栈的栈顶是当前正在执行的活动栈,也就是当前正在执行的方法,程序计数器也会指向这个地址。只有这个活动的栈帧的本地变量才可以被操作数栈使用,当在这个栈帧中调用另一个方法,与之对应的栈帧又会被创建,新创建的栈帧压入栈顶,变为当前的活动栈帧。方法结束后,当前栈帧被移出,栈帧的返回值变成新的活动栈帧中操作数栈的一个操作数。如果没有返回值,那么新的活动栈帧中操作数栈的操作数没有变化。

方法调用时的一些概念:

  1. 静态链接:当一个字节码文件被装载进JVM内部时,如果被调用的方法在编译期可见,且运行时保持不变,这个情况下调用方的符号引用转为直接引用的过程称为静态链接(静态方法)。
  2. 动态链接:如果被调用的方法无法在编译期被确定下来,只能在运行期间将调用的方法的符号引用转为直接引用,这种转换过程具备动态性,因此被称为动态链接。
  3. 方法绑定:分为早期绑定和晚期绑定。前者指被调用的目标方法如果在编译期可知,且运行期保持不变;后者指被调用的方法在编译期无法被确定,只能在程序运行期根据实际的类型绑定相关的方法(与静态链接,动态链接类似)。
  4. 非虚方法:如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚静态方法。私有方法,final方法,实例构造器,父类方法,静态方法都是非虚方法,除了这些以外都是虚方法。
  5. 虚方法表:面向对象的编程中,会很频繁的使用动态分配,如果每次动态分配的过程都要重新在类的方法元数据中搜索合适的目标的话,就可能影响到执行效率,因此为了提高性能,JVM采用在类的方法区建立一个虚方法表,使用索引表来代替查找。
  6. 方法重写的本质:找到操作数栈顶的第一个元素所执行的对象实际类型,记作C,如果在类型C中找到常量池中描述符和简单名称都相符的方法,则进行访问权限校验。如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError 异常。如果没找到相符的方法,则按照继承关系从下往上依次对C的各个父类进行上一步的搜索和验证过程。如果始终没找到合适的方法,则抛出java.lang.AbstractMethodError 异常。
1.2.3 特点
  1. 运行速度很快,仅次于程序计数器。
  2. 局部变量表随着栈帧的创建而创建,它的大小在编译时被确定,在运行过程中,大小不会发生改变。
  3. 会出现两种异常:若 Java 虚拟机栈的大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度时,抛出 StackOverFlowError 异常;若允许动态扩展,那么当线程请求栈时内存用完了,无法再动态扩展时,抛出 OutOfMemoryError 异常。
  4. 生命周期与线程相同。
  5. 出现StackOverFlowError时,内存空间可能还有很多。

1.3 本地方法栈(C栈)

1.3.1 定义

它是为了JVM运行Native方法准备的空间,由于很多Native方法都是用C语言实现的,所以它通常又叫C栈。它与Java虚拟机栈实现的功能类似,只不过本地方法栈是描述本地方法运行过程的内存模型。

1.3.2 作用

它的作用与Java虚拟机栈类似,这里不再赘述。二者的区别在于虚拟机栈为Java方法服务,而本地方法栈为Native方法服务。

1.3.3 特点

本地方法执行时,在本地方法栈也会创建一块栈帧,用于存放该方法的局部变量表,操作数栈,动态链接,方法出口等信息。方法结束后,相应的栈帧也会出栈,并释放内存空间,与Java虚拟机栈类似。

1.4 堆

1.4.1 定义

堆事用来存放对象的内存空间,几乎所有的对象都存储在堆里。

1.4.2 作用

GC触发条件:

  1. 显式调用System.gc(),老年代空间不够,方法去的空间不够等情况都会触发Full GC,同时对新生代和老年代回收,Full GC的时间最长,尽量避免
  2. 在出现Major GC之前,会先触发Minor GC,如果老年代的空间还是不够就会出发Major GC,时间长于Minor GC

对象分配:

  1. new出来的对象先放在Eden区,大小有限制
  2. 如果创建新对象时,Eden空间填满了,就会触发Minor GC,将Eden中不再被引用的对象进行销毁,再加载新的对象到Eden区,如果Survivor区满了是不会触发Minor GC的,而Eden空间填满了,Minor GC才顺便清理Survivor区。
  3. 将Eden中剩余对象移到Survivor0区
  4. 再次触发垃圾回收,此时上次Survivor下来的,放在Survivor0区,如果没有回收则放到Survivor1区
  5. 再次经历垃圾回收,又会将幸存者重新放回Survivor0区,以此类推
  6. 默认是15次的循环,超过15次,会将幸存者区幸存下来的对象转移到老年代。可通过 -XX:MaxTenuringThreshold=N 设置次数
  7. 频繁在新生代收集,很少在老年代收集,几乎不在永久代收集

对象的引用方式:

  1. 强引用:创建一个对象并把这个对象赋给一个引用变量,普通new出来对象的变量都是强引用,有引用变量指向时永远不会被垃圾回收,可以将引用赋值为null,那么它所指向的对象就会被垃圾回收。
  2. 弱引用:非必需对象,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱饮用关联的对象。
  3. 软引用:如果一个对象具有软引用,内存空间足够,垃圾回收器就不会回收它,如果内存空间不足,就会回收。只要垃圾回收器没有回收它,该对象就可以被程序使用。
  4. 虚引用:虚引用并不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
1.4.3 特点
  1. 线程共享,整个Java虚拟机只有一个堆,所有线程都访问同一个堆
  2. 在虚拟机启动时创建
  3. 是垃圾回收的主要场所
  4. Java虚拟机规范中,堆可以处于物理上不连续的内存空间,但在逻辑上它应该被视为连续的
  5. 关于Survivor1 和 Survivor0,复制后有交换,谁空谁就是Survivor1

1.5 方法区

1.5.1 定义

在Java虚拟机规范中定义方法区是堆的一个逻辑部分,用于存放:已经被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码。

1.5.2 特点
  1. 线程共享
  2. 永久代:方法区中的信息一般长期存在,而且又是堆的逻辑分区,因此用堆的划分方法称为永久代
  3. 内存回收效率低
  4. Java虚拟机规范对方法区的要求比较宽松,和堆一样,允许固定大小,也允许动态扩展,还允许不实现垃圾回收

二. OutOfMemoryError问题分析

OutOfMemoryError简称OOM,Java中对OOM的解释是,没有空闲内存,并且GC也无法提供更多内存。通俗的解释是:JVM内存不足了。

2.1 堆空间溢出

堆空间溢出是一个典型的OOM场景,错误信息为:java.lang.OutOfMemoryError: Java heap space

Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免GC回收这些对象,那么当堆空间达到最大容量限制就会产生OOM。

分析步骤

  1. 使用jmap或-XX:+HeapDumpOnOutOfMemoryError获取堆快照
  2. 使用内存分析工具(visualvm,mat,jProfile等)对堆快照文件进行分析
  3. 根据分析图,重点确认内存中的对象是否是必要的,分清究竟是内存泄漏还是内存溢出
2.1.1 内存泄漏

内存泄漏是指由于疏忽或错误造成程序为能释放已经不再使用的内存。

内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该内存的控制,因而造成了内存的泄漏。内存泄漏随着被执行的次数不断增加,最终会导致内存溢出。

常见场景

  1. 静态容器:声明为static的hashMap,Vector等集合。通俗来说A中有B,当前只把B设置为空,A没有设置为空,回收时B无法回收,因为还被A所引用。
  2. 监听器:监听器被注册后释放对象时没有删除监听器。
  3. 物理连接:各种连接池建立了连接,必须通过close()关闭连接。
  4. 内部类和外部模块等引用:发现它的方式同内存溢出,可以加个实时观察:jstat -gcutil 7362 2500 70

示例:

/**
 * 内存泄漏示例
 * 错误现象:java.lang.OutOfMemoryError: Java heap space
 * VM Args:-verbose:gc -Xms10M -Xmx10M -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOutOfMemoryDemo {

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

    static class OomObject {}

}

上述代码不断向容器中添加元素,但是没有清理,导致容器内存不断膨胀。

2.1.2 内存溢出

如果不存在内存泄漏,即内存中的对象确实都必须存活着,则应当检查虚拟机的堆参数(-Xmx和-Xms),与机器物理内存进行对比,看看是否可以调大,并从代码上检查是否存在某些对象生命周期较长,持有时间过长的情况,尝试减少程序运行期间的内存消耗。

示例:

/**
 * 堆溢出示例
 * <p>
 * 错误现象:java.lang.OutOfMemoryError: Java heap space
 * <p>
 * VM Args:-verbose:gc -Xms10M -Xmx10M
 *
 */
public class HeapOutOfMemoryDemo {

    public static void main(String[] args) {
        Double[] array = new Double[999999999];
        System.out.println("array length = [" + array.length + "]");
    }

}

上述代码是一个很极端的例子,试图创建一个纬度很大的数组,堆内存无法分配这么大的内存,从而报错OOM。

2.2 GC开销超过限制

超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常:java.lang.OutOfMemoryError: GC overhead limit exceeded。这意味着,在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制,导致异常的原因一般是因为堆太小,没有足够的内存。

示例:

/**
 * GC overhead limit exceeded 示例
 * 错误现象:java.lang.OutOfMemoryError: GC overhead limit exceeded
 * VM Args: -Xms10M -Xmx10M
 */
public class GcOverheadLimitExceededDemo {

    public static void main(String[] args) {
        List<Double> list = new ArrayList<>();
        double d = 0.0;
        while (true) {
            list.add(d++);
        }
    }

}

与堆空间溢出的错误处理方式类似,先判断是否存在内存泄漏,如果有则需要修正代码, 如果没有就去调整堆内存大小。

2.3 无法新建本地线程

Java应用程序已达到其可以启动线程数的限制时,会抛出java.lang.OutOfMemoryError: Unable to create new native thread。

当发起一个线程的创建时,虚拟机会在JVM内存创建一个Thread对象同时创建一个操作系统线程,而这个系统线程的内存用的不是JVM内存,而是系统中剩下的内存。那么,究竟能创建多少线程呢?这里有个公式:

线程数 = (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize)

MaxProcessMemory:一个进程的最大内存

JVMMemory:JVM内存

ReservedOsMemory:保留的操作系统内存

ThreadStackSize:线程栈的大小

给JVM分配的内存越多,那么能用来创建系统线程的内存就会越少,越容易发生无法创建本地线程的错误,所以JVM内存不是分配的越大越好。

该异常的产生流程:

  1. JVM内部运行的应用程序请求新的Java线程
  2. JVM本机代码代理为操作系统创建新本机线程的请求
  3. 操作系统尝试创建一个新的本机线程,该线程需要将内存分配给该线程
  4. 操作系统将拒绝本季内存分配,原因是32为Java进程大小已耗尽其内存地址空间(已达到2-4G进程大小限制)或者操作系统的虚拟内存已完全耗尽。
  5. 引发java.lang.OutOfMemoryError: Unable to create new native thread错误

示例:

public class UnableCreateNativeThreadErrorDemo {

    public static void main(String[] args) {
        while (true) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.MINUTES.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
}

解决该异常的办法可已通过增加操作系统级别的限制来绕过无法创建新的本机线程问题,例如,如果限制JVM可在用户空间中产生的进程数,则应检查出并可能增加该限制:

[root@dev ~]# ulimit -a

core file size (blocks, -c) 0

--- cut for brevity ---

max user processes (-u) 1800

通常,OOM对新的本机线程的限制表示编程错误,当应用程序产生数千个线程时,很可能出了一些问题:很少有应用程序可以从如此大量的线程中受益,解决问题的一种办法是开始进行线程转储以了解情况。

2.4 直接内存溢出

由直接内存导致的内存溢出,一个明显的特征是在Head Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,就可以考虑检查一下是不是这方面的原因。

示例:

/**
 * 本机直接内存溢出示例
 * 错误现象:java.lang.OutOfMemoryError
 * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
 */
public class DirectOutOfMemoryDemo {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) throws IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe) unsafeField.get(null);
        while (true) {
            unsafe.allocateMemory(_1MB);
        }
    }

}

2.5 StackOverflowError

对于HotSpot虚拟机来说,栈容量只由-Xss参数来决定如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

常见原因

  1. 递归函数调用层数太深
  2. 大量循环或者死循环

示例:

public class StackOverflowDemo {

    private int stackLength = 1;

    public void recursion() {
        stackLength++;
        recursion();
    }

    public static void main(String[] args) {
        StackOverflowDemo obj = new StackOverflowDemo();
        try {
            obj.recursion();
        } catch (Throwable e) {
            System.out.println("栈深度:" + obj.stackLength);
            e.printStackTrace();
        }
    }

}
  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值