Java虚拟机(1)-----内存区域和内存溢出异常

一.运行时数据区域

       Java虚拟机将其管理的内存划分为不同的数据区域,他们各自有不同的用途,创建及销毁时间。

总体情况如下:

    1.1程序计数器

            每当我们创建一个线程时,便会相对应的使其拥有一个程序计数器。程序计数器的主要功能是作为当前线程的行号指示器。字节码指示器会因为程序计数器的值的不同而执行不同的代码。各个线程的程序计数器互不影响,独立储存。

            在实际运行中,如果当前线程执行的是java方法,程序计数器记录的是正在执行的虚拟机字节码指令的地址,如果当前线程执行的是本地Native方法,程序计数器的值为空。

            此区域是唯一没有规定OutOfMemoryError异常的区域!

    1.2虚拟机栈

            每当我们创建一个线程时,就会分配一片内存作为虚拟机栈。一个线程对应一个虚拟机栈。他们的生命周期相同。
            
            在实际运行过程中,当前线程每执行一个方法就会虚拟机栈放入一个栈帧,方法执行完毕就会释放此栈帧。栈帧中包含了此方法的 局部变量表,操作数栈,动态链表,方法出口等信息。局部变量表存放了基本数据类型(boolean,byte,long,char,integer,short,float,double),对象引用,returnAddress类型(指向了一条字节码的地址)。其中,long,double占用两个局部变量空间(Slot),其余数据类型只占用一个。
    
            我们平常所说的堆,栈中的栈就是指虚拟机栈,或者说虚拟机栈的局部变量表。

            此区域会抛出两种异常:

                1.StackOverflowError : 当对栈的访问深度超出栈本身的深度时抛出,即栈已空还在get。

                2.OutOfMemoryError :虚拟机栈的大小是支持扩展的,当虚拟机栈需要扩展而无法分配内存时抛出此异常。   

   1.3本地方法栈

            本地方法栈发挥的功能于虚拟机栈大致相同,只不过他们的区别时虚拟机栈执行的时Java方法,而本地方法栈执行的时本地Native方法。
                
            和虚拟机栈相同,本地方法栈也会抛出两种异常:
                

                1.StackOverflowError : 当对栈的访问深度超出栈本身的深度时抛出,即栈已空还在get。

                2.OutOfMemoryError :栈无法分配内存时抛出此异常。 

  1.4堆 

            堆是所有线程共享的用于存放所有对象实例(包含数组)的一片很大的内存区域。此区域在逻辑上处于连续的内存空间中,而可以不处于连续的物理空间中。

            堆可以稍微细分为 1新生代(Eden空间,From Survivor空间,To Survivor空间)    2老年代。其中,可能还会存在一些线程私有的分配缓冲区(TLAB-----Thread Local Allocation Buffer),分配缓冲去在下文中会仔细介绍。

            抛出OutOfMemorryError异常 :当堆中没有内存完成实例对象的分配,与此同时又没有内存供其扩展时抛出异常。

    1.5方法区

            方法区时所有线程共享的用于储存已被虚拟机加载的类信息,常量,静态变量等数据。方法区的一部分叫做运行时常量池,数据进入运行时常量池有两种途径:1.class文件中含有一部分信息叫常量池(存放编译器生成的字面量和符号引用),在类被加载后会放入运行时常量池。2非编译器也可以进入运行时常量池,比如String的intern()方法。

            在HotSpot虚拟机中,方法去更多的被叫做永久带,这是为了同堆一起进行垃圾收集。但是,这造成了很多的内存溢出问题。

            抛出OutOfMemorryError异常:无法满足内存分配需求时抛出。


