《深入理解JVM虚拟机》第二章笔记内存区域、对象创建、内存溢出异常

相较于之前看的Java虚拟机规范(JVM内存区域),这本书多了实践内容,感觉看着更带感呢(主要是趁着打折买了新书,喜新厌旧的我).
在我上一篇关于内存区域的博客中对内存区域的讲解与这一章的内容都大同小异 (里面有段话,挺好玩的,一些资料称Java堆为GC堆,作者说幸好没翻译为垃圾堆.俺又想到之前网传的一句啥子话,不要在垃圾堆里找对象,对象又都在GC堆里啊,难怪找不到对象(开玩笑的)) .

运行时数据区域

相较于规范,本书多了一些细节吧,俺记录一下.
1.永久代: HotSpot虚拟机团队将垃圾收集器的分代设计拓展到了方法区,使得垃圾收集器可以像管理堆一样管理方法区的内存 (主要针对常量池的回收以及对类型的卸载),以省去为方法区专门编写管理代码的方式,可是这种设计并不太好,更容易出现内存溢出问题.到了Java8就全部移至使用直接内存的元空间了.
2.运行时常量池的动态性: 运行时期也可以将新常量放入池中(如String::intern()方法)
**3.直接内存:**它不是运行时数据区的一部分,也不是规范中定义的内存区域,比如NIO类(基于通道与缓冲区的I/O流)可直接分配堆外内存,以提升性能,受本机内存以及处理器寻址空间的限制,设置各区域动态内存拓展上限时别忘了考虑直接内存的大小.
那么按照这些更新,再画一下运行时数据区域:
在这里插入图片描述

对象

1.内存分配

主要分为两种分配方式:
在这里插入图片描述
而在多线程的情景下就需要考虑到内存分配的安全性了,毕竟对象的创建是一个很频繁的事情,这里也有两种方式来保证线程安全:
在这里插入图片描述

2.初始化

对象信息分为: 对象头,实例数据,对其填充 .
如果是TALB的方式可直接将内存区域提前初始化为0值,如果不是则将对象除了对象头初始化为对应的0值.

对象头:

初始化必要信息(对象头).对象头分为两类信息;

  1. Mark Word:HashCode,分代年龄,锁标志状态,线程持有锁,偏向ID,偏向时间戳等
   static class test{

        public synchronized void test(){

        }
    }
    public static void main(String[] args) throws InterruptedException {

        Object obj = new test();
        print(ClassLayout.parseInstance(obj)
        .toPrintable());//1。偏向锁
        System.out.println(obj.hashCode());
        print(ClassLayout.parseInstance(obj).
        toPrintable());//2.无锁
        new Thread(()->{
            synchronized (obj){
                System.out.println("thread1");
                print(ClassLayout.parseInstance(obj).
                toPrintable());//3.轻量级锁
            }
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        Thread.sleep(300);
       synchronized (obj){
            System.out.println("main");
           print(ClassLayout.parseInstance(obj).
           toPrintable());//3.轻量级锁
       }
        //查看对象内部信息
        print(ClassLayout.parseInstance(obj).
        toPrintable());//4.无锁
        new Thread(()->{
            synchronized (obj){
                System.out.println("thread1");
                print(ClassLayout.parseInstance(obj).
                toPrintable());//5.重量级
            }
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        synchronized (obj){
            System.out.println("thread1");
            print(ClassLayout.parseInstance(obj).
            toPrintable());//6,重量级锁
        }
//        //查看对象外部信息
//        print(GraphLayout.parseInstance(obj).toPrintable());
//
//        //获取对象总大小
//        print("size : " + GraphLayout.parseInstance(obj).totalSize());
    }
}

1.偏向锁1偏向锁
2.无锁
2无锁
3.轻量级锁3轻量级锁
4.轻量级锁4.轻量级锁
5.重量级锁无锁
6.重量级锁
6.重量级锁

存储内容标志位状态
对象哈希码,对象分代年龄01无锁
指向锁记录的指针00轻量级锁
指向重量级锁的指针10重量级锁
11GC
偏向ID,偏向时间戳,分代年龄01可偏向

对象头具体信息

  1. 指向类型的指针:以此来确定是哪个类的实例(不是所有虚拟机都会保留类型指针的).
  2. 如果是数组对象就还需要记录数组长度
实例数据

HotSpot的默认分配顺序是按照宽度大小顺序分配,同宽度的一起存放,父类中定义的变量再按照这个基础规律,先于子类,也可以设置+XX:CompactFields参数,让子类窄数据放于父类变量的间隙.

对齐填充

系统要求起始地址必须是8字节的整数倍,所以若需要则通过对齐填充实现.

3.对象访问的定位

栈上reference数据来操作堆上具体对象.
有两种访问方式:

  1. 句柄访问:堆中划分出一个内存作为句柄池,reference保存的对象句柄的地址,而句柄存储了对象示例数据和类型数据的地址,好处是如果对象被移动(垃圾收集时会被移动(比如标记-复制算法,标记整理算法啊啥的)),那么可以直接修改句柄信息,无需修改栈上reference.
    在这里插入图片描述
  2. 直接访问:访问对象地址,对象中存储的对象类型指针再指向方法区中的对象类型信息,好处是快.
    在这里插入图片描述

OOM异常

1.堆溢出

设置堆最大内存和最小内存都为20m(即不能扩展),当堆内存空间溢出时输出堆的内存快照:

//-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError

主要思路就是疯狂的实例化对象,直到塞满堆.
排查思路是使用内存映像分析工具,分析是内存泄漏(含无用对象)还是内存溢出(增大堆空间(注意与机器内存比对))

栈溢出

设置栈内存为128k:

-Xss128k

栈有两种异常,
1.当没有空间以分配栈帧时,会报栈溢出异常.
2.当多线程的场景下无法分配栈空间时会报内存溢出异常.(俺是64位机器复现不了,还把电脑搞挂了23333)
解决思路: 栈深度来说一般是够用的,如果内存溢出,如果无法换64位机器或者减少线程数,那就减小每个栈所占内存.

方法区和运行时常量池溢出
-XX:PermSize=10M//不过8以后会报异常,没有这个空间了
-XX:MaxMetaspaceSize=10M //设置元空间最大为10M,默认为-1,即不受限制
-XX:MetaspaceSize=10M//初始空间

方法区内存溢出,可能常量池溢出,也阔能方法区溢出
常量池的话,用于存储编译时期产生的字面量和符号引用的,所以可以疯狂产生字面量(书中例子就是字符串常量池,不过,Java6以后的JVM都不会引发这个异常,因为字符串常量池被移到堆中了)
方法区的话,存储类信息,实际场景中当出现很多动态加载的类时,就要格外小心方法区内存溢出.

本地直接内存溢出
-XX:MaxDirectMemorySize=10M//指定直接内存为10M

直接操作本地内存的,比如NIO之类,如果产生的Heap Dump(Java进程所使用的内存情况在某一时间的一次快照比较小),就有可能是这个问题了。

未解决问题

为啥1,2锁状态的转换,为啥调用了hashcode方法就由偏向锁变为了无锁状态,

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值