JVM笔记1:Java内存模型及内存溢出

 

灰色:所有线程间共享

白色:线程私有

参照:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5


程序计数器:当前线程所执行的字节码的行号指示器,字节码解释器通过改变该计数器的值来选取下一条需要执行的字节码指令。

1,一块很小的内存空间

2,每条线程都需要一个独立的计数器(java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的计数器)

3,该内存区域不存在OOM

 

Java虚拟机栈:为虚拟机执行java方法服务。每个方法被执行的时候都会同时创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在java虚拟机栈中从出栈到入栈的过程。

(通常习惯于将java内存区划分为堆内存和栈内存,这种划分方式很粗糙,其中的栈内存对应的就是虚拟机栈中的局部变量表部分。局部变量表在编译期间完成分配,当进入一个方法时,需要在帧上分配多大的局部变量空间是完全确定的,方法运行期间不会改变局部变量表的大小)

1,线程私有,生命周期和线程相同

2,线程运行时需要的栈空间大于虚拟机所允许的大小,抛出StackOverflowError

3,如果虚拟机栈可以动态扩展,当扩展时无法申请到足够的内存会抛出OutOfMemoryError

4,新的线程创建时,没有足够的内存创建与之关联的栈空间会抛出OutOfMemoryError

设置方式:-Xss,每个线程栈空间大小

Java基本数据类型保存在虚拟机栈中,如果线程运行时虚拟机栈没有足够的空间存入更多基本数据类型,则会报栈内存溢出

//-Xss100k
public class Test {
	private int num = 1;
	
	public void recursion(){
		num++;
		recursion();
	}
	
	public static void main(String[] args) throws Throwable{
		Test test = new Test();
		try{
			test.recursion();
		}catch(Throwable e){
			System.out.println(test.num);
			throw e;
		}
	}
}
2403
Exception in thread "main" java.lang.StackOverflowError

 

本地方法栈:和虚拟机栈的作用类似,不过是为虚拟机执行native方法服务,虚拟机规范对这块没有强制规定,具体虚拟机可以自由实现(sun Hotspot将本地方法栈和虚拟机栈合二为一),本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError

设置方式-Xoss,Hotspot虚拟机本地方法栈和虚拟机栈合二为一,故这个参数不生效

 

Java堆:虚拟机规范要求所有的对象实例及数组都需要在堆上分配(垃圾收集器管理的主要区域),随着技术的发展,现在也不是很绝对

1,所有线程共享

2,虚拟机启动时创建

3,可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可

4,如果在堆中没有完成实例分配,并且堆也无法再扩展时(通过-Xms和-Xmx控制),会抛出OutOfMemoryError

设置方式:

-Xms Java堆初始内存,默认值为物理内存的1/64,当可用的Java堆内存小于40%时,JVM会将内存调整至-Xmx所允许的最大值

-Xmx Java堆最大内存,默认值为物理内存的1/4,当可用的Java堆内存大于70%时,JVM会将内存调整至-Xms所指定的初始值

根据存放对象生命周期的不同,将堆分为新生代和老年代2个部分,新生代用来存放生命周期较短的对象,老年代则存放生命周期较长的对象

由于大部分新生代对象会在GC时被回收,为了提高回收效率,新生代使用复制算法进行GC(JVM笔记3:Java垃圾收集算法与垃圾收集器),并将其按照8:1:1的比例分为一块较大的Eden空间和2个较小的Survivor空间

设置方式:

-Xmn 新生代内存大小,无默认值,建议为整个堆的3/8

-XX:NewSize 新生代与老年代的比值,如果-XX:NewSize=4,则新生代:老年代=1:4,如果已经设置了-Xmn参数,则不需要设置此参数

-XX:SurvivorRatio 新生代中Eden区与Survivor区的大小比率,默认为8,即Eden:survivor=8:1

 

堆最小值为10m,并且不进行自动扩展,当没有足够的内存分配给新创建的对象时,便会堆内存溢出

