前言

在JVM的管控下,Java程序员不再需要管理内存的分配与释放,这和在C和C++的世界是完全不一样的。所以,在JVM的帮助下,Java程序员很少会关注内存泄露和内存溢出的问题。但是,一旦JVM发生这些情况的时候,如果你不清楚JVM内存的内存管理机制是很难定位与解决问题的。

一、JVM 内存区域

Java虚拟机在运行时,会把内存空间分为若干个区域,根据《Java虚拟机规范(Java SE 7 版)》的规定,Java虚拟机所管理的内存区域分为如下部分:方法区、堆内存、虚拟机栈、本地方法栈、程序计数器。

 

1、方法区

方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据。在jdk1.7及其之前,方法区是堆的一个“逻辑部分”(一片连续的堆空间),但为了与堆做区分,方法区还有个名字叫“非堆”,也有人用“永久代”(HotSpot对方法区的实现方法)来表示方法区。

从jdk1.7已经开始准备“去永久代”的规划,jdk1.7的HotSpot中,已经把原本放在方法区中的静态变量、字符串常量池等移到堆内存中,(常量池除字符串常量池还有class常量池等),这里只是把字符串常量池移到堆内存中;在jdk1.8中,方法区已经不存在,原方法区中存储的类信息、编译后的代码数据等已经移动到了元空间(MetaSpace)中,元空间并没有处于堆内存上,而是直接占用的本地内存(NativeMemory)。根据网上的资料结合自己的理解对jdk1.3~1.6、jdk1.7、jdk1.8中方法区的变迁画了张图如下(如有不合理的地方希望读者指出):

 

去永久代的原因有:

(1)字符串存在永久代中,容易出现性能问题和内存溢出。

(2)类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。

(3)永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

2、堆内存

堆内存主要用于存放对象和数组,它是JVM管理的内存中最大的一块区域,堆内存和方法区都被所有线程共享,在虚拟机启动时创建。在垃圾收集的层面上来看,由于现在收集器基本上都采用分代收集算法,因此堆还可以分为新生代(YoungGeneration)和老年代(OldGeneration),新生代还可以分为 Eden、From Survivor、To Survivor。

3、程序计数器

程序计数器是一块非常小的内存空间,可以看做是当前线程执行字节码的行号指示器,每个线程都有一个独立的程序计数器,因此程序计数器是线程私有的一块空间,此外,程序计数器是Java虚拟机规定的唯一不会发生内存溢出的区域。

4、虚拟机栈

虚拟机栈也是每个线程私有的一块内存空间,它描述的是方法的内存模型,直接看下图所示:

 

虚拟机会为每个线程分配一个虚拟机栈,每个虚拟机栈中都有若干个栈帧,每个栈帧中存储了局部变量表、操作数栈、动态链接、返回地址等。一个栈帧就对应 Java 代码中的一个方法,当线程执行到一个方法时,就代表这个方法对应的栈帧已经进入虚拟机栈并且处于栈顶的位置,每一个 Java 方法从被调用到执行结束,就对应了一个栈帧从入栈到出栈的过程。

5、本地方法栈

本地方法栈与虚拟机栈的区别是,虚拟机栈执行的是 Java 方法,本地方法栈执行的是本地方法(Native Method),其他基本上一致,在 HotSpot 中直接把本地方法栈和虚拟机栈合二为一,这里暂时不做过多叙述。

6、元空间

上面说到,jdk1.8 中,已经不存在永久代(方法区),替代它的一块空间叫做 “ 元空间 ”,和永久代类似,都是 JVM 规范对方法区的实现,但是元空间并不在虚拟机中,而是使用本地内存,元空间的大小仅受本地内存限制,但可以通过 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 来指定元空间的大小。

二、JVM 内存溢出

1、堆内存溢出

堆内存中主要存放对象、数组等,只要不断地创建这些对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾收集回收机制清除这些对象,当这些对象所占空间超过最大堆容量时,就会产生 OutOfMemoryError 的异常。堆内存异常示例如下:

