JVM(一):内存区域与内存溢出异常
JVM(一):内存区域与内存溢出异常
最近买了《深入理解Java虚拟机这本书》,将自己的学习记录一下
概述
jvm的优点
1:虚拟机自动内存管理机制,不需要为每一个new去写配对的delete/free代码(也就是说不容易出现内存溢出的情况)
2:程序员将内存管理的权利交给了java虚拟机,方便开发
JVM运行时的数据区域
私有(线程独享):程序计数器、java虚拟机栈、本地方法栈
公有:方法区、堆
程序计数器
作用:程序计数器是一块很小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
简单来说,就是用于记录程序运行到哪里了,以便于进行分支、循环、跳转、异常处理、线程恢复等操作
异常:不会出现异常
Java虚拟机栈(栈)
作用:每个方法被执行时,java虚拟机都会同步创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法出口等信息
局部变量表存放了编译期间可知的基本数据类型、对象引用(reference,指向对象地址)
异常:
1:如果线程请求的栈深度大于虚拟机所允许的深度,会抛出SrackOverflowError异常
2:当栈扩展时,无法申请到足够的内存,会抛出OutOfMemoryError异常(内存溢出)
本地方法栈
**作用:和栈的作用基本类似,区别为:栈是为虚拟机执行Java方法服务的,本地方法栈是为虚拟机使用本地(Native)方法时服务的
异常:同栈的异常一样
堆
作用:存放对象实例,几乎所有的对象实例(包括数组)都在这里分配内存(在这本书里说到:随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。)
分代设计:
堆会分为新生代、老年代、永久代三个区域
而新生代又会分为:Eden区、Survivor区(包括from和to区域,比例为1:1)
一般来说,堆初始大小假设为600M,则新生代占200M(1/3),老年代占400M(2/3)
新生代中的Eden区,from区、to区分别占比为8:1:1(具体原因会在第二节垃圾收集器里提到)
堆大小改变:
-Xmx:最大大小
-Xms:最小大小
异常:当堆扩展时,无法申请到足够的内存,会抛出OutOfMemoryError异常(内存溢出)
方法区
作用:主要存储已经被虚拟机加载的类型信息、常量、静态变量、代码缓存等数据
运行吃常量池:是方法区的一部分,包含了类的版本、字段、方法、接口等描述信息以及常量池表(存放编译期生成的各种字面量与符号引用)
jdk1.8之前:方法区中包括了字符串常量池和运行时常量池
jdk1.8之后:将字符串常量池移入了堆当中
异常:当方法区扩展时,无法申请到足够的内存,会抛出OutOfMemoryError异常(内存溢出)
HotSpot虚拟机对象
对象分配空间的两种方法
1:指针碰撞
假设堆中的内存是规整的,则把所有正在使用的内存放到一边,未使用的放到另一边,中间放置一个指针作为分界点指示器,每当分配内存时,则将指针向空闲空间的方向移动对应的大小
2:空闲列表
空闲列表记录哪些内存空间是可用的,在分配的时候找到一块足够大的空间划分给对象实例
分配方式的选择
由Java堆是否规整决定,是否规整由所采用的垃圾收集器是否带有空间压缩整理的能力决定的(Serial、ParNew),带有则可选用指针碰撞算法
像CMS基于清除算法的收集器时,理论上只能使用空闲列表分配内存
指针碰撞存在的问题
并发情况下,修改指针位置会出错,也就是说线程不安全
例如,正在给A对象分配内存,但还没来得及修改,B对象又使用了原来的指针分配内存
解决方案
1:对分配空间的动作进行同步处理(采用CAS配上失败重试的方式保证原子性)
2:将分配空间的动作按照线程划分在不同的空间进行,称为本地线程分配缓冲
对象的内存布局
分为三部分:对象头、实例数据和对齐填空
对象头
第一类:用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
第二类:类型指针,即对象指向它的类型元数据的指针,通过这个指针来确定该对象是哪个类的实例
对象的访问定位
1:句柄访问
在堆中划出一块内存作为句柄池,reference(引用)存储对象的句柄地址,句柄中包含了对象实例数据和类型数据
优点:对象被移动时,只需要改变句柄中的实例数据指针,而引用本身不用改变
2:直接指针访问
直接存储对象的地址
优点:访问速度更快