//-Xms10m -Xmx10m
public class Test {
	public static void main(String[] args) {
		List<Object> list = new ArrayList<Object>();
		while(true){
			list.add(new Object());
		}
	}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

 

方法区:用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

JVM规范中称之为方法区。对于Hotspot虚拟机,其使用堆中的一小块空间实现JVM规范中定义的方法区功能,并称之为Permanent Generation Space(永久代)

1,所有线程共享

2,可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可

3,该区域的垃圾收集比较少见,主要针对常量池的回收和类型的卸载。回收条件苛刻但十分必要

4,方法区无法满足内存分配需求时,会抛出OutOfMemoryError

设置方式:

-XX:PermSize Java方法区初始内存,默认值为物理内存的1/64

-XX:MaxPermSize Java方法区最大内存,默认值为物理内存的1/4

 

运行时常量池:方法区的一部分。Class文件除了保存类的字段、方法、接口等信息外,还保存有常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中

1,并非Class文件中的常量池内容才能进入方法区运行常量池,运行期间也能将新的常量放入池中

2,作为方法区的一部分,受到方法区内存的限制,常量池无法再申请到内存时,会抛出OutOfMemoryError

运行时常量池为方法区的一部分,故可以通过方法区大小的设置间接的控制运行时常量池的大小

//-XX:PermSize=2m -XX:MaxPermSize=2m
public class Test {
	public static void main(String[] args){
		int num = 1;
		List<String> list = new ArrayList<String>();
		while(true){
			//String类单独维护了一个初始为空的字符串池。
			//当调用字符串的intern方法时,如果池中存在一个相同的字符串,则返回池中的该字符串,
			//否则将字符串添加至字符串池中,并返回该字符串的引用
			list.add(String.valueOf(num++).intern());
			System.out.println(list.size());
		}
	}
}
35899
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space


直接内存:并不是虚拟机运行时数据区的一部分也不是java虚拟机规范中定义的内存区域

经常会被使用,如:NIO(New Input/Output)类中,引入了基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作

直接内存分配不会受到堆内存的限制,但是会受到总内存大小和处理器寻址空间(如32位系统最大支持3g内存,非确切值,只是举例,实际内存为4g,-Xmx为3g,则直接内存将在剩下的1g中分配,实际上操作系统不知道如何处理3g-4g的地址部分,就会产生异常)的限制,在实际配置内存参数时(-Xmx),如果忽略直接内存,将会使各个内存区域的总和大于物理内存限制(操作系统级和物理上的限制),从而导致OutOfMemoryError异常

设置方式:-XX:MaxDirectMemorySize,如果不指定,则默认与java堆的最大值(-Xmx指定)一样

ByteBuffer.allocateDirect先判断是否有足够的内存用来分配,然后向操作系统申请内存分配

//-XX:MaxDirectMemorySize=1m
public class Test {
	public static void main(String[] args){
		Buffer b =  ByteBuffer.allocateDirect(1024*1024*2);
	}
}
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
}
DirectByteBuffer(int cap) {			
	super(-1, 0, cap, cap, false);
	Bits.reserveMemory(cap);//是否有足够内存
	int ps = Bits.pageSize();
	long base = 0;
	try {
	    base = unsafe.allocateMemory(cap + ps); //真正的申请内存分配
	} catch (OutOfMemoryError x) {
	    Bits.unreserveMemory(cap);
	    throw x;
	}
	unsafe.setMemory(base, cap + ps, (byte) 0);
	if (base % ps != 0) {
	    // Round up to page boundary
	    address = base + ps - (base & (ps - 1));
	} else {
	    address = base;
	}
	cleaner = Cleaner.create(this, new Deallocator(base, cap));
}

 

特别说明:

代码示例均在jdk1.6.0_24下运行调试

不同JDK的运行结果并不相同

Java虚拟机栈的测试代码,在使用jdk1.5.0_22时,无论如何调整-Xss的值,最终显示的num值均为固定值,而使用jdk1.6.0_24时,num值将会随着-Xss值的变大而变大

 

2017-10-20更新:

OOM异常时打印堆信息:

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=d:\


1 从JDK1.7开始,JVM开始进行去永久代工作,运行时常量池已经移动至堆

使用JDK1.7_71测试如下

//-Xmx5m -XX:PermSize=5m -XX:MaxPermSize=5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\
// -XX:-UseGCOverheadLimit
public class Test {
    public static void main(String[] args){
        int num = 1;
        List<String> list = new ArrayList<String>();
        while(true){
            list.add(String.valueOf(num++).intern());
            System.out.println(list.size());
        }
    }
}
94255
java.lang.OutOfMemoryError: Java heap space
Dumping heap to d:\java_pid13036.hprof ...
Heap dump file created [8856263 bytes in 0.044 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

增加永久代的大小并不能增加OOM之前打印出来的数字大小,然而堆内存空间越大,OOM前打印出来的数字越大

增加-XX:-UseGCOverheadLimit是为了避免在OOM之前抛出

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded

官方对此的定义是:并行/并发回收器在GC回收时间过长。其中过长的定义是,超过98%的时间用来做GC但回收了不到2%的堆内存。JVM通过统计GC时间来预测是否将要OOM,提前抛出异常

但是1.7中并未完全移除永久代,虚拟机加载的类信息依然保存在永久代中(使用cglib-nodep-3.2.5.jar)

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

import java.lang.reflect.Method;

//-Xmx5m -XX:PermSize=5m -XX:MaxPermSize=5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\
// -XX:-UseGCOverheadLimit
public class Test {
    public static void main(String[] args){
        int i = 0;
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.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();
            System.out.println(++i);
        }
    }
}
10
java.lang.OutOfMemoryError: PermGen space
Dumping heap to d:\java_pid12980.hprof ...
Heap dump file created [2133283 bytes in 0.009 secs]


2 从JDK1.8开始,JVM完全移除了永久代,虚拟机加载的类信息将保存在新增加的Metaspace元数据空间中

JDK1.8_121,使用上面cglib实现的代码进行测试,结果如下:

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

import java.lang.reflect.Method;

/**
 * Created by seanzou on 2017/10/19.
 */
//-Xmx5m -XX:PermSize=5m -XX:MaxPermSize=5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:\
// -XX:-UseGCOverheadLimit -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
public class Test {
    public static void main(String[] args){
        int i = 0;
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.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();
            System.out.println(++i);
        }
    }
}
595
java.lang.OutOfMemoryError: Metaspace
Dumping heap to d:\java_pid9064.hprof ...
Heap dump file created [3624794 bytes in 0.023 secs]
Exception in thread "main" net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException-->null
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345)
	at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492)
	at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114)
	at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291)
	at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480)
	at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305)
	at com.sean.Test.main(Test.java:26)
Caused by: java.lang.reflect.InvocationTargetException
	at sun.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:459)
	at net.sf.cglib.core.ReflectUtils.defineClass(ReflectUtils.java:452)
	at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:334)
	... 6 more
Caused by: java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	... 12 more
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=5m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=5m; support was removed in 8.0

由于永久代已经移除,JVM已经不再支持PermSize和MaxPermSize这两个JVM参数



  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值