jvm笔记01:自动内存管理机制

本文深入介绍了Java虚拟机(JVM)的内存管理机制,包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区等关键概念。同时,文章详细分析了HotSpot虚拟机的对象创建、内存布局和访问定位过程,并提供了多种导致OutOfMemoryError异常的实战案例。


运行时数据区域

                java 虚拟机在执行java 程序的时候会把它管理的内存划分为若干个不同的数据区域,如下:



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


java虚拟机栈

          java虚拟机栈和程序计数器一样,也是线程私有的,它的生命周期与线程相同。  虚拟机栈描述的是java 的内存模型:每个方法在执行的同时,都会创建一个叫栈帧用户存储局部变量表,动态链接,方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。

           局部变量表存放了编译期间可知的个各种基本数据类型(boolean,byte,short,character,int,float,long,dubbo),局部变量表所需的内存空间在编译期间完成分配,当一个方法进入时候,这个方法在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

          java虚拟机规范中:如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverFlowError;如果虚拟机栈可以动态扩展,但在在扩展的时候无法申请到足够的内存空间,则会抛出OutOfMemoryError异常


本地方法栈

         本地方法栈和虚拟机栈发挥的作用是非常相似的,他们之间的区别不过是虚拟机栈执行的是java方法服务,本地方法栈执行的是虚拟机栈中使用到的native服务。

         与虚拟机栈一样,本地方法栈也会抛出StackOverFlowError和OutOfMemoryError异常。

java堆

          java堆是java虚拟机中所管理内存中最大的一块,java堆是被所有现存共享的,在虚拟机启动时候创建,此内存唯一的目的就是存放对象实例,几乎所有的对象实例都在这里分配。

          从内存分配的角度来看,线程共享的java堆可能划分出多个线程私有的分配缓冲区,不过无论如何划分,都与存放内容无关,无论哪个区域,存储的都依然是对象实例,进一步划分的目的是为了更好的回收内存。

            java堆可以是物理上不连续内存空间。目前主流的虚拟机都是按照可扩展内存实现的,如果在堆中没有内存完成实例的分配,并且堆也无法扩展时,将会抛出OutOfMemoryError异常。


方法区

           方法去和java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编辑后的代码等数据。虽然java虚拟机规范中把该方法描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是要与java堆区分开来。

           垃圾收集在这个区域比较少出现,但是并非数据进入了这个方法区就如永久代的名字一样“永久存在”,这个区域的内存回收主要目标是针对常量池的回收以及类型的卸载,这个区域的回收成绩比较令人满意,尤其是对类型的卸载,条件相当苛刻。

运行时常量池

          运行时常量池是方法去的一部分,用于存放编译期间生产的各种字面量和符号引用。这部分在类加载后进入方法区的运行时常量池存放。

           java语言并不要求常量一定是编译期间才能产生,也就是并非预置入Class文件中的常量池内容才能进入方法区的运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用的比较多的便是String的intern()方法。

            当常量池无法再申请到内存的时候便会抛出OutOfMemoryError异常。

直接内存

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

             java1.4中新加入的NIO类,引入了以一种基于通道(Channel)与缓冲区的IO方式,通过Native函数库可以直接分配堆外的内存。

             本机的内存分配不会受到java堆大小的分配,但是,既然是内存,肯定还是会受到本机总内存大小以及处理器寻址空间的限制。服务器管理员,在配置虚拟机参数时,会根据实际内存设置 -Xmx等参数,但是经常忽略直接内存,使得各个内存区域总和大于物理内存,从而导致动态扩展时候出现OutOfMemoryError异常。



HotSpot虚拟机对象探秘

           以常用的HotSpot虚拟机和常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在java堆中对象分配,布局和访问的全过程。

对象创建
             虚拟机遇到new指令的时候首先会判断这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被加载,解析,初始化过,如果没有则执行相应的类加载过程。
           类加载完成之后,接下来虚拟机将为新生的对象分配内存,对象所需的内存大小在类加载完成之后就可以确定了。如果java堆是绝对规整的,则采用“指针碰撞”的方式分配内存;如果java堆的内存不是规整的,则采用“空闲列表”的方式分配。java 堆是否规整,由所采用的垃圾收集器是否带有压缩整理算法决定。使用Serial、ParNew、Compact过程的收集器时,系统采用的是指针碰撞,使用CMS这种基于Mark-Sweep算法的收集器时候,通常采用的是空闲列表。
          划分可用的内存空间,在多线程下,直接划分并不是安全的,可能出现在给A分配内存时候,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。解决这个问题有两种方案:
1)对分配的内存空间的动作同步处理(实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性)
2)把内存分配的动作按照线程划分在不同的空间执行,即每个线程在java堆中预先分配一小块内存,称为本地线程分配缓冲区,哪个线程需要分配内存,就在哪个线程的TABLE上分配,只有TABLE用完并分配新的TABLE时候,才需要同步锁定,虚拟机是否使用TABLE,可以通过-XX:+/-UseTABLE参数来设定。
           内存分配完成之后,虚拟机需要将分配到的内存空间都初始化为零值。接下来虚拟机需要对对象进行必要的设置,例如这个对象属于哪个类实例,如何才能找到类的元数据,对象的哈希码,对象的分配GC分代年龄等信息,这些信息存放在对象的对象头中。
          上面的操作完成之后,一个新的对象已经产生,但是所有的字段都还是为零,还没执行init方法,一般来说执行new指令之后接着执行init方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

             
