(一)jvm内存分布与内存溢出异常

第一部分:运行时内存的划分
第二部分:堆上对象的创建,对象的内存布局,对象的访问定位
第三部分:OutOfMemoryError异常,堆溢出,栈和本地方法区溢出,方法区和运行时常量池溢出,本地直接内存溢出 

第一部分:

一、主要讲解内存划分,用途,创建及销毁时间,有些区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

(一)jvm内存分布与内存溢出异常_第1张图片

运行时内存的划分

堆(共享)

方法区 ●运行时常量池(共享)

本地方法栈

程序计数器

(-直接内存,并非虚拟机运行时数据区域一部分,也不是java虚拟机规定中定义的内存区域)

1、栈

线程私有的,生命周期与线程相同栈描述的是java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧,用来存储局部变量表、操作数栈,动态链接、方法出口等信息。每个方法的开始结束都对应一个栈帧的入栈出栈过程。
       局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是代表对象的句柄或相关位置)和 returnAddress 类型(指向了一条字节码指令的地址)

-Xss
StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

2、堆

虚拟机启动时创建,线程共享。对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。主要是存放对象实例和数组。Java堆是垃圾收集器管理的主要区域。java堆可以处于物理上不连续的空间,但是逻辑上要连续。
       从内存回收的角度,由于现在收集器基本采用分代收集算法,java堆细分:新生代,老年代,再细致点有Eden空间,From Survivor空间、To Survivor空间。
       从内存分配的角度,线程共享的java堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。主要用于解决高并发情况下创建对象的线程安全问题。

-Xmx最大内存    -Xms最小内存
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。

3、方法区

和堆一样线程共享。存储已被jvm加载的class信息 (类名、访问修饰符、常量池、字段描述、方法描述等)、常量、静态变量、即时编译器编译后的代码等数据。在HotSpot,方法区又被成为“永久代(PermGen)”,本质上不等价,仅仅是因为GC分代收集扩张至方法区,或者说使用永久代来实现方法区而已,这样垃圾收集器就可以像管理java堆一样管理方法区。
       和java堆样,内存在物理上可以不连续,可以选择固定大小或者可拓展,还可以不实现垃圾收集。但是这个区域还是有内存回收的,主要是针对常量池的回收和对类型的卸载。

-XX:MaxPermSize
OutOfMemoryError:当方法区无法满足内存分配需求时

运行时常量池:是方法区的一部分。class文件中除了有类的版本、字段、方法、接口等描述以外。还有一项是常量池,用于存放编译器生成的各种字面和符号引用,这部分内容将在 类加载后进入方法区的  运行时常量池中 存放。运行常量池相对于class文件常量池的另外一个特征是具备动态性,java并不要求常量一定只有编译期才能产生,也就是并非预置入class文件中常量池的内容才能进入将方法区运行常量池,运行期间也可能将新的常量放入池中,比如String类的inter()方法。

OutOfMemoryError:受限于方法区内存,当常量池无法再申请内存时会抛出。

JDK8永久代替换为元数据区

        随着JDK8的到来,JVM不再有PermGen。但类的元数据信息(metadata)还在,只不过不再是存储在连续的堆空间上,而是移动到叫做“Metaspace”的本地内存(Native memory)中。
        类的元数据信息转移到Metaspace的原因是PermGen很难调整。PermGen中类的元数据信息在每次FullGC的时候可能会被收集,但成绩很难令人满意。而且应该为PermGen分配多大的空间很难确定,因为PermSize的大小依赖于很多因素,比如JVM加载的class的总数,常量池的大小,方法的大小等。
        此外,在HotSpot中的每个垃圾收集器需要专门的代码来处理存储在PermGen中的类的元数据信息。从PermGen分离类的元数据信息到Metaspace,由于Metaspace的分配具有和Java Heap相同的地址空间,因此Metaspace和Java Heap可以无缝的管理,而且简化了FullGC的过程,以至将来可以并行的对元数据信息进行垃圾收集,而没有GC暂停。

4、本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务

StackOverflowError
       OutOfMemoryError

5、程序计数器

线程私有的;

