鲁迅曾说过:Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进来,墙里面的人想出去。

一.虚拟机内存分布

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。

1.  程序计数器(Program Counter Register)

  程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。

  由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储。称之为“线程私有”的内存。程序计数器内存区域是虚拟机中唯一没有规定OutOfMemoryError情况的区域。

2.  Java虚拟机栈(Java Virtual Machine Stacks)

  java虚拟机也是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

  咱们常说的堆内存、栈内存中,栈内存指的就是虚拟机栈。局部变量表存放了编译期可知的各种基本数据类型(8个基本数据类型)、对象引用(地址指针)、returnAddress类型。

  局部变量表所需的内存空间在编译期间完成分配。在运行期间不会改变局部变量表的大小。

  这个区域规定了两种异常状态:如果线程请求的栈深度大于虚拟机所允许的深度,则抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,在扩展是无法申请到足够的内存,就会抛出OutOfMemoryError异常。

3.  本地方法栈(Native Method Stack)

  本地方法栈与虚拟机栈所发挥作用非常相似,它们之间的区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务。本地方法栈也是抛出两个异常。

4.  Java堆(Java Heap)

  java堆是java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

  java堆是垃圾收集器管理的主要区域,因此也被成为“GC堆”(Garbage Collected Heap)。从内存回收角度来看java堆可分为:新生代和老生代(当然还有更细致的划分,在下一章会讲到)。从内存分配的角度看,线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。无论怎么划分,都与存放内容无关,无论哪个区域,存储的都是对象实例,进一步的划分都是为了更好的回收内存,或者更快的分配内存。

  根据Java虚拟机规范的规定,java堆可以处于物理上不连续的内存空间中。当前主流的虚拟机都是可扩展的(通过 -Xmx 和 -Xms 控制)。如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5.  方法区(Method Area)

  方法区与java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。它有个别命叫Non-Heap(非堆)。当方法区无法满足内存分配需求时,抛出OutOfMemoryError异常。

6.  运行时常量池(Runtime Constant Pool)

  运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在加载后进入方法区的运行时常量池中存放。

7.  直接内存(Direct Memory)

  直接内存不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。但这部分区域也呗频繁使用,而且也可能导致OutOfMemoryError异常

  在JDK1.4中新加入的NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

二.HotSpot虚拟机对象探秘 

了解Java虚拟机的运行时数据区之后,我们再深入探讨下HotSpot虚拟机(虚拟机的一种)在Java堆中对象分配、布局和访问的全过程。

1.  对象的创建

  在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,看看虚拟机中对象(限于普通Java对象,不包括数组和Class对象等)的创建具体的过程。

  虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那么必须执行相应的类加载过程,在之后章节将会详细探讨。

  在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

  分配内存有两种方式:当java堆中内存绝对规整时,使用过的内存放在一边,空闲的内存放在一边,中间放着一个指针作为分界点。这种方式叫“指针碰撞”(Bump The Pointer);还有一种是java堆内存不规整,使用内存与空闲内存交错,这时虚拟机会维护一个列表,记录哪些内存是可用的,需要分配时在列表中找到一块够大的内存分配出去,这种方式叫“空闲列表”(Free List)

  内存分配完成后,虚拟机需要将分配到内存空间都初始化为零值(不包括对象头),如果使用TLAB(线程私有的分配缓冲区),这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用。

2.  对象的内存布局

  对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

  对象头包括两部分信息:第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称这部分为(Mark Word)。另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录。存储位置:1.相同宽度的字段总是被分配到一起;2.父类中的定义在子类的之前。

  对齐填充没有特别的含义,它仅仅起着占位符的作用。JVM要求对象的起始地址必须是8字节的整数倍。

3.  对象的访问定位

  建立对象是为了使用对象,我们的java程序需要通过栈上的reference数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。如图:

  两张访问方式有各有优势。使用句柄来访问最大的好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例指针,而reference本身不需要修改。

  使用直接指针访问方式最大的好处是速度更快,它节省了一次指针定位的时间开销,                                  

三.实战:OutOfMemoryError异常  

通过若干个实例来验证异常的发生场景,并且初步介绍几个与内存相关的最基本的虚拟机参数。

1.  Java堆溢出

  Java堆用于存储对象实例,我们只要不断创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清楚这些对象。

