一、Java虚拟机(1)

一、Java虚拟机

1、Java内存区域

简单说下Javad内存区域划分,如图所示:

1.1、运行时数据区域(五大区域)

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

1.1.1、 程序计数器(Program Counter Register)

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完。另外,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

1.1.2、Java虚拟机栈(Java Virtual Machine Stacks)

与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期和线程相同,描述的是Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用直至执行完成的过程就对应着一个栈帧在虚拟机中的入栈到出栈的过程。Java内存可以粗糙的区分为堆内存(Heap)和栈内存(Stack),其中栈就是现在说的虚拟机栈,或者说是虚拟机栈中局部变量表部分,比如常用的int,char基础类型的变量,都是存储在该区域内。

局部变量表主要存放了编译器可知的各种数据类型、对象引用。

1.1.3、本地方法栈(Native Method Stack)

    和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行Java方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。

1.1.4、JAVA堆(Java Heap)

    Java虚拟机所管理的内存中最大的一块,Java堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆还可以细分为:新生代和老年代。新生代再细致一点有:Eden空间、From Survivor、To Survivor空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。堆区也是Java GC机制所管理的主要内存区域,如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

1.1.4.1、新生代

    是用来存放新生的对象。一般占据堆的 1/3空间。由于频繁创建对象,所以新生代会频繁触发

MinorGC 进行垃圾回收。新生代又分为 Eden 区、 ServivorFrom、 ServivorTo 三个区。

  • Eden 区

    Java新对象的出生地(如果新创建的对象占用内存很大,则直接分配到老年代)。当 Eden区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。

  • ServivorFrom

    上一次 GC 的幸存者,作为这一次 GC 的被扫描者。

  • ServivorTo

    保留了一次 MinorGC 过程中的幸存者。

MinorGC 的过程(复制->清空->互换)

    MinorGC 采用复制算法。

  1. eden、 servicorFrom 复制到 ServicorTo,年龄+1:首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(如果有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(如果 ServicorTo 不够位置了就放到老年区);
  2. 清空 eden、 servicorFrom:然后,清空 Eden 和 ServicorFrom 中的对象;
  3. ServicorTo 和 ServicorFrom 互换:最后, ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom区。

1.1.4.2、 老年代

    主要存放应用程序中生命周期长的内存对象。

    老年代的对象比较稳定,所以 MajorGC 不会频繁执行。在进行 MajorGC 前一般都先进行了一次 MinorGC,使得有新生代的对象晋身入老年代,导致空间不够用时才触发。当无法找到足够大的连续空间分配给新创建的较大对象时也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

    MajorGC 采用标记整理算法:首先扫描一次所有老年代,标记出存活的对象,然后回收没有标记的对象。 MajorGC 的耗时比较长,因为要扫描再回收。 MajorGC 会产生内存碎片,为了减少内存损耗,我们一般需要进行合并或者标记出来方便下次直接分配。当老年代也满了装不下的时候,就会抛出 OOM( Out of Memory)异常。

1.1.4.3、永久代

    指内存的永久保存区域,主要存放 Class 和 Meta(元数据)的信息,Class 在被加载的时候被

放入永久区域, 它和和存放实例的区域不同,GC 不会在主程序运行期对永久区域进行清理。所以这

也导致了永久代的区域会随着加载的 Class 的增多而胀满,最终抛出 OOM 异常。

    在 Java8 中, 永久代已经被移除,被一个称为“元数据区”(元空间)的区域所取代。元空间

的本质和永久代类似,元空间与永久代之间最大的区别在于: 元空间并不在虚拟机中,而是使用

本地内存。因此,默认情况下,元空间的大小仅受本地内存限制。 类的元数据放入 native

memory, 字符串池和类的静态变量放入 java 堆中, 这样可以加载多少类的元数据就不再由

MaxPermSize 控制, 而由系统的实际可用空间来控制。

1.1.5、方法区(Method Area)

    方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。

    HotSpot虚拟机中方法区也常被称为 “永久代”,本质上两者并不等价,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。仅仅是因为HotSpot虚拟机设计团队用永久代来实现方法区而已,这样HotSpot虚拟机的垃圾收集器就可以像管理Java堆一样管理这部分内存了。但是这并不是一个好主意,因为这样更容易遇到内存溢出问题。 相对而言,垃圾收集行为在这个区域是比较出现的,但并非数据进入方法区后就“永久存在”了。

    在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑。在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

1.1.6、运行时常量池(Runtime Constant Pool)

    运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池信息(用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译)。

    运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址,比如:“abc”.intern()==new String("abc")))。