/**

* 设置最大堆最小堆:-Xms20m -Xmx20m

* 运行时,不断在堆中创建OOMObject类的实例对象,且while执行结束之前,GC Roots(代码中的oomObjectList)到对象(每一个OOMObject对象)之间有可达路径,垃圾收集器就无法回收它们,最终导致内存溢出。

*/

public class HeapOOM {

static class OOMObject {

}

public static void main(String[] args) {

List<OOMObject> oomObjectList = new ArrayList<>();

while (true) {

oomObjectList.add(new OOMObject());

}

}

}

运行后会报异常,在堆栈信息中可以看到:

java.lang.OutOfMemoryError: Java heap space 的信息,说明在堆内存空间产生内存溢出的异常。

新产生的对象最初分配在新生代,新生代满后会进行一次 Minor GC,如果 Minor GC 后空间不足会把该对象和新生代满足条件的对象放入老年代,老年代空间不足时会进行 Full GC,之后如果空间还不足以存放新对象则抛出 OutOfMemoryError 异常。

常见原因:内存中加载的数据过多如一次从数据库中取出过多数据;集合对对象引用过多且使用完后没有清空;代码中存在死循环或循环产生过多重复对象;堆内存分配不合理;网络连接问题、数据库问题等。

2、虚拟机栈/本地方法栈溢出

(1)StackOverflowError:当线程请求的栈的深度大于虚拟机所允许的最大深度,则抛出StackOverflowError,简单理解就是虚拟机栈中的栈帧数量过多(一个线程嵌套调用的方法数量过多)时,就会抛出StackOverflowError异常。

最常见的场景就是方法无限递归调用,如下:

/**

* 设置每个线程的栈大小:-Xss256k

* 运行时,不断调用doSomething()方法,main线程不断创建栈帧并入栈,导致栈的深度越来越大,最终导致栈溢出。

*/

public class StackSOF {

private int stackLength=1;

public void doSomething(){

stackLength++;

doSomething();

}

public static void main(String[] args) {

StackSOF stackSOF=new StackSOF();

try {

stackSOF.doSomething();

}catch (Throwable e){//注意捕获的是Throwable

System.out.println("栈深度:"+stackSOF.stackLength);

throw e;

}

}

}

上述代码执行后抛出:

Exception in thread "Thread-0" java.lang.StackOverflowError 的异常。

(2)OutOfMemoryError:如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError。

我们可以这样理解,虚拟机中可以供栈占用的空间≈可用物理内存 - 最大堆内存 - 最大方法区内存,比如一台机器内存为 4G,系统和其他应用占用 2G,虚拟机可用的物理内存为 2G,最大堆内存为 1G,最大方法区内存为 512M,那可供栈占有的内存大约就是 512M,假如我们设置每个线程栈的大小为 1M,那虚拟机中最多可以创建 512个线程,超过 512个线程再创建就没有空间可以给栈了,就报 OutOfMemoryError 异常了。

 

栈上能够产生 OutOfMemoryError 的示例如下:

/**

* 设置每个线程的栈大小:-Xss2m

* 运行时,不断创建新的线程(且每个线程持续执行),每个线程对一个一个栈,最终没有多余的空间来为新的线程分配,导致OutOfMemoryError

*/

