JVM内存结构模型
程序计数器:
1.线程私有的,是一块较小的内存空间,当前线程所执行的字节码的行号指示器
2.每个线程都有一个独立的程序计数器,各线程之间程序计数器互不影响,独立存储
3.此内存区域是唯一一个在java虚拟机中没有OutOfMemoryError异常的区域
Java虚拟机栈:
1.线程私有的,生命周期跟线程相同
2.虚拟机栈描述的是Java方法执行的内存模型。每个方法执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息
3.局部变量表存放了编译期间各种基本数据类型和对象引用,在编译期间完成内存分配
4.如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机栈可以动态扩展,但扩展时无法申请到足够的内存,将抛出OutOfMemoryError异常
本地方法栈:
1.根Java虚拟机栈的作用类似,Java虚拟机栈为执行Java方法服务,而本地方法栈为Native方法服务
2.跟Java虚拟机一样,本地方法区也会抛出StackOverflowError和OutOfMemoryError异常
Java堆:
1.线程共享的,存放对象的实例
2.Java虚拟机所管理的内存中最大的一块,可分为新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间
3.当堆中没有内存完成实例分配,且堆内存也无法再扩展时,将抛出OutOfMemoryError异常
方法区:
1.线程共享的,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器后的代码等数据
2.当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常
内存分配方式
虚拟机遇到一条new指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行响应的类加载过程。
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可以完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
内存分配方式有两种:指针碰撞 和 空闲列表
-
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲那边挪动一段与对象大小相等的距离,这种分配方式称为"指针碰撞"
-
空闲列表
如果Java堆中内存不是规整的,已使用和未使用内存相互交错,那么无法简单进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为"空闲列表"
PS:并发安全问题
例如正在给A对象分配内存,指针还没来得及修改,此时对象B又同时使用了原来的指针来分配内存的情况。
两种解决方案:
1.CAS
对分配内存空间同步处理-虚拟机采用CAS失败重试的方式保证更新操作的原子性
2.TLAB(本地线程分配缓冲:Thread Local Allocation Buffer)
把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存
对象的内存布局
对象的访问定位
使用对象需要通过栈上的reference数据来操作堆上的具体对象,对象访问方式也是取决于虚拟机实现而定的。目前主流的访问方式有使用"句柄"和"直接指针"两种。
1.句柄:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了"对象实例数据"与"对象类型数据"各自的具体地址信息
优点:reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的示例数据指针而reference本身不需要修改
2.直接指针:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
优点:速度更快,它节省了一次(对象实例数据)指针定位的时间开销
JVM参数设置
-Xmx:堆最大值
-Xms:堆初始值
当-Xmx和-Xms设置一样时,可避免堆自动扩展
-Xss:栈大小
-XX:PermSize 方法区初始值
-XX:MaxPermSize 方法区最大值
-XX:+HeapDumpOnOutOfMemoryError:可以让虚拟机在出现内存溢出异常时Dump出当前的内存堆转存储快照以便事后进行分析