1.1.7、直接内存(Direct Memory)

    直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK1.4中有一种基于通道(Channel)和缓冲区(Buffer)的I/O内存分配方式,将由C语言实现的native函数库分配在直接内存中,然后通过一个存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在Java堆和Native堆之间来回复制数据。由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。本机直接内存的分配不会收到Java堆的限制,但是,既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

1.2、 HotSpot虚拟机对象访问

    通过上面的介绍我们大概知道了虚拟机的内存情况,下面我们来详细的了解一下HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。

1.2.1、对象的创建

    java是面向对象的语言,因此对象的创建无时无刻都存在。虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、准备、解析和初始化过。如果没有,那必须先执行相应的类加载过程。

    在类加载检查通过后,接下来虚拟机将为新生对象分配内存当然是在java堆中分配。对象所需的内存大小在类加载过程中就已经确定了,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。分配方式有 “指针碰撞(Bump the Pointer)” 和 “空闲列表(Free List)” 两种,选择那种分配方式由Java堆是否规整决定。

    指针碰撞:如果java堆是规整的,即所有用过的内存放在一边,没有用过的内存放在另外一边,并且有一个指针指向分界点,在需要为新生对象分配内存的时候,只需要移动指针画出一块内存分配和新生对象即可。

    空闲列表:当java堆不是规整的,意思就是使用的内存和空闲内存交错在一起,这时候需要一张列表来记录哪些内存可使用,在需要为新生对象分配内存的时候,在这个列表中寻找一块大小合适的内存分配给它即可。而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

    在为新生对象分配内存的时候,同时还需要考虑线程安全问题。因为在并发的情况下内存分配并不是线程安全的。有两种方案解决这个线程安全问题:

  1. 为分配内存空间的动作进行同步处理;
  2. 为每个线程预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer, TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配。

    虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。内存分配后,虚拟机需要将每个对象分配到的内存初始化为0值(不包括对象头),这也就是为什么实例字段可以不用初始化,直接为0的原因。接下来,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的GC分代年龄等信息。这些信息存放在对象头中,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会与不同的设置方式。 new指令执行完后,所有的字段都为0,再按照程序员的意愿执行init方法后一个真正可用的对象才诞生。

1.2.2、对象的内存布局

    在Hotspot虚拟机中,对象在内存中的布局可以分为3快区域:对象头、实例数据、对齐填充。

对象头(Header)包括2部分信息:

1:存储对象自身的运行时数据(哈希吗、GC分代年龄、锁状态标志等等);

2:类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。

实例数据(Instance Data):这部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。

对齐填充(Padding):这部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

1.2.3、对象的访问定位

    建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种:

1、使用句柄方式:会在java堆中创建一个句柄池,reference指向的这块句柄池,句柄池中包括两个指针,其中一个指针指向对象实例数据,另外一个指针指向对象的类型数据。

2、 直接指针访问,那么Java堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference中存储的直接就是对象的地址。

    这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改。使用指针的方式优势则是速度快,并且省去了一次指针定位的开销。

1.3、 Java内存模型

    在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。那么Java内存模型规定了哪些东西呢,它定义了程序中变量的访问规则,往大一点说是定义了程序执行的次序。注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。

  Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

  举个简单的例子:在java中,执行下面这个语句:

i  = 10;

执行线程必须先在自己的工作线程中对变量i所在的缓存行进行赋值操作,然后再写入主存当中。而不是直接将数值10写入主存当中。

  那么Java语言 本身对 原子性、可见性以及有序性提供了哪些保证呢?

1、原子性

  在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

  上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子i:

  请分析以下哪些操作是原子性操作:

x = 10;         //语句1 y = x;         //语句2 x++;           //语句3 x = x + 1;     //语句4

咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。

  语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。

  语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。

  同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。

   所以上面4个语句只有语句1的操作具备原子性。

  也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。

  不过这里有一点需要注意:在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。但是好像在最新的JDK中,JVM已经保证对64位数据的读取和赋值也是原子性操作了。

  从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2、可见性

  对于可见性,Java提供了volatile关键字来保证可见性。

  当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3、有序性

  在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

  在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

  另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  下面就来具体介绍下happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

