Java内存区域

JVM中内存区域的划分为下图几个模块:

javacounter javacounter写在哪_java

1,程序计数器(Program Counter Register):程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。

       字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。

  每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。

  如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。

2,虚拟机栈(JVM Stack):一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。

局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。

需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。

虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。

每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。

3,本地方法栈(Native Method Statck):本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。

本地方法栈也是线程私有的。

4,堆区(Heap):堆区是理解Java GC机制最重要的区域。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。

  一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。

1 public class HeapOomTest {
 2 
 3     public static void main(String[] args) {
 4         List<byte[]> list = new ArrayList<byte[]>();
 5         while (true) {
 6             try {
 7                 list.add(new byte[1 * 1024 * 1024]);// 每次增加一个1M大小的数组对象
 8             } catch (Throwable e) {
 9                 e.printStackTrace();
10                 System.out.println("END...");
11                 System.exit(0);
12             }
13         }
14     }
15 }

运行结果:

javacounter javacounter写在哪_JVM_02

 

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

  方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。

  在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。

符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。

6,直接内存(Direct Memory):直接内存并不是JVM管理的内存,可以这样理解,直接内存,就是JVM以外的机器内存,比如,你有4G的内存,JVM占用了1G,则其余的3G就是直接内存,JDK中有一种基于通道(Channel)和缓冲区(Buffer)的内存分配方式,将由C语言实现的native函数库分配在直接内存中,用存储在JVM堆中的DirectByteBuffer来引用。由于直接内存收到本机器内存的限制,所以也可能出现OutOfMemoryError的异常。

Java对象的访问方式

一般来说,一个Java的引用访问涉及到3个内存区域:JVM栈,堆,方法区。

  以最简单的本地变量引用:Object obj = new Object()为例:

  • Object obj表示一个本地引用,存储在JVM栈的本地变量表中,表示一个reference类型数据;
  • new Object()作为实例对象数据存储在堆中;
  • 堆中还记录了Object类的类型信息(接口、方法、field、对象类型等)的地址,这些地址所执行的数据存储在方法区中;

在Java虚拟机规范中,对于通过reference类型引用访问具体对象的方式并未做规定,目前主流的实现方式主要有两种:

1,通过句柄访问(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》):

javacounter javacounter写在哪_java_03

通过句柄访问的实现方式中,JVM堆中会专门有一块区域用来作为句柄池,存储相关句柄所执行的实例数据地址(包括在堆中地址和在方法区中的地址)。这种实现方法由于用句柄表示地址,因此十分稳定。

2,通过直接指针访问:(图来自于《深入理解Java虚拟机:JVM高级特效与最佳实现》)

javacounter javacounter写在哪_JVM_04

通过直接指针访问的方式中,reference中存储的就是对象在堆中的实际地址,在堆中存储的对象信息中包含了在方法区中的相应类型数据。这种方法最大的优势是速度快,在HotSpot虚拟机中用的就是这种方式。

Java内存分配机制

        这里所说的内存分配,主要指的是在堆上的分配,一般的,对象的内存分配都是在堆上进行

分代分配,分代回收。对象将根据存活的时间被分为:年轻代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法区)。

    

javacounter javacounter写在哪_java_05

年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。注意,Minor GC并不代表年轻代内存不足,它事实上只表示在Eden区上的GC。

  年轻代上的内存分配是这样的,年轻代可以分为3个区域:Eden区和两个存活区(Survivor 0 、Survivor 1)

    