public class StackOOM {

private static int threadNum = 0;

public void doSomething() {

try {

Thread.sleep(100000000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

public static void main(String[] args) {

final StackOOM stackOOM = new StackOOM();

try {

while (true) {

threadNum++;

Thread thread = new Thread(new Runnable() {

@Override

public void run() {

stackOOM.doSomething();

}

});

thread.start();

}

} catch (Throwable e) {

System.out.println("目前活动线程数量:" + threadNum);

throw e;

}

}

}

上述代码运行后会报异常

在堆栈信息中可以看到java.lang.OutOfMemoryError: unable to create new native thread的信息,无法创建新的线程,说明是在扩展栈的时候产生的内存溢出异常。

总结:在线程较少的时候,某个线程请求深度过大,会报 StackOverflow 异常,解决这种问题可以适当加大栈的深度(增加栈空间大小),也就是把 -Xss 的值设置大一些,但一般情况下是代码问题的可能性较大;在虚拟机产生线程时,无法为该线程申请栈空间了。

会报 OutOfMemoryError 异常,解决这种问题可以适当减小栈的深度,也就是把 -Xss 的值设置小一些,每个线程占用的空间小了,总空间一定就能容纳更多的线程,但是操作系统对一个进程的线程数有限制,经验值在 3000~5000 左右。

在 jdk1.5 之前 -Xss 默认是 256k,jdk1.5 之后默认是 1M,这个选项对系统硬性还是蛮大的,设置时要根据实际情况,谨慎操作。

3、方法区溢出

前面说到,方法区主要用于存储虚拟机加载的类信息、常量、静态变量,以及编译器编译后的代码等数据,所以方法区溢出的原因就是没有足够的内存来存放这些数据。

由于在 jdk1.6 之前字符串常量池是存在于方法区中的,所以基于 jdk1.6 之前的虚拟机,可以通过不断产生不一致的字符串(同时要保证和 GC Roots 之间保证有可达路径)来模拟方法区的 OutOfMemoryError 异常;但方法区还存储加载的类信息,所以基于 jdk1.7 的虚拟机,可以通过动态不断创建大量的类来模拟方法区溢出。

/**

* 设置方法区最大、最小空间:-XX:PermSize=10m -XX:MaxPermSize=10m

* 运行时,通过cglib不断创建JavaMethodAreaOOM的子类,方法区中类信息越来越多,最终没有可以为新的类分配的内存导致内存溢出

*/

public class JavaMethodAreaOOM {

public static void main(final String[] args){

try {

while (true){

Enhancer enhancer=new Enhancer();

enhancer.setSuperclass(JavaMethodAreaOOM.class);

enhancer.setUseCache(false);

enhancer.setCallback(new MethodInterceptor() {

@Override

public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

return methodProxy.invokeSuper(o,objects);

}

});

enhancer.create();

}

}catch (Throwable t){

t.printStackTrace();

}

}

}

上述代码运行后会报:

java.lang.OutOfMemoryError: PermGen space 的异常,说明是在方法区出现了内存溢出的错误。

4、本机直接内存溢出

本机直接内存(DirectMemory)并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但 Java 中用到 NIO 相关操作时(比如 ByteBuffer 的 allocteDirect 方法申请的是本机直接内存),也可能会出现内存溢出的异常。

总结

JVM内存区域划分,便于它能够更加高效的管理自身的内存。当程序中出现这种由于JVM造成的内存溢出的情况的时候,需要根据不同的情况做不同的分析与处理。

BAT面试必问题系列:深入详解JVM 内存区域及内存溢出分析的更多相关文章

  1. 直通BAT必考题系列:深入详解JVM内存模型与JVM参数详细配置

    VM基本是BAT面试必考的内容,今天我们先从JVM内存模型开启详解整个JVM系列,希望看完整个系列后,可以轻松通过BAT关于JVM的考核. BAT必考JVM系列专题 1.JVM内存模型 2.JVM垃圾 ...

  2. 反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑) C#中缓存的使用 C#操作redis WPF 控件库——可拖动选项卡的TabControl 【Bootstrap系列】详解Bootstrap-table AutoFac event 和delegate的分别 常见的异步方式async 和 await C# Task用法 c#源码的执行过程

    反爬虫:利用ASP.NET MVC的Filter和缓存(入坑出坑)   背景介绍: 为了平衡社区成员的贡献和索取,一起帮引入了帮帮币.当用户积分(帮帮点)达到一定数额之后,就会“掉落”一定数量的“帮帮 ...

