第二章——Java内存区域与内存溢出异常

本文详细介绍了Java虚拟机(JVM)的内存管理机制,包括线程间的共享内存区域(Java堆和方法区)及线程私有的内存区域(虚拟机栈、程序计数器)。此外,还探讨了直接内存的概念,并通过具体的实战案例分析了不同内存区域可能导致的OutOfMemoryError异常。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、运行时数据区域

运行时数据区域
虚拟机运行的时候会把内存分成几个不同的部分来管理,包括:线程间共享的Java堆和方法区、线程私有的虚拟机栈和程序计数器,还有个单独拿出来讲的直接内存。

下面这几个除了程序计数器,其它的都会报OutOfMemoryError异常。

1、程序计数器

这个好理解,就是当前线程执行到哪里了。每个线程都有一个这个计数器。

2、Java虚拟机栈和本地方法栈

虚拟机栈也是线程私有的,生命周期和线程相同。
每个方法被执行的时候都会创建一个栈帧,栈帧里面存着:局部变量表、操作栈、动态链接、方法出口等信息。
每调用一次方法就创建一个栈帧放在栈里,当请求的栈深度大于虚拟机允许的深度的时候就会报StackOverflowError异常。
这个局部变量表所需的内存空间在编译期间就完成分配了,运行期间不会改变局部变量表的大小。
本地方法栈和虚拟机栈类似,虚拟机栈为执行Java方法服务,本地方法栈为Native方法服务。HotSpot虚拟机直接把本地方法栈和虚拟机栈合二为一了。

3、Java堆

虚拟机启动时创建,此内存区域的唯一目的就是存放对象实例。(随着某些技术发展,现在不是很绝对)
Java堆是垃圾收集器管理的主要区域。这块内存很大,对这块内存安装垃圾收集器再分区的话可以分成新生代、老生代。也能按照内存分配的角度分类,划分出多个线程私有的分配缓冲区。
这块内存在物理上不连续(想来也不能连续?)。

4、方法区和运行时常量池

方法区是线程共享的,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
HotSpot把这块内存也交给GC来管理,所以也叫永久代。这个区域的垃圾回收主要是针对常量池的回收和对类型的卸载。
运行时常量池是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译器生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池里面。(看书上这句话意思是每个Class里面都有一个常量池,这个常量池在类被加载到内存之后会被存放到运行时常量池中,也就是Class的常量池和运行时常量池是两个东西。)
1.8开始HotSpot直接把永久代去了,好像从1.7开始就把本应该在方法区放的东西逐渐的转移到了Java堆和本地内存里面了。在1.8设置永久代的大小的时候它会提醒你配置失效了,现在用metaSpace这个新名字配置。运行时常量池也在本地内存里面了,换句话说在规范里面规定的方法区,实现出来实际上在Java堆里面有一部分,在本地内存有一部分……

5、直接内存

说是这个不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,我的理解是这块内存的引用保存在Java内存中,通过引用直接操作内存,提高某些场景的性能。

二、对象访问

一个在方法体中的对象:Object obj = new Object();
Object obj,表明栈帧里面的局部变量表中有个引用类型的变量,而new Object(),则说明在Java堆里面有一块地方放着这个类的实例。
具体怎么通过本地变量表中的引用访问到Java堆里面的实例呢?有两种方法:句柄、直接指针。
Java堆中的对象涉及到对象实例本身和这个实例对应的类型数据,实例是在Java堆里面,但是这个实例的类型数据是在方法区里面的,所以实例是要存储对应类型在方法区的位置的。实例有了类型数据的位置,就能通过实例来访问到类型数据。

  • 使用句柄

通过句柄访问对象
如图所示,Java堆里面专门有一块地方专门放这个类的实例数据地址和类型数据地址。要访问这个对象的实例,需要先到这个句柄池里面,再从句柄池里面到实例。

  • 直接指针