二.以HotSpot虚拟机为例探索内存区域

    2.1普通对象的创建

        普通对象的创建过程分为5个步骤:

           2.1.1.进行类加载检查

           2.1.2分配内存

                    内存分配有两种方式    

                    1.指针碰撞:适用于堆内存绝对工整时。用过的内存放一边,没用过的内存放在另一边。需要分配内存时指针向未分配的区域移动即可。
                    2.空闲列表:适用于堆内存不工整,用过的内存和没用过的内存犬牙交错时。虚拟机会维护一个表用于记录那些区域可用,那些区域不可用。为对象分配内存时选择一块够大的区域即可。
                分配内存时会出现并发问题,A线程想在某一区域分配内存还未完成时,B线程也想使用此区域的内存。解决办法有两种。
                    1.线程分配内存时进行同步处理。
                    2.为每一个线程创建一块线程私有的线程分配缓冲区(TLAB),线程分配内存时在此区域分配即可,当TLAB区不够时,需要重新分配TLAB时,再进行同步处理。

          2.1.3分配的内存空间初始化为0值

                        这步是为了使Java字段不需要初始化便有默认值。如果使用了TLAB,可以在初始化TLAB时进行这一工作。

         2.1.4进行必要设置

                        这部分在下文中的内存布局中说到

          2.1.5按程序员意愿进行初始化

            

    2.2内存布局

            对象在其所分配的内存中的布局分为3部分

            2.2.1对象头

                        对象头中有两部分数据,一部分用于储存自身的运行是数据(HashCode,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等)。在32位和64位的虚拟机中其长度分别位32bit和64bit。另一部分则用于储存类型指针。如果是对象是数组,还会有一块区域储存数组的长度。

            2.2.2实例数据

                        这一部分储存真正的有效信息,即各种字段的内容。父类继承的内容和自己本身的内容都会在此区域。各个字段的在此区域顺序一般按照其自身的宽度排列。所以宽度相同的父类字段可能和子类字段挨在一起,这种情况下,父类字段出现在子类字段之前。 

            2.2.3对齐填充

                        此区域不是必然存在的,因为虚拟器要求对象的其起始地址必须是8的倍数,即对象的大小必须是8bit的整数倍,所以用此区域当作占位符的作用。

    2.3访问定位

            对象的访问定位方式大体有两种,句柄和直接指针

            2.3.1句柄访问

                    句柄访问会在堆中分配出一块内存作为句柄池,句柄池会储存每个对象两个地址,即实例数据指针(指向对象实例的数据)和类型对象指针(指向对象类型的数据)。
                    句柄访问的优点是虚拟机栈的引用指针稳定,对象移动后,引用不变,只改动实例数据指针。
        

           2.3.2直接指针访问

                        直接指针访问中,引用存放的是对象实例的地址,而在对象实例中会存放对象类型数据的地址,对象类型数据一般放在方法区。直接指针访问的有点是快,可以节省一次指针定位的时间开销。
                    
            

         

三.内存溢出实例

              

                以下代码需要堆虚拟机进行必要的设置,在Eclipse IDE中可以这样设置
                

            3.1堆溢出 

                        不断创建对象并且避免对象被回收便可以使堆抛出OutOfMemorryErroe异常。
/**
 * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @author zzm
 */
public class HeapOOM {

	static class OOMObject {
	}

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

		while (true) {
			list.add(new OOMObject());
		}
	}
}

            3.2虚拟机栈溢出/本地方法栈溢出

                    在单线程操作中,用递归将请求深度大于虚拟机允许的深度会抛出StackOverflowError异常
/**
 * VM Args:-Xss128k
 * @author zzm
 */
public class JavaVMStackSOF {

	private int stackLength = 1;

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

	public static void main(String[] args) throws Throwable {
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try {
			oom.stackLeak();
		} catch (Throwable e) {
			System.out.println("stack length:" + oom.stackLength);
			throw e;
		}
	}
}
                  不停创建线程并为其分配各自的虚拟机栈会抛出OutOfMemorryError异常, 别运行这段代码,会死机 !!!!
/**
 * VM Args:-Xss2M (这时候不妨设大些)
 * @author zzm
 */
public class JavaVMStackOOM {
 
       private void dontStop() {
              while (true) {
              }
       }
 
       public void stackLeakByThread() {
              while (true) {
                     Thread thread = new Thread(new Runnable() {
                            @Override
                            public void run() {
                                   dontStop();
                            }
                     });
                     thread.start();
              }
       }
 
       public static void main(String[] args) throws Throwable {
              JavaVMStackOOM oom = new JavaVMStackOOM();
              oom.stackLeakByThread();
       }
}

           3.3方法区(包含运行时常量池)

                    我们不停创建不同的String对象并调用intern()方法将其放入运行时常量池中,使方法区抛出异常。
/**
 * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
 * @author zzm
 */
public class RuntimeConstantPoolOOM {

	public static void main(String[] args) {
		// 使用List保持着常量池引用,避免Full GC回收常量池行为
		List<String> list = new ArrayList<String>();
		// 10MB的PermSize在integer范围内足够产生OOM了
		int i = 0; 
		while (true) {
			list.add(String.valueOf(i++).intern());
		}
	}
}
                也可以生成大量的动态类区填满方法区使其抛出异常。在当下的主流框架如Spring,Hibernate中会使用CGLib这类字节码技术对类进行增强,增强的类越多,就需要更大的方法区保证动态类生成的Class可以载入内存。
/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author zzm
 */
public class JavaMethodAreaOOM {

	public static void main(String[] args) {
		while (true) {
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {
				public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
					return proxy.invokeSuper(obj, args);
				}
			});
			enhancer.create();
		}
	}

	static class OOMObject {

	}
}

        最后,本片文章是我阅读《深入理解Java虚拟机-----JVM高级特性于最佳实践》总结而来,溢出异常的代码也是来自于书中的源码。初学java,出错实乃再平常不过之事,万望各位提出我的错误,在此谢过诸位了。





猜你喜欢

转载自blog.csdn.net/zh328271057/article/details/80504751