  3. 【tomcat系列】详解tomcat架构(上篇)

    java中,常用的web服务器一般由tomcat,weblogic,jetty,undertwo等,但从用户使用广泛度来说,tomcat用户量相对比较大一些,当然这也基于它开源和免费的特点. 从软件架 ...

  4. BAT批处理中的字符串处理详解(字符串截取)

    BAT批处理中的字符串处理详解(字符串截取)   BAT批处理中的字符串处理详解(字符串截取 批处理有着具有非常强大的字符串处理能力,其功能绝不低于C语言里面的字符串函数集.批处理中可实现的字符串处理 ...

  5. PHP输出缓存ob系列函数详解

    PHP输出缓存ob系列函数详解 ob,输出缓冲区,是output buffering的简称,而不是output cache.ob用对了,是能对速度有一定的帮助,但是盲目的加上ob函数,只会增加CPU额 ...

  6. SignalR新手系列教程详解总结(转)

    SignalR新手系列教程详解总结 GlobalHost.ConnectionManager.GetHubContext<TodoListHub>() .Clients.Clients(l ...

  7. Java虚拟机详解----JVM常见问题总结

    [声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4 ...

  8. Java虚拟机详解----JVM内存结构

    http://www.cnblogs.com/smyhvae/p/4748392.htm 主要内容如下: JVM启动流程 JVM基本结构 内存模型 编译和解释运行的概念 一.JVM启动流程: JVM启 ...

  9. Java 详解 JVM 工作原理和流程

    Java 详解 JVM 工作原理和流程 作为一名Java使用者,掌握JVM的体系结构也是必须的.说起Java,人们首先想到的是Java编程语言,然而事实上,Java是一种技术,它由四方面组成:Java ...

随机推荐

  1. SpringBoot -- 配置mysql、hibernate

    # application.properties# Server settings (ServerProperties)server.port=8081server.address=127.0.0.1 ...

  2. 用 Python 解答两道来自阿里伯乐系统的笔试题

    目录 目录 前言 题目一 分析 实现 题目二 分析 实现 前言 朋友到阿里面试,分享两道小题,博主比较闲就试着用 Python 解答一下,实现方式肯定是多种多样的,优劣也会各有不同,欢迎交流. 题目一 ...

  3. httpd启动显示Could not reliably determine the server's fully qualified domain name, using 127.0.0.1. Set the 'ServerName'

    AH00557: httpd: apr_sockaddr_info_get() failed for masterAH00558: httpd: Could not reliably determin ...

  4. 7.k8s.调度器scheduler 亲和性、污点

    #k8s. 调度器scheduler 亲和性.污点 默认调度过程:预选 Predicates (过滤节点) --> 优选 Priorities(优先级排序) --> 优先级最高节点 实际使 ...

  5. c# AES128 加解密算法

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.D ...

  6. Chapter03 第四节 c++的算术运算符

    c++算术运算符 3.4.1 运算符的优先级和结合性 四则运算参考四则运算优先级,注意括号. 3.4.2 除法的默认类型转换和精度问题 /* * @Description: 除法 * @Author: ...

  7. BCD与ASCII码互转-C语言实现

    /*BCD 与 ASCII码转换*/ /******************************************************************* 函数名:  asc2bc ...

  8. 【嵌入式 Linux文件系统】如何使用NFS文件系统

    (1)内核配置 取消选项 General setup-->Initial RAM filesystem and RAM disk (initramfs/initrd) support 进入Fil ...

  9. Python环境配置:anaconda+pycharm一站式解决

    https://www.cnblogs.com/yuxuefeng/p/9235431.html 不错的博文,码一下.

  10. Text Autosizer&&解决移动端网页文本字体怪异增大问题

    在做移动端页面时,有时你设置了字体大小,有的部分即使设置了行内样式也不生效,而有些显示正常,这个特性就是Text Autosizer在搞鬼. 以下是解决方案: ①给元素设置 -webkit-text- ...