这8条原则摘自《深入理解Java虚拟机》。这8条规则中,前4条规则是比较重要的,后4条规则都是显而易见的。

  下面我们来解释一下前4条规则:

  对于程序次序规则来说,我的理解就是一段程序代码的执行在单个线程中看起来是有序的。注意,虽然这条规则中提到“书写在前面的操作先行发生于书写在后面的操作”,这个应该是程序看起来执行的顺序是按照代码顺序执行的,因为虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

  第二条规则也比较容易理解,也就是说无论在单线程中还是多线程中,同一个锁如果出于被锁定的状态,那么必须先对锁进行了释放操作,后面才能继续进行lock操作。

  第三条规则是一条比较重要的规则,也是后文将要重点讲述的内容。直观地解释就是,如果一个线程先去写一个变量,然后一个线程去进行读取,那么写入操作肯定会先行发生于读操作。

  第四条规则实际上就是体现happens-before原则具备传递性。

1.4、内存溢出或泄露

内存溢出的方式,大致有以下几种:

  1. 栈溢出(StackOverflowError)
  2. 堆溢出(OutOfMemoryError:Java heap space)
  3. 永久代溢出(OutOfMemoryError: PermGen space)
  4. 直接内存溢出

1、栈溢出

    -Xoss参数设置本地方法栈大小 -Xss 参数设置栈容量。

    -Xoss参数是否有效,取决于jvm采用了哪种虚拟机,譬如如果采用HotSpot虚拟机,-Xoss参数(无效),这样虚拟机栈和本地方法栈通过栈容量控制。附:当前大部分的虚拟机栈都是可动态扩展的。

关于虚拟机栈和本地方法栈,在java虚拟机规范中描述了两种异常: 

  • 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  •  虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

    在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。 如果不限於单线程,在这种情况下,为每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。

    如果建立过多线程导致内存溢出,在不能减少线程数或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

1)、StackOverflowError实例

/** * 循环调用对象引用的方式实现栈溢出 **/ public class StackSOFTest {     int depth = 0;     public void sofMethod(){         depth ++ ;         sofMethod();     }     public static void main(String[] args) {         StackSOFTest test = null;         try {             test = new StackSOFTest();             test.sofMethod();         } finally {             System.out.println("递归次数:"+test.depth);         }     } } 执行结果: 递归次数:982 Exception in thread "main" java.lang.StackOverflowError     at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:8)     at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9)     at com.ghs.test.StackSOFTest.sofMethod(StackSOFTest.java:9) ……后续堆栈信息省略

2)、栈空间不足——OutOfMemberError实例 

    单线程情况下,不论是栈帧太大还是虚拟机栈容量太小,都会抛出StackOverflowError,导致单线程情境下模拟栈内存溢出不是很容易,循环调用new A()实现可以产生内存溢出异常。

如何让虚拟机栈快速内存溢出呢?比如ArrayList,当扩容量(newCapacity)大于ArrayList数组定义的最大值后会调用hugeCapacity来进行判断。如果minCapacity已经大于Integer的最大值(溢出为负数)那么抛出OutOfMemoryError(内存溢出)否则的话根据与MAX_ARRAY_SIZE的比较情况确定是返回Integer最大值还是MAX_ARRAY_SIZE。这边也可以看到ArrayList允许的最大容量就是Integer的最大值(-2的31次方~2的31次方减1)。

2、堆内存溢出

java堆用于存储对象实例,只要不断地创建对象,并且保证gc roots到对象之间有可达路径来避免垃圾回收机制来清楚这些对象,那么在 对象到达最大堆的容量限制后就会产生内存溢出溢出。

异常:java.lang.OutOfMemoryError: java heap space

要解决这个区域的异常,首先要区分是出现了内存泄露(Memory Leak)还是内存溢出(Memory OverFlow)。 解决方式:如果是内存泄露,通过工具(eclipse memory analyzer)查看泄露对象到gc roots的引用链。于是就能找到泄露对象是通过怎样的路径与gc roots相关联 并导致垃圾回收器无法自动回收它们的。掌握了泄露对象的类型信息及gc roots引用链的信息,就可以准确的找出泄露代码的位置。 如果不存在泄露,换句话说,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms)与机器物理内存是否还可以调大,从代码上检查 是否存在某些对象生命周期过长,持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

