《深入理解JVM》第二章 Java内存区域与内存溢出异常


深入理解JVM

框架图

高清图片地址Java内存区域和内存溢出异常


Java内存区域与内存溢出异常

线程隔离是什么意思?
答:线程隔离用ThreadLocal实现,为解决多线程并发问题提供了一种思路。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本。(感觉像是从共享内存中拷贝出来的本地变量,不需要还原回去那种?)

感觉下面这个表贯穿全部,每看一部分都可以回来看一下。

两种异常

  • StackOverflowError:
    • 原因:线程请求的栈深度大于虚拟机所允许的深度
    • 包含区域:虚拟机栈、本地方法栈(就两个栈)
  • OutOfMemoryError:
    • 原因:Java虚拟机栈容量可以动态拓展,但是申请不到足够内存
    • 包含区域:虚拟机栈、本地方法栈、Java堆、方法区、运行时的常量池

程序计数器:控制不同线程往下运行,一个线程分配一个互不干扰,有利于线程切换回来的时候能接着上次运行的地方继续。这个内存区是唯一一个没有任何OOM情况的区域。

java虚拟机栈:java方法被调用的时候会创建一个栈帧,运行时栈帧入虚拟机栈,结束时出栈。通常讲的是虚拟机栈中的局部变量表,包含了一些编译器的基本数据类型、对象引用、returnAddress类型,里面的空间用局部变量槽表示,每次进入方法时槽的数量是完全确定的,但是一个槽分配多少内存空间是虚拟机决定的。

本地方法栈:和java虚拟机栈差不多,不过是为虚拟机使用的本地(Native)方法服务,异常一样。

java堆:是垃圾收集器管理的内存区域,也是内存中最大的一块,被所有线程共享内存,用来存放几乎所有的对象实例以及数组。GC堆大部分基于“分代收集理论”设计,所以会有很多“代”名词。Java堆可以划分出多个线程私有地分配缓冲区,来提升对象分配时地效率。堆细分的目的是为了更好地回收内存,或者更快地分配内存。物理上不要求连续地内存空间,逻辑上应连续。

垃圾回收就是围绕着这个在做。

方法区:各个线程共享的内存区域,存放已经被虚拟机加载的类型信息、常量等数据。后来的版本用本地内存代替了永久代。可以选择不是先垃圾回收,这块的回收目的是针对常量池的回收和对类型的卸载,但是效果并不好。也会抛出OOM异常。

常量池出现了,在这里。

运行时的常量池:是方法区的一部分,内容来自class文件中的常量池表,在类加载后这部分内容就来到了常量池。具备动态性,不一定要编译器提前植入才行,运行期间也可以把新的常量放到池中。也会抛出OOM

直接内存:不是虚拟机运行时数据区的一部分,使用Native函数库直接分配堆外内存,然后通过java堆里的DirectByteBuffer对象作为这块内存的引用进行操作,这样能显著提高性能,还避免了java堆和native堆的来回复制。


HotSpot虚拟机对象探秘

对象的创建

java代码new了之后,虚拟机的操作:

  1. 检查:先检查一下这个指令的参数能不能在常量池中找到,并且检查一下代表的类有没有被加载、解析、初始化过(为了复用吗?答:有的话就用这个类的信息,以后判断起来也是同一类,类自己的常量池应该也能共有。),没有的话就执行相应的类加载过程。
  2. 分配内存:虚拟机给新对象分配内存,大小在类加载完了的时候就已经知道了,分配的方法视java堆内存是否规整而行,规整就动动指针就行了;不规整就得按照能用的内存表来分配。规整与否由是否代空间压缩整理能力决定。(规整与否看GC方法吧,如果是标记-清除那就不规整,有很多碎片;还有个什么指针,也可以规整操作)。
  3. 分内存的时候还得考虑下线程安全问题,不能分重复了,这里由两个方法,一个是锁定原子性操作;一个是每个线程都预先分配一小块内存,不够的时候再开启同步。
  4. 内存初始化:内存分完了之后,把空间里的值都初始化为零,这样就保证了java代码中的对象不赋值就能直接使用。
  5. 信息设置:接下来还有些必要的设置,把对象的一些信息放到对象的对象头中。(信息一般放在对象头,标记有三种)。
  6. init函数:到此虚拟机的工作就完了,但java程序还有的构造函数,在使用了new关键字之后,java编译器会同时生成两条字节码指令,一个是上面那些,另一个是继续执行init(),至此就结束了。

对象的内存布局

Mark Word内容
三部分,对象头、实例数据、对齐填充。

对象头:包含两类信息,第一类用于存储对象自身运行时的数据,这些被称为“Mark Word”,这个被设计成动态定义的数据结构了,以便在极小空间内存储尽量多的数据;另一部分是类型指针,指向他的类型元数据的指针,java虚拟机通过这个指针来确定是哪个类的实例(继承啥的都在这里说明了?)。如果对象是java数组,那还必须要记录长度,不然虚拟机无法在不知道长度的情况下推断出数组的大小。

实例数据部分:对象正真存储的有效信息,各种字段内容,有自己的也有父类的。相同宽度的字段总被分配到一起存放,所以父类中定义的变量会出现在子类之前。

对齐填充:不是必须存在的,起着占位符的作用。HotSpot虚拟机要求对象起始地址必须是8字节的整数倍,所以任何对象的大小都要是8的整数倍,不够的话就用对齐填充补全。


对象的访问定位

有了对象后要访问,java通过栈上的reference数据来操作堆上的数据,而reference只是一个引用,具体怎么用还得看虚拟机怎么实现的,主流有两种方法:

  1. 句柄访问:java堆中划出一块内存作为句柄池,reference中存储的是对象的句柄地址,而句柄中包含的是对象实例数据和类型数据的地址。

  2. 直接访问:reference中直接存放对象地址,但是这样的话java堆中对象的内存布局就得好好想想了,不过这样就不需要间接访问了,少了一次开销,速度更开,一般都用的这种。


实战:OOM异常(没看)

除了程序计数器,其他的运行时区域都有发生OOM的可能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值