/**
* 堆内存最小 20m 最大20m,设置虚拟机在内存溢出时dump出当前快照
* VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
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_pid5536.hprof ...
Heap dump file created [ bytes in 0.138 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:)
at java.util.Arrays.copyOf(Arrays.java:)
at java.util.ArrayList.grow(ArrayList.java:)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:)
at java.util.ArrayList.add(ArrayList.java:)
at jvm.HeapOOM.main(HeapOOM.java:)

  要解决这个区域的异常,一般都需要使用一些内存映射分析工具(如 Eclipse Memory Analyzer)

2.  虚拟机栈和本地方法栈溢出

  如果线程请求深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。

/**
* 设定栈容量
* -Xss128k
*/
public class StackSOF {
private int stackInt = ;
public void stackLeak() {
stackInt++;
stackLeak();
} public static void main(String[] args) {
StackSOF sof = new StackSOF();
try {
sof.stackLeak();
} catch (Throwable e) {
System.out.println("stackInt length:" + sof.stackInt);
throw e;
}
}
}

运行结果:

stackInt length:
Exception in thread "main" java.lang.StackOverflowError
at jvm.StackSOF.stackLeak(StackSOF.java:)
at jvm.StackSOF.stackLeak(StackSOF.java:)

实验结果表明:在单线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配时,虚拟机都是抛出StackOverflowError异常。

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

/**
* 这时候不妨设置的大些 特别提示:如果你想尝试运行这段代码,记得保存当前工作,很可能电脑会卡死机
* -Xss4M
*/
public class StackOOM {
private void notStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
notStop();
}
});
thread.start();
}
}
public static void main(String[] args) {
StackOOM oom = new StackOOM();
oom.stackLeakByThread();
}
}

运行结果:

我在windows环境下,出现内存使用爆满,电脑卡死,只能被迫重启电脑。

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

  方法区用于存放Class相关信息,基本思路就是运行时产生大量的类去填满方法区,直到溢出。