当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
       Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的。每条线程都需要一个独立的程序计数器,各条线程之间计数器互补影响,独立存储,我们称这类内存区域为“线程私有”内存。如果线程在正指向java方法,这个计数器记录都是正在执行的虚拟机字节码指令的地址;如果是native方法,计数器值为空。
       此内存是唯一一个java虚拟机规范中没有规定任何OutOfMemoryErroe的区域

6、直接内存并不是虚拟机运行时数据区域的部分,也不是java虚拟机规范中定义的内存区域。jdk1.4后新加入NIO(New Input/Out)类,引入另一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能提高性能,避免在java堆和Native堆中来回复制数据

-Xmx
OutOfMemoryError 忽略其大小,会使各个内存区域总和大于物理内存限制,然后出现内存溢出

 

第二部分:

本部分主要讲,HotSpot虚拟机在java堆中对象分配、布局和访问的全过程

1、对象的创建

①创建
       虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有则必须先执行相应类的加载过程(后续会讲)。
       类检查通过后,为新对象分配内存,内存大小在类加载完就完全确定了。根据java堆的规整分为“指针碰撞”和“空闲列表”。
       指针碰撞,所有用过的内存放一边,没用过的放另一边,中间放着一个指针作为分加点的指示器,那么分配内存仅仅是将指示器向空间空间挪动一段与对象大小一样的距离。
       空闲列表,列表记录了哪些内存块可用,在分配的时候从列表中找到一块足够大空间划分给对象,并更新列表的记录。
       在使用Serial、ParNew等带Compact过程的收集器时,通常采用的分配算法是指针碰撞
       在使用CMS这种基于Mark-Sweep算法的收集器时,采用空闲列表。
       另外对象的创建在虚拟机中是非常频繁的,即使是修改一个指针所指的位置,在并发的情况下也并不是线程安全,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存情况。解决办法两种,、对分配内存空间的动作进行同步处理——实际上采用CAS配上失败重试的方法保证更新操作的原子性。、TLAB:如果使用CAS其实对性能还是会有影响的,所以JVM又提出了一种更高级的优化策略:每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB),线程内部需要分配内存时直接在TLAB上分配就行,避免了线程冲突。只有当缓冲区的内存用光需要重新分配内存的时候才会进行CAS操作分配更大的内存空间。虚拟机是否使用TLAB,可以通过-XX:+/-UserTLAB参数设定。
       ②初始化
        内存分配完后,jvm将分配到的内存空间初始化为零值(不包括对象头),如果是TLAB可以提前至TLAB分配时初始化。可以保证对象的实例字段在java代码中可以不赋值直接使用。
       ③对象信息设置(对象头信息设置)
        虚拟机对对象进行必要的设置,这个对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存在对象的对象头(Object Header)里。

2、对象的内存布局

对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)
①对象头:
第一部分:用于存储自身运行时的数据,如哈希码、GC分代年龄、锁状态标志,线程持有的锁、偏向线程ID、偏向时
                                  间戳等。
                第二部分:是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实
                                  例。并不是所有的虚拟机在类对象上保留类指针,查找对象的元数据并不一定经过对象本身(见下面讲
                                  解)。如果对象是数组,对象头还要记录数组长度(数组无法通过元数据中确定数组大小)
②实例数据:对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容
③对齐填充:
不是必然存在的,没有特别的含义,起占位符的作用,HotSpot vm自动内存管理系统要求对象的起始值都是8字节的
                    整数倍,就是对象的大小必须是8字节的整数倍,对象头是8字节的倍数,但是实例数据部分没有对齐时就通过
                    对齐填充来对齐

3、对象的访问定位

建立对象是为了使用对用,java程序通过栈上的reference数据来操作堆上的具体对象。reference类型在jvm规范中规定了一个指向对象的引用。对象的访问方式取决于虚拟机实现而定,主流的有使用句柄和直接指针两种

①使用句柄:java堆中划分出一块内存作为句柄池,栈上的reference中存储的就是对象的句柄地址,句柄中包含了对象的实例数据和对象的类型数据各自的地址信息

指针访问:java堆对象就要考虑如何访问对象类型数据,在对象实例数据中直接存储对象类型数据的指针。而reference中存储的是对象地址