对象内存布局
         对象在内存中存储的布局可用分为3块区域:对象头,实例数据和对齐填充。
对象头:一部分是储存对象自身运行时数据,如哈希码,GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID,偏向时间戳等,这部分的数据长度在32位和64位的虚拟机中分别为32bit和64bit。另一部分是类型指针,即对象指向它的类元数据指针,虚拟机通过这个指针来确定这个对象属于哪个类实例。
实例数据:程序代码中所定义的各种类型字段内容。
对齐填充:由于自动内存管理系统要求对象的大小必须是8字节的整数倍,如果不够,那么通过对其填充来补全。


对象的访问定位
          使用对象的时候,我们需要通过栈上面的reference数据来操作堆上的具体对象。对象的访问方式有两种,分别是使用句柄和直接指针两种。

1)如果使用句柄,java堆中会划分出一块内存来作为句柄池,reference中的存储的就是对象句柄的地址,句柄中包含了实例数据与类型数据各自的地址信息。


2)如果使用直接指针访问,那么java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。


两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时候对象移动是非常普遍的行为)时候只会修改句柄中的实例数据地址,而reference本身不需要修改;直接使用指针访问的最大好处就是访问的速度更快,,节省了一次指针的定位时间开销。

Sun HotSpot采用的是第二种对象访问方式。


实战OutOfMemoryError异常


java堆溢出

          将java堆大小设置为20MB,通过参数--XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时候Dump出当前的内存堆转储快照以便事后分析

import java.util.ArrayList;
import java.util.List;

/**
 *  Args: -Xms10m -Xmx10m  -XX:+HeapDumpOnOutOfMemoryError
 */
public class JavaTest {
 
	public static void main(String[] args) {
		List<JavaTest> list=new ArrayList<JavaTest>();
		while(true){
			list.add(new JavaTest());
		}
	}
	
}

运行之后:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2704.hprof ...
Heap dump file created [13107553 bytes in 0.073 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:261)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227)
	at java.util.ArrayList.add(ArrayList.java:458)
	at JavaTest.main(JavaTest.java:12)

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

使用-Xss参数来减少栈内存容量。结果:抛出StackOverFlowError异常,异常出现时输出堆栈的深度相应缩小。

定义大量的本地变量,增大此方法帧中本地变量表的长度。结果:抛出StackOverFlowError异常,输出堆栈的深度相应缩小。

/**
 * 虚拟机栈和本地方法栈OOM测试
 *  Args: -Xss128k
 */
public class JavaTest {
 
	private int stackLength = 1;
	
	public static void main(String[] args) {
		JavaTest jt=new JavaTest();
		try {
			jt.stackLeak();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void stackLeak(){
		stackLength++;
		stackLeak();
	}
}


运行结果:

Exception in thread "main" java.lang.StackOverflowError
	at JavaTest.stackLeak(JavaTest.java:18)
	at JavaTest.stackLeak(JavaTest.java:19)
	at JavaTest.stackLeak(JavaTest.java:19)


创建线程导致内存溢出异常

/**
 * 创建线程导致内存异常
 *  Args: -Xss1m -Xms2m -Xmx2m
 */
public class JavaTest {
	
	public static void main(String[] args) {
		JavaTest jt=new JavaTest();
		try {
			jt.stackLeakThread();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void dontStop(){
		while(true){
		}
	}
	
	public void stackLeakThread(){
		while(true){
			Thread thread=new Thread(new Runnable() {
				
				public void run() {
					dontStop();
				}
			}); 
			thread.start();
		}
	}
}

运行结果

Exception in thread "main" 
Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"


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

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * -XX:PermSize=2M -XX:MaxPermSize=2M
 */
public class JavaTest {

	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 arg1, Object[] args, MethodProxy proxy) throws Throwable {
					return proxy.invoke(obj, args);
				}
			});
			enhancer.create();
		}
	}

	static class OOMObject {
		 
	}
}


本机直接内存溢出

直接内存可以通过 -XXMaxDirectMemorySize制定,如果不指定,默认是堆最大值,下面通过反射获取Unsafe实例,执行内存分配。

 

import java.lang.reflect.Field;

import sun.misc.Unsafe;
 
public class JavaTest {

	private static final int _1MB=1024 * 1024;
	
	public static void main(String[] args) throws Exception{
		 Field unsafeField=Unsafe.class.getDeclaredFields()[0];
		 unsafeField.setAccessible(true); 
		 Unsafe unsafe=(Unsafe)unsafeField.get(null);
		 while(true){
			 unsafe.allocateMemory(_1MB);
		 }
	}

 
}

运行结果:

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


 





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值