/**
* 方法区内存10M 方法区最大内存10M
* -XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class MethodAreaOOM { static class OOMObject {
} public static void main(String[] args) {
while (true) {
//使用了CFLIB这类字节码技术,有兴趣可以另外了解一下
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
enhancer.create();
}
}
}

运行结果:

书中介绍会抛出异常
caused by: java.OutOfMemoryError: PermGen space
.....
.....
我自己执行时出现内存使用率迅速增长,
我顾及电脑会再次卡死就手动关闭了程序

4.  本机直接内存溢出

  使用Unsage类的allocateMemory()方法来真正申请分配内存。

/**
* 设置最大堆内存20M 最大直接内存10M
* -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = *; public static void main(String[] args) throws Exception{
Field field = Unsafe.class.getDeclaredFields()[];
field.setAccessible(true);
Unsafe unsafe = (Unsafe)field.get(null);
while (true) {
unsafe.allocateMemory(_1MB);//分配内存
}
//执行后 360提示电脑虚拟内存不足
}
}

运行结果:

Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at jvm.DirectMemoryOOM.main(DirectMemoryOOM.java:)

本章小结

  通过这一章的学习,我们明白了虚拟机中的内存是怎么划分的,哪部分区域、什么样的代码和操作可能导致内存溢出异常。虽然Java有垃圾收集机制,但内存溢出异常离我们仍然并不遥远,

  本章只讲解了各个区域出现异常的原因,下一章会详细讲解Java垃圾收集机制的底层实现。

  

【JVM.1】java内存区域与内存溢出的更多相关文章

  1. JVM内存区域与内存溢出异常

    Java虚拟机在执行java程序时会把它所管理的内存会分为若干个不同的数据区域,不同的区域在内存不足时会抛出不同的异常. >>运行时数据区域的划分 (1)程序计数器程序计数器(Progra ...

  2. JVM基础知识(1)-JVM内存区域与内存溢出

    JVM基础知识(1)-JVM内存区域与内存溢出 0. 目录 什么是JVM 运行时数据区域 HotSpot虚拟机对象探秘 OutOfMemoryError异常 1. 什么是JVM 1.1. 什么是JVM ...

  3. 深入理解jvm之内存区域与内存溢出

    文章目录 1. Java内存区域与内存溢出异常 1.1. 运行时数据区域 1.1.1. 程序计数器 1.1.2. java虚拟机栈 1.1.3. 本地方法栈 1.1.4. Java堆(Java Hea ...

  4. 深入理解java虚拟机系列(一):java内存区域与内存溢出异常

    文章主要是阅读<深入理解java虚拟机:JVM高级特性与最佳实践>第二章:Java内存区域与内存溢出异常 的一些笔记以及概括. 好了開始.假设有什么错误或者遗漏,欢迎指出. 一.概述 先上 ...

  5. 深入理解java虚拟机---->java内存区域与内存溢出异常

    2. java内存区域于内存溢出异常 2.1 概述: 对于C/C++而言,内存管理具有最高的权利,既拥有每一个对象的“所有权”,又担负着每一个对象生命开始到结束的维护责任. 对于java而言,则把内存 ...

  6. 第二章Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 一.概述 对与Java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为每个new操作去写delete/free代码,不容易出现内存泄露和内存溢出问 题, ...

  7. 《深入理解java虚拟机》第二章 Java内存区域与内存溢出异常

    第二章 Java内存区域与内存溢出异常 2.2 运行时数据区域  

  8. 深入了解Java虚拟机(1)java内存区域与内存溢出异常

    java内存区域与内存溢出异常 一.运行时数据区域 1.程序计数器:线程私有,用于存储当前所执行的指令位置 2.Java虚拟机栈:线程私有,描叙Java方法执行模型:执行方法时都会创建一个栈帧,存储局 ...

  9. 2.1 自动内存管理机制--Java内存区域与内存溢出异常

    自动内存管理机制 第二章.Java内存区域与内存溢出异常 [虚拟机中内存如何划分,以及哪部分区域.什么样代码和操作会导致内存溢出.各区域内存溢出的原因] 一.运行时数据区域 Java虚拟机所管理的内存 ...

  10. 虚拟机--第二章java内存区域与内存溢出异常--(抄书)

    这是本人阅读周志明老师的<深入理解Java虚拟机>第二版抄写的,有很多省略,不适合直接阅读,需要阅读请出门左转淘宝,右转京东,支持周老师(侵权请联系删除) 第二章java内存区域与内存溢出 ...

随机推荐

  1. SpringCloud+Feign环境下文件上传与form-data同时存在的解决办法(2)

    书接上文. 上文中描述了如何在 SpringCloud+Feign环境下上传文件与form-data同时存在的解决办法,实践证明基本可行,但却会引入其他问题. 主要导致的后果是: 1. 无法与普通Fe ...

  2. [20170825]11G备库启用DRCP连接3.txt

    [20170825]11G备库启用DRCP连接3.txt --//昨天测试了11G备库启用DRCP连接,要设置alter system set audit_trail=none scope=spfil ...

  3. 洗礼灵魂,修炼python(41)--巩固篇—从游戏《绝地求生-大逃杀》中回顾面向对象编程

    声明:本篇文章仅仅以游戏<绝地求生>作为一个参考话题来介绍面向对象编程,只是作为学术引用,其制作的非常简易的程序也不会作为商业用途,与蓝洞公司无关. <绝地求生>最近很火,笼络 ...

  4. Django框架的简介

    Django框架的背景 Django是一款基于Python开发的全栈式一体化Web 应用框架.2003 年问世之初,它只是 美国一家报社的内部工具,2005 年 7 月使用 BSD 许可证完成了开源. ...

  5. win10 文件扩展名的更改

    win10 文件扩展名的改 随便打开一个文件夹,最好是"此电脑",  第二行是 "     文件  -   计算机  -  查看   " 在查看里面就可以更改了 ...

  6. django生命周期和事件委派

    这是事件委派如果不用事件委派   直接绑定的话,新添加的按钮不会有删除或者编辑的功能 上面是事件委派的代码 新添加的编辑按钮可以弹出123 django生命周期: 这是Django的生命周期 首先会通 ...

  7. linux命令之 df file fsck fuser

    有非常多人说,网上非常多知识点都有了.为什么你还要在自己的博客中反复这些东西呢? 我想说的是.别人写的东西是别人理解的东西,同一时候也是别人学习过程的总结,对于自己来说.自己写自己的博客最基本的目的就 ...

  8. 2017-2018-2 20155314《网络对抗技术》Exp9 Web安全基础

    2017-2018-2 20155314<网络对抗技术>Exp9 Web安全基础 目录 实验目标 实验内容 实验环境 基础问题回答 预备知识 实验步骤--WebGoat实践 0x10 We ...

  9. WebService(基于AXIS的WebService编程)

    一.服务端代码 1.创建Maven工程 注意pom.xml文件的配置,需要引入axis的相关包 <project xmlns="http://maven.apache.org/POM/ ...

  10. 转载 锁机制与原子操作 <第四篇>

    一.线程同步中的一些概念 1.1临界区(共享区)的概念 在多线程的环境中,可能需要共同使用一些公共资源,这些资源可能是变量,方法逻辑段等等,这些被多个线程共用的区域统称为临界区(共享区),临界区的资源 ...