/** * 堆溢出 VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */ public static void main(String[] args) { List<byte[]> list = new ArrayList<>(); int i=0; while(true){ list.add(new byte[5*1024*1024]); System.out.println("分配次数:"+(++i)); } }

3、永久代溢出(OutOfMemoryError: PermGen space)

运行时常量池是方法区的一部分。 从JDK1.7开始逐步“去永久代”,我们这里讨论1.6版本,在1.6版本中,由于常量池分配在永久代内,我们可以 通过-XX:PermSeize和-XX:MaxPermSeize限制方法区大小,从而间接限制其中常量池的容量。

异常:java.lang.OutOfMemoryError: PermGen space

方法区用于存放Class的相关信息,如类名,访问修饰符,常量池,字段描述,方法描述等。 方法区异常是一种常见的内存溢出异常,一个类要被垃圾收集器回收掉,判定条件是比较苛刻的。在经常动态生成大量Class的应用中,需要特别注意类的回收情况。

永久代溢出可以分为两种情况,第一种是常量池溢出,第二种是方法区溢出。

1)、永久代溢出——常量池溢出 

要模拟常量池溢出,可以使用String对象的intern()方法。如果常量池包含一个此String对象的字符串,就返回代表这个字符串的String对象,否则将String对象包含的字符串添加到常量池中。

public class ConstantPoolOOMTest { /** * VM Args:-XX:PermSize=10m -XX:MaxPermSize=10m * @param args */ public static void main(String[] args) { List<String> list = new ArrayList<>(); int i=1; try { while(true){ list.add(UUID.randomUUID().toString().intern()); i++; } } finally { System.out.println("运行次数:"+i); } } }

因为在JDK1.7中,当常量池中没有该字符串时,JDK7的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用。 

简单来说,就是对象实际存储在堆上面,所以,让上面的代码一直执行下去,最终会产生堆内存溢出。 下面我将堆内存设置为:-Xms5m -Xmx5m,执行上面的代码,运行结果如下:

运行次数:58162 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.lang.Long.toUnsignedString(Unknown Source) at java.lang.Long.toHexString(Unknown Source) at java.util.UUID.digits(Unknown Source) at java.util.UUID.toString(Unknown Source) at com.ghs.test.ConstantPoolOOMTest.main(ConstantPoolOOMTest.java:18)

2)、永久代溢出——方法区溢出 

方法区存放Class的相关信息,下面借助CGLib直接操作字节码,生成大量的动态类。

public class MethodAreaOOMTest { public static void main(String[] args) { int i=0; try { while(true){ Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); i++; } } finally{ System.out.println("运行次数:"+i); } } static class OOMObject{ } } 运行结果: 运行次数:56 Exception in thread "main" Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

4、直接内存溢出

异常:java.lang.OutOfMemoryError

DirectMemory容量可通过-XX:MaxDirectMemorySize,如果不指定,默认与java堆最大值(-Xmx指定)一样。 由DirectMemory导致的内存溢出,一个明显的特征是在Heap Dump文件中不会看见明显的异常,如果发现OOM之后Dump文件很小,而程序中又直接或间接使用了NIO,那就可以 考虑检查一下是不是这方面的原因。

NIO会使用到直接内存,你可以通过NIO来模拟,在下面的例子中,跳过NIO,直接使用UnSafe来分配直接内存。

public class DirectMemoryOOMTest { /** * VM Args:-Xms20m -Xmx20m -XX:MaxDirectMemorySize=10m * @param args */ public static void main(String[] args) { int i=0; try { Field field = Unsafe.class.getDeclaredFields()[0]; field.setAccessible(true); Unsafe unsafe = (Unsafe) field.get(null); while(true){ unsafe.allocateMemory(1024*1024); i++; } } catch (Exception e) { e.printStackTrace(); }finally { System.out.println("分配次数:"+i); } } } 运行结果: Exception in thread "main" java.lang.OutOfMemoryError at sun.misc.Unsafe.allocateMemory(Native Method) at com.ghs.test.DirectMemoryOOMTest.main(DirectMemoryOOMTest.java:20) 分配次数:27953

总结: 

  • 栈内存溢出:程序所要求的栈深度过大。 
  • 堆内存溢出: 分清内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用,不足则通过调大-Xms,-Xmx参数。 
  • 永久代溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象。 
  • 直接内存溢出:系统哪些地方会使用直接内存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章