【第22期】观点:IT 行业加班,到底有没有价值?

第二章 JAVA内存区域与内存溢出异常

原创 2015年07月21日 23:19:27

概述

Java程序员不需要自己去手动释放内存,而C、C++程序员就需要自己去操作了,原因是Java虚拟机帮我们完成了这个动作,所以我们也要了解jvm的机制,这样出问题后才会知道它到底是什么情况。本章介绍JVM内存各个区域。

运行时数据区域

根据《Java虚拟机规范(Java SE 7 版)》规定,Java虚拟机的内存包括以下几个运行时数据区域

程序计数器

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。Java虚拟机的多线程是通过线程切换并分配处理器执行时间的方式实现,程序计数器的作用就是记住当前线程执行到哪里。
当前线程所执行的字节码的行号指示器,字节码解释器就是改变当前程序计数器的行号来工作

Java虚拟机栈

英文名为 Java virtual machine stacks ,线程私有,生命周期与线程相同
每个方法在执行的时候都会创建一个栈帧(Stack Frame),方法从调用到执行的过程对应着栈帧的入栈和出栈。
栈帧的概念:用来存储局部变量表、操作数栈、动态链接、方法出口登信息。
局部变量表:存储的是编译器可知的各种基本数据类型和对象引用、returnAddress类型等,所需的内存在编译期完成分配
异常:
1. 请求的栈深度大于虚拟机所允许的深度,抛出StackOverflowError异常
2. 如果虚拟机栈可动态扩展时,申请不到足够的内存,抛出OutOfMemoryError异常

本地方法栈

本地方法栈和Java虚拟机栈的功能一样,只不过本地方法栈的方法是native方法

Java堆

Java虚拟机内存中最大的一块,存储对象实例和数组,线程共享区域。垃圾收集器管理的主要区域,也称GC堆。
可以处于物理上不连续的内存空间中,逻辑连续。
通过-Xmx和-Xms控制,若在堆中没有内存完成实例分配也无法扩展堆,将抛出OutOfMemoryError异常

方法区

线程共享内存,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,别名 Non-Heap(非堆)
内存回收针对的主要目标为常量池的回收和对类型的卸载(条件苛刻)
方法区无法满足内存分配需求,抛出OutOfMemoryError异常

运行时常量池

Runtime Constant Pool 方法区一部分,存放编译期生成的各种字面量和符号引用,运行期间也可能将新的常量放入池中
无法申请到内存时,抛出OutOfMemoryError异常

直接内存

Direct Memory不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但这部分内存也被频繁使用,也可导致OutOfMemoryError异常。
JDK1.4 新加入NIO,一种基于通道与缓冲区的I/O方式,可以使用native函数库直接分配堆外内存,然后通过DirectByteBuffer对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据。

hotspot虚拟机对象探秘

对象的创建

Created with Raphaël 2.1.0new关键字常量池中定位到一个符号引用?分配内存(内存已确认)将分配到的内存初始化为零值设置对象的对象头(e.g 哪个实例、如何找到类元数据信息、对象哈希码、对象GC分代年龄等)按程序进行初始化设置对象创建完成类加载yesno

java堆中分配内存的方法:指针碰撞(Bump the Pointer)、空闲列表(Free List)
并发操作指针非线程安全解决方案:①对分配内存空间的动作同步锁定处理、②将内存分配的动作按照线程话费到不同空间,即Thread Local Allocation Buffer,TLAB分配完并分配新的TLAB时,才需要同步锁定,虚拟机是否使用TLAB,通过-XX:+/-UseTLAB参数来设定

对象的内存布局

三块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
- 对象头
1. 官方称为Mark Word,长度在32和64位的虚拟机中分别为32bit和64bit。存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。因为需要存储很多的数据,所以其被设计成一个非固定数据结构,以便利用更多的空间,其根据对象状态复用当前空间
2. 类型指针,对象指向其类元数据的指针,确定此对象为哪个类的实例。若对象为java数组,对象头中还要有一块用于记录数组长度的区域
- 实例数据
对象真正存储的有效信息,即各种类型字段类容,无论是父类还是子类的,都要记录下来
- 对齐填充
不是必然存在的。占位符的作用。HotSpot VM自动内存管理系统要求对象起始地址必须为8字节的整数倍,对象头正好为8的整数倍,因此当实例数据没有对齐时需要自动补全来完成。

对象的访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。由于Java虚拟机规范中没有定义此引用该如何实现,所以实现方式也是随虚拟机变化的。
主流的访问方式有:

  • 使用句柄
    使用句柄

  • 直接指针
    直接指针

使用句柄访问的好处是当对象变动时,reference无需修改,只改变到对象实例的指针就行
使用直接指针的好处是,访问对象时比较快

实战:OutOfMemoryError异常

Java堆溢出

-Xms堆最小值 -Xmx堆最大值 -XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dump出当前内存堆转储快照以便时候进行分析
报错时显示异常堆栈信息 java.lang.OutOfMemoryError,会跟着进一步提示 Java heap space
解决此区域的异常,一般手段是通过内存映像分析工具(如 Eclipse Memory Analyzer),分清楚到底是内存泄露(Memory Leak)还是内存溢出(Memory Overflow)
记得要把下面的VM Args在eclipse中设置下。在Run/Debug的configurations中的标签Arguments中

import java.util.ArrayList;
import java.util.List;

/**
 *  VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 *  @author lishun
 *
 */