直接指针访问对象
如果用直接指针访问,那么本地变量里面直接存放实例数据的引用,访问实例的时候可以直接找到这个实例,但是,每个实例数据里面都要专门放一个类型数据的指针,用来从实例找到类型数据。
这两种方式各有特色,句柄方法比较稳定,在对象被移动的时候只需要改变句柄中的实例数据指针,不需要动本地变量表中的引用。
直接访问比较速度快,HotSpot用的就是这种,从整体来看,各种语言和框架访问对象的时候使用句柄的情况也很常见。

三、实战:OutOfMemoryError异常

1、堆溢出
//测试溢出代码
//执行指令如下:Xms和Xmx分别设置堆内存最小和最大值,-XX:+HeapDumpOnOutOfMemoryError用来生成溢出时的文件用来后期分析。
// java -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError HeapOOM
import java.util.ArrayList;import java.util.List;
public class HeapOOM{
	private static class OOMObject{}
	public static void main(String[] args) {
		List<OOMObject> list = new ArrayList<OOMObject>();
		while(true){
			list.add(new OOMObject());
		}
	}
}

发生堆溢出的时候要分析生成的文件,查看GC Roots,定位泄露代码,至于具体怎么搞书上说后面三章讲……

2、栈内存(也就是虚拟机栈和本地方法栈)溢出

栈内存破事比较多,规定了两个异常,一个栈溢出一个内存溢出,单线程情况下无论是创建很多栈帧还是每个栈帧尽可能的大都会出现栈溢出,而不是内存溢出。但是在多线程条件下每个栈内存(注意了,栈内存时线程私有的,多线程会有多个栈内存),所以多线程下每个线程的栈内存越大反而容易出现内存溢出。
每个进程的内存限制下,每个线程的栈内存很大的时候,线程一多就容易报内存溢出,所以反而要适当的减小栈内存,减小栈帧,或者减小堆内存,给栈内存腾地方。
实验代码看起来很麻烦的样子,现在先不试了。

3、运行时常量池溢出

就像上面说的,1.8不兴永久代那一套了,改成元空间了。提示也从书上的永久代溢出变成元空间溢出了……

import java.util.ArrayList;
import java.util.List;
//设置参数
//-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M 
public class RuntimeContantOOM {
    static String base = "string";

    public static void main(String[] args) {
        List list = new ArrayList();
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}

提示我Java堆溢出,我看别人的博客上都能看到元空间溢出来着,结果我自己的显示Java堆…
当然这也说明运行时常量被放在了Java堆里面。

4、方法区溢出

方法区里面除了常量池之外还有类信息,类信息1.8应该存在本地内存里面,调用了测试代码出来的结果是Metaspace溢出,而不是常量池的Java堆溢出。
模拟生成大量动态类在实际应用中经常会出现,方法区溢出的时候可以注意是不是Spring和Hibernate创建了太多的动态类。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
//指令和上面一样也要对元空间的大小做出限制。
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> methodProxy.invokeSuper(o,objects));
            enhancer.create();
        }
    }
    static class OOMObject{}
}

5、本机直接内存溢出

书上这段讲的我大概精简一下吧。
直接内存可以通过-XX:MaxDirectMemorySize指定,如果不指定就和Java堆的最大值一样。
代码没有用DirectByteBuffer类,而是通过反射获取到了Unsafe实例进行内存分配(设计者希望只有rt.jar的类才能使用Unsafe功能),DirectByteBuffer类分配内存也会抛异常,但是这个异常时计算后手动抛出的,真正申请内存的方法是unsafe.allocateMemory()。

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class JavaMethodAreaOOM {

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

和预想一样抛出了内存溢出异常?

小结

本章讲了虚拟机内存如何划分?哪部分区域、什么代码和操作会导致内存溢出?
下一章将详细介绍Java的垃圾收集机制为了避免内存溢出异常做了哪些努力O(∩_∩)O。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值