javacounter javacounter写在哪_JVM_06

  1. 绝大多数刚创建的对象会被分配在Eden区,其中的大多数对象很快就会消亡。Eden区是连续的内存空间,因此在其上分配内存极快;
  2. 最初一次,当Eden区满的时候,执行Minor GC,将消亡的对象清理掉,并将剩余的对象复制到一个存活区Survivor0(此时,Survivor1是空白的,两个Survivor总有一个是空白的);
  3.  下次Eden区满了,再执行一次Minor GC,将消亡的对象清理掉,将存活的对象复制到Survivor1中,然后清空Eden区;
  4.  将Survivor0中消亡的对象清理掉,将其中可以晋级的对象晋级到Old区,将存活的对象也复制到Survivor1区,然后清空Survivor0区;
  5. 当两个存活区切换了几次(HotSpot虚拟机默认15次,用-XX:MaxTenuringThreshold控制,大于该值进入老年代,但这只是个最大值,并不代表一定是这个值)之后,仍然存活的对象(其实只有一小部分,比如,我们自己定义的对象),将被复制到老年代。

  从上面的过程可以看出,Eden区是连续的空间,且Survivor总有一个为空。经过一次GC和复制,一个Survivor中保存着当前还活着的对象,而Eden区和另一个Survivor区的内容都不再需要了,可以直接清空,到下一次GC时,两个Survivor的角色再互换。因此,这种方式分配内存和清理内存的效率都极高,这种垃圾回收的方式就是著名的“停止-复制(Stop-and-copy)”清理法(将Eden区和一个Survivor中仍然存活的对象拷贝到另一个Survivor中),这不代表着停止复制清理法很高效,其实,它也只在这种情况下高效,如果在老年代采用停止复制,则挺悲剧的。

  在Eden区,HotSpot虚拟机使用了两种技术来加快内存分配。分别是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),这两种技术的做法分别是:由于Eden区是连续的,因此bump-the-pointer技术的核心就是跟踪最后创建的一个对象,在对象创建时,只需要检查最后一个对象后面是否有足够的内存即可,从而大大加快内存分配速度;而对于TLAB技术是对于多线程而言的,将Eden区分为若干段,每个线程使用独立的一段,避免相互影响。TLAB结合bump-the-pointer技术,将保证每个线程都使用Eden区的一段,并快速的分配内存。

  年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC。  

   可以使用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄。

  如果对象比较大(比如长字符串或大数组),Young空间不足,则大对象会直接分配到老年代上(大对象可能触发提前GC,应少用,更应避免使用短命的大对象)。用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。

  可能存在年老代对象引用新生代对象的情况,如果需要执行Young GC,则可能需要查询整个老年代以确定是否可以清理回收,这显然是低效的。解决的方法是,年老代中维护一个512 byte的块——”card table“,所有老年代对象引用新生代对象的记录都记录在这里。Young GC时,只要查这里即可,不用再去查全部老年代,因此性能大大提高。

永久代(Perm Generation)

JDK 1.8之前存在, JDK 1.8开始,HotSpot 已经没有 PermGen space 这个区间了,取而代之是一个叫做 Metaspace(元空间)

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

  除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
  -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

元空间也会受到内存空间的限制,所以也会产生OutOfMemoryError,
网上有用cglib来产生OutOfMemoryError的列子,如下

1 import java.lang.reflect.Method;
 2 
 3 import net.sf.cglib.proxy.Enhancer;
 4 import net.sf.cglib.proxy.MethodInterceptor;
 5 import net.sf.cglib.proxy.MethodProxy;
 6 
 7 public class TestMain {
 8 
 9     public static void main(String[] args) {
10         try {
11             while (true) {
12                 Enhancer enhancer = new Enhancer();
13                 enhancer.setSuperclass(OOMObject.class);  // 设置代理目标
14                 enhancer.setUseCache(false); //这个为false, 关闭CGLib缓存,否则总是生成同一个类,因此不会引发OutOfMemoryError
15                 enhancer.setCallback(new MethodInterceptor() {
16 
17                     @Override
18                     public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
19                         System.out.println("Before invoke " + method);
20                         return proxy.invokeSuper(obj, args);
21                     }
22                 });
23                 enhancer.create();
24             }
25         } finally {
26         }
27     }
28 }

 

1 public class OOMObject {
2     public long id;
3     public String name = "www";
4     public int type;
5 
6     public String getName() {
7         return name;
8     }
9 }

运行时配置的vm参数: -XX:PermSize=10M -XX:MaxPermSize=10M -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\oom.dump

HeapDump文件是一个二进制文件,它保存了某一时刻JVM堆中对象使用情况,它是Java虚拟机(JVM)在某一时刻所有对象的快照(其实扩展名应该为  .hprof,而不是  .dump)

输出结果:

1 java.lang.OutOfMemoryError: Metaspace
 2 Dumping heap to d:\oom.dump ...
 3 Heap dump file created [3543677 bytes in 0.015 secs]
 4 Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
 5     at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
 6     at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
 7     at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
 8     at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
 9     at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
10     at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
11     at bytecode.TestMain.main(TestMain.java:25)
12 Caused by: java.lang.reflect.InvocationTargetException
13     at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
14     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
15     at java.lang.reflect.Method.invoke(Method.java:498)
16     at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)
17     at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:336)
18     ... 6 more
19 Caused by: java.lang.OutOfMemoryError: Metaspace
20     at java.lang.ClassLoader.defineClass1(Native Method)
21     at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
22     ... 11 more
23 Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
24 Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M-XX:MetaspaceSize=10M; support was removed in 8.0

红色说明产生错误的地方

分析: 找到d:\oom.dump文件, 基于jvisualvm来打开dump文件,可以发现defineClass的时候发生的内存溢出,另外出现了大量的BaseFlyer子类的动态类,这个导致了Meta区域的溢出。

 

javacounter javacounter写在哪_JVM_07

 装入之后在界面右侧的概要、类等选项卡可以看到生成dump文件当时的堆信息:

 

javacounter javacounter写在哪_java_08

 

 点击 “类”图标,界面如下

javacounter javacounter写在哪_JVM_09

 

 双击第一项,进入查看,500个一组,进入某一组,可以看到很多以bytecode.OOMObject$$EnhancerByCGLIB开头的class

javacounter javacounter写在哪_javacounter_10