使用句柄的优势是存储的是稳定的句柄地址,在对象被移动(垃圾收集时经常移动),只会改变句柄中的实例数据指针。
使用指针的优势是访问速度快,它节省了一次指针定位的开销(对象访问是很频繁的)。sun HotSpot,用的就是指针访问

 

第三部分

①代码验证各个运行时区域存储的内容②各个内存区域(除程序计数器)的内存溢出,定位、原因,如何处理。

一、java堆溢出(Java heap space)

java堆用于存储对象实例,只要不断创建对象,并保证GC Roots到对象之间有可达路径来避免清楚这些对象,达到最大堆容量限制后就会出现内存溢出异常。-Xms  -Xmx

/**
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeadpOOM {
    static class OOMBject {
    }

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

(一)jvm内存分布与内存溢出异常_第2张图片

异常堆栈信息“java.lang.OutOfMemoryError”进一步提示“ Java heap space“

解决方法①通过内存映像分析工具对dump出来的堆转存储快照进行分析,先分清是泄露还是溢出。如果是内存泄露可进一步通过工具查看泄露对象到gc roots的引用连。

二、栈和本地方法栈溢出

虚拟机并不区分栈和本地方法栈。栈容量只由-Xss参数设定。

VM Args: -Xss2M
       StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
       OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存
       其实本质上是同一种描述,到底是是栈空间太大,还是内存太小

单线程下,无论是栈帧太大还是虚拟机栈容量太小,都派出StackOverError异常。
        操作系统给每个进程分配的内存时有限制的。虚拟机提供了参数来控制java堆和方法区的这两部分内存的最大值。操作系统限制的内存 - Xmx(最大堆容量) - MaxPermSize(最大方法区容量),程序计数器内存很小忽略,如果虚拟机进程本身耗费的内存不计算在内,剩下的内存就由栈和本地方法栈瓜分。每个栈分配的栈容量越大,建立的线程数就越少。
        栈内存溢出,在不能减少线程的数量的情况下,可以减少最大堆容量和减少栈容量来换取更多线程。

三、方法区和运行时常量池溢出(PermGen space )

运行时常量池内存溢出

String.intern()是一个Native方法,它的作用是:如果字符串常量池已经包含一个等于此String对象的字符串,则返回代表池中这个字符串的String对象,否则,将此String对象包含的字符串添加到常量池中,并返回此String对象在常量池的引用。

/*
 * VM Args: -XX:PermSize=10m -XX:MaxPermSize=10m
 */
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        // 使用List保持着常量池引用,避免Full GC回收常量池行为
        List list = new ArrayList();
        
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}

上述方法在jdk1.6及以前版本报错“PermGen space”。在jdk1.7之后,while就会一直运行下去,直到内存用完。

public class Hello {
    public static void main(String[] args) {
        String str1 = new StringBuilder("计算机").append("软件").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}

这段代码在JDK1.6中运行,会得到两个false,而在JDK1.7中运行,会得到一个true和一个false。产生差异的原因是:在JDK1.6中,intern()方法会把首次遇到的字符串复制到永久代中,返回的也是永久代中这个字符串的引用,而由StringBuilder创建的字符串实例在Java堆中,所以必然不是同一个引用,将返回false。而JDK1.7(以及部分其他虚拟机,例如JRockit)的intern()实现不会再复制实例,而是在常量池中记录首次出现的实例引用,因此intern()返回的引用和由StringBuilder创建的那个字符串是同一个。对str2比较返回false是因为"java"字符串在执行StringBuilder()之前就已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”原则,而“计算机软件”这个字符串则是首次出现的,因此返回true

方法区内存溢出

基本思路是运行产生大量的类去填满方法区,直到溢出

四、本机直接内存溢出

DirectMemory容量可通过-XX:MaxDirectMemorySize指定,如果不指定,默认与java堆大小一样。

由DirectMemory导致溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常。

----

 

* A:栈(掌握)

    * 存储局部变量

    局部变量:定义在方法声明上和方法中的变量

* B:堆(掌握)

    * 存储new出来的数组或对象

* C:方法区

    * 方法的加载,class的加载

* D:本地方法区

    * 和系统相关

* E:寄存器

    * 给CPU使用

你可能感兴趣的:(JVM)