public class HeapOOM {
    static class OOMObject {}

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

执行结果
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid9152.hprof …
Heap dump file created [27849668 bytes in 0.090 secs]
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Unknown Source)
at java.util.Arrays.copyOf(Unknown Source)
at java.util.ArrayList.grow(Unknown Source)
at java.util.ArrayList.ensureExplicitCapacity(Unknown Source)
at java.util.ArrayList.ensureCapacityInternal(Unknown Source)
at java.util.ArrayList.add(Unknown Source)
at heap.HeapOOM.main(HeapOOM.java:17)

虚拟机栈和本地方法溢出

在hotSpot中是不区分虚拟机栈和本地方法栈的,所以-Xoss(设置本地方法栈大小)无效,栈容量由-Xss参数设定。
有2种异常可发生
1. 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常
2. 虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常

在单线程实验中,尝试下面2种方法均无法出现OutOfMemoryError异常
1. 使用-Xss参数减少栈内存容量。结果:抛出StackOverflowError异常
2. 定义了大量的本地变量,增大此方法帧中本地变量的长度。结果抛出StackOverflowError异常

/**
 * VM Args: -Xss128k
 * @author lishun
 *
 */
public class Zhan {

    private int stackLength = 1;

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

    public static void main (String[] args) {
        Zhan oom = new Zhan();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

运行结果
stack length:11419
Exception in thread “main” java.lang.StackOverflowError
at Zhan.stackLeak(Zhan.java:12)
at Zhan.stackLeak(Zhan.java:13)
at Zhan.stackLeak(Zhan.java:13)

结果表明,单线程模式下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候还是会抛出StackOverflowError异常。
但是再多线程时,可以通过不断的简历线程产生内存溢出异常,如下代码


/**
 * VM Args: -Xss128k
 * @author lishun
 *
 */
public class Zhan1 {

    private void dontStop () {
        while (true) {
        }
    }

    public void stackLeakByThread () {
        while (true) {
             Thread thread = new Thread (new Runnable () {
                 public void run () {
                     dontStop();
                 }
             });
             thread.start();
        }
    }

    public static void main (String[] args) {
        Zhan1 oom = new Zhan1();
        oom.stackLeakByThread();
    }
}

原因为,操作系统分配给每个进程的内存是有限制的,32位的windows为2GB。虚拟机提供参数来控制Java堆和方法去的这两部分内存的最大值,分配完剩余的内存为2GB减去Xmx,MaxPermSize,忽略程序计数器消耗的内存,如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由虚拟机和本地方法栈瓜分,每个线程分配到的栈容量越大,可以建立的线程自然就越少,建立线程就越容易把剩下的内存耗尽。

方法区和运行时常量池溢出

运行时常量池是方法区的一部分,所以此处溢出测试放在一起。
首先介绍下String.intern()。它的作用是:如果字符串常量池中已经包含一个等于String对象的字符串,则返回代表池中这个字符串的String对象;否则将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。在JDK1.6及之前的版本中,常量池分配在永久代内,所以通过-XX:PermSize和=XX:MaxPermSize限制方法区大小,从而间接限制方法池的容量。代码如下

import java.util.ArrayList;
import java.util.List;


/**
 * VM Args: -XX:PermSize=10M -XX:PermSize=10M
 * @author lishun
 *
 */
public class FangfaquAndChangliangchi {

    public static void main (String[] args) {
        //使用List保持着常量池引用,避免Pull GC回收常量池行为
        List<String> list = new ArrayList<String>();
        //10MB的PermSize在Integer范围内足够产生OOM了
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

会报Exception in thread “main” java.lang.OutOfMemoryError:PermGen space
说明运行时常量池属于方法区的一部分
如果用JDK1.7 此程序会一直运行下去,原因是1.7里的intern方法不会复制第一次出现的对象,而是复制该对象的引用

本机直接内存溢出

/**
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author lishun
 *
 */
public class Zhijie {

    private static final int _1M = 1024 * 1024;

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

此处没多研究了,下章是垃圾收集机制,每天回到家9点,都不怎么想写了,还是逼着自己每天写一点吧

版权声明:本文为博主原创文章,未经博主允许不得转载。 举报

相关文章推荐

Java内存区域介绍与Java内存溢出异常分析

本博客是在读《深入理解Java虚拟机》这本书第二章的一个笔记。 首先介绍一下Java执行程序时的内存区域: 如图,我们将内存分成了5大块区域:方法区,堆,虚拟机栈,本地方法栈以及程序计数器。 这几个区...

JVM——java 内存区域与内存溢出分析

转载自:http://matt33.com/2016/09/07/jvm-basic1/ 法区2.6. 运行时常量池 Runtime Constant Pool2.7. ...

程序员升职加薪指南!还缺一个“证”!

CSDN出品,立即查看!

深入理解java虚拟机笔记(一)-java内存区域与内存溢出

1. 前言这是深入理解java虚拟机一书的笔记,来自第二章。因为这本书讲的比较深奥,这是第二次看,需要记录一下笔记。2. 运行时数据区域java虚拟机所管理的内存分为以下几个区域。ps:图片来自网络2...

《深入理解Java虚拟机》读书笔记:第二章Java内存区域与内存溢出异常

《深入理解Java虚拟机》读书笔记:第二章Java内存区域与内存溢出异常

第二章:java内存区域与内存溢出异常

目的  最近在学习<>一书,特整理成学习笔记。概要  对于java而言,我们在编码的时候不需要显式的编码分配内存和回收内存,可以节约我们大量的时间去关注业务。但是不需要显式的编码,并不意味着我们不需要...
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)