JAVA内存区域与对象的内存布局--深入理解JAVA虚拟机读书笔记

一、JAVA内存区域及内存溢出异常

内存模型-c500

1. 程序计数器–线程私有

一块较小内存,当前线程所执行的字节码的行号指示器,是唯一一个在java虚拟机规范中没有任何OutOfMemoryError的内存区域。

2. Java虚拟机栈–线程私有

Java虚拟机栈描述的是Java方法执行的内存模型,存储Java运行的局部变量表。

  • 如果线程请求的栈深度大于虚拟机所运行的深度,将抛出StackOverflowError异常
  • 如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够内存,抛出OutOfMemoryError异常
public class JavaVMStackSOF {

    private int index;
    public void stackLeak() {
        index++;
        stackLeak();
    }

    /**
     * JVM运行参数:-Xss128k
     * 参数说明:-Xss128k设置栈内存容量
     */
    public static void main(String[] args) {
        JavaVMStackSOF stackSOF = new JavaVMStackSOF();
        stackSOF.stackLeak();
    }
}

//运行结果
Exception in thread "main" java.lang.StackOverflowError
	at com.zk.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
	at com.zk.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)

在单个线程下,无论是由于栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常(当栈空间无法继续分配时候,到底是内存太小还是已使用的栈空间太大,本质上是一样的)
如果通过不断创建线程(每个线程会分配对应的栈内存,线程分配的栈空间越大越容易OOM),理论上是可以抛出OutOfMemoryError异常。

3. 本地方法栈–线程私有

本地方法栈与虚拟机栈的作用非常相似,虚拟机栈为执行Java方法服务,本地方法栈为虚拟机使用到的Native方法服务

  • 本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError异常

内存不足异常模拟方式同Java虚拟机栈

4. Java堆–线程共享

Java堆是虚拟机所管理的内存中最大的一块,它被所有线程共享。所有的对象实例以及数组都在堆上分配。现在收集器基本采用分带收集算法,所以Java堆可以细分为:新生代和老年代,在细致点有Eden空间,From,Survivor空间,To Survivor空间等。

  • 如果在堆中没有足够内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常
/**
 * JVM运行参数:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 * 参数说明:Xms堆最小值,Xmx堆最大值,-XX:+HeapDumpOnOutOfMemoryError出现内存溢出Dump当前内存堆快照
 */
public static void main(String[] args){
    List<Object> list = new ArrayList<>();
    while (true){
        list.add(new Object());
    }
}

//运行结果
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 com.netease.loan.fcs.domain.postloan.dto.flat.FlatPubFundsListReqDto.main(FlatPubFundsListReqDto.java:54)

5. 方法区–线程共享

方法区(又称”永久带“)存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据

  • 当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
// 借助CGLib使方法区内存溢出
public class JavaMethodAreaOOM {
    static class OOMObject{}

    /**
     * JVM运行参数:-XX:PermSize=128K -XX:MaxPermSize=128k
     * 参数说明:-XX:PermSize设置方法区内存容量;-XX:MaxPermSize设置方法区最大内存容量
     */
    public static void main(String[] args) {
        while (true){
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                @Override
                public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
                    return methodProxy.invoke(o,objects);
                }
            });
            enhancer.create();
        }
    }
}

以上代码在jdk1.7下抛出"OutOfMemoryError: PermGen space";在jdk1.8下不会(1.8移除了方法区内存)。

6. 运行时常量池–线程共享

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用,这部分内存将在类加载后进入方法区的运行时常量池中存放

  • 当常量池无法再申请到内存时会抛出OutOfMemoryError异常

在JDK1.6[常量池分配在永久代中]中可以通过String.intern()来动态新增运行时常量,模拟OOM; 在JDK1.7中常量池移到了堆中, 无法通过String.intern模拟。

7. 直接内存–线程共享

直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的区域。JDK1.4新增的NIO类,引入基于通道的缓冲区(Buffer)的IO方式,它可以使用Native函数库直接分配堆外内存,然后通过DiractByteBuffer对象引用进行操作。直接内存的分配不受Java堆大小的限制。

  • 在配置虚拟机参数时,会根据内存设置-Xmx等参数信息,如果忽然了直接内存,使得各个内存区域的总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError异常
/**
 * JVM运行参数:-Xmx10m -XX:MaxDirectMemorySize=2M
 * 参数说明:-XX:MaxDirectMemorySize限制最大直接内存大小
 */
public static void main(String[] args) throws Exception {
    List<ByteBuffer> list = new ArrayList<>();
    while (true) {
        list.add(ByteBuffer.allocateDirect(1024 * 1024));
    }
}

// 运行结果
Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
    at java.nio.Bits.reserveMemory(Bits.java:693)
    at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
    at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
    at com.zk.memory.DirectMemoryOOM.main(DirectMemoryOOM.java:21)

二、JAVA对象内存布局

1. 对象的创建

1.加载对应类[加载->解析->初始化]
2.为新对象分配内存[指针碰撞,空闲列表]
3.将分配到的内存空间初始化为零值[不包括对象头]
4.对创建对象进行必要的设置(类元数据信息,对象哈希吗,对象GC分带年龄等)
5.执行对象的方法

指针碰撞:Java堆中的内存绝对规整,所有用过的内存都放在一边,空闲的放在另一边,中间指针作为分界点指示器,那内存分配仅把指针挪动一段与对象大小相等的距离。
空闲列表: Java堆中已使用内存和空闲内存相互交错(不规整),虚拟机必须维护一个列表,记录哪些内存空闲。在分配的时候从空闲列表中找到一块足够大的空间划分给对象实例,并更新内存记录列表。

2. 对象内存布局

在虚拟机中,对象在内存中存储布局分为3块区域:对象头(Header),实例数据(Instance Data),对齐填充(Padding)

  1. 对象头:哈希码,GC分代年龄,锁状态,类型指针(类元数据类型的指针)
  2. 实例数据:实例对象的字段内容
  3. 对齐填充:占位符(对象的大小必须是8字节的整数倍)

3. 对象的访问定位

  1. 通过句柄访问对象
    通过句柄访问对象-c500

  2. 通过直接指针访问对象
    通过直接指针访问对象-c500

使用句柄访问的最大好处就是reference中存储的是稳定的句柄地址,在对象移动(GC时候)只会改变句柄的实例数据指针;使用直接指针的最大好处就是速度更快,节省了一次指针定位的时间开销。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值