1.JVM 运行时数据区域
一、定义
JVM 在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域
二、类型
程序计数器、虚拟机栈、本地方法栈、Java堆、方法区(永久区、运行时常量池)、直接内存
程序计数器:
较小的内存空间、当前线程执行的字节码行号指示器;各个线程之间独立存储,互
不影响。
Java栈:
线程私有、生/‘’命周期和线程,每个方法在执行的同时都会创建一个栈帧,用于储存
局部变量表,操作数栈,动态链接,方法出口等信息。方法的执行就对应着栈帧在
虚拟机栈中入栈和出栈的过程,栈里面存放这各种基本信息和对象引用
(-Xss 为jvm启动的每个线程分配的栈内存大小,默认JDK1.4中是256K,JDK1.5+中是1M,可用参数 –Xss调整大小,例如-Xss256k)
本地方法栈(native method):
本地方法栈保存的是 native method 的信息,当一个jvm 创建的线程调
用native 方法后,jvm 不再为其在虚拟机中创建栈帧,jvm只是简单地动态链接并直
接调用native 方法。
Java堆(heap):
Java堆是Java里面重点关注的一块区域,因为涉及到内存分配(例如:new 关键字
,反射)与回收(回收算法,收集器等)。
(-Xms 为jvm启动时占用内存的大小,堆分配内存的最小值 缺省 为物理内存的
1/64;
-Xmx 堆分配内存的最大值 缺省 为物理内存的1/4;
-Xmn 新生代的大小--------------------------------待补充
-NewSize 新生代最小值--------------------------------待补充
-MaxNewSize 新生代最大值--------------------------------待补充
公式:老生代 = Xmx - Xmn;
)
用native 方法后,jvm 不再为其在虚拟机中创建栈帧,jvm只是简单地动态链接并直
接调用native 方法。
方法区(永久区):
用于存储已经被虚拟机加载的的类信心,常量("aaa",'1111"),静态变量 (static
变量) 。
(
JDK1.7 及以前
-XX:PermSize 为非堆区初始内存分配大小(permanent size 持久化内存);
-XX:MaxPermSiez 为非堆区分配的内存的最大上限;
JDK1.8 以后
-XX:MetaSpaceSize 为非堆区初始内存分配大小(达到该值就会触发gc 进行
类型卸载,同时gc会对该值进行调整:如果释放了大量的空间,就适当降低该
值;如果释放的很少空间,会适当提升该值,最大值不会超过
MaxMetaSpaceSize);
-XX:MaxMetaSpaceSize 为非堆区内存分配最大值(只会受物理机内存大小限
制);
)
运行时常量池(方法区的一部分):
运行时常量池是方法区的一部分(JDK1.6),运行时常量池是堆的一部(JDK1.7,
JDK1.8),用于存储编译期生成的各种字面量("aaa",'1111")等和符号引用。
直接内存:
不是虚拟机运行时数据区的一部分,也不Java虚拟机规范中定义的内存区域;
1.如果使用了NIO,这块区域会被频繁使用,在Java堆内可以用directByteBuffer对
象直接引用并操作;
2.这块区域不堆内存大小限制、只受物理机内存大小限制;可以通过
MaxDirectByteBuffer来设置(缺省为堆内存最大值)所以也会oom异常;
三、各个版本内存区域的变化
四、深入辨析堆和内存
功能:
1. 以栈帧的方式储存方法调用的过程,并储存方法调用方程中基本数据类型的变量
(int,short,long,byte,float,double,boolean,char)以及对象的的引用变量,其内存分配还栈
上,变量出了作用域就会自动释放;
2. 而堆内存用来储存Java中的对象,无论是成员变量、局部变量、还是类变量,他们指向的
对象都存储在堆内存中;
线程独享还是共享:
1. 栈内存归属于单个线程(线程私有),每个线程都会有一个栈内存,其储存的变量只能该
线程可见;
2 .堆内存中的对象对所有线程可见(线程共享),堆内存中的对象可以被所有线程访问;
(int,short,long,byte,float,double,boolean,char)以及对象的的引用变量,其内存分配还栈
3 .栈内存要远远小于堆内存,栈的深度是有限制的,可能发生StackOverFlowError;
五、方法出入栈
1.方法会打包成栈帧,然后存入栈里面(存储结构先进后出)一个栈帧最少包括局部变量表,操作数栈和帧数据区。
2.栈上分配(几乎所以的对象都是堆上分配的,但也有例外)。
虚拟机提供的一种优化技术,基本思想是,对于线程私有的对象,将打散分配在栈上,而不分配在堆上。好处是对象跟随着方法的调用结束自行销毁,不需要GC回收,可以提升性能;栈上分配技术的基础(逃逸分析----判断对象是否会逃逸出该方法体(作用域) 例如:return;)
“-” 关闭 ”+“开启
JVM 运行模式(-mix 自动识别/-client 客户端 /-server 服务器(只有在该模式下可以开启逃逸分析))
-XX:+DoEscapeAnalysis (开启逃逸分析 默认打开)
-XX:+PrintGC(打印GC)
-XX:+EliminateAllocations (标量替换 例如:User 对象的两个成员变量有可能被识别为独立的局部变量在栈上分配 默认打开)
-XX:-UseTLAB
TLAB:ThreadLocalAllocationBuffer(线程本地分配缓存)
案例:
VM arguments:
-server -Xms10m -Xmx10m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations -XX:-UseTLAB
六、虚拟机中内的对象
对象的分配(当虚拟机接收一条new obj 指令时)
1. 先执行想对应的类加载过程(先根据对象检查是否加载过该类,如果不存在则会加载这个类)
2. 为该对象在内存中分配对象(确定大小的一块内存从Java 堆中 划分出来)
如果Java堆中内存是绝对规整的所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲的区域那边挪动一段对象大小的内存区域。这种分配方式称之为”指针碰撞“。
如果Java堆中内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机必须维护一个列表(关系映射表)记录那些内存是空闲的可用的,在分配的时候从列表种找一块足够大的区域划分给实例对象,并更新表上的记录。这种分配称之为”空闲列表“。
选择那种分配方式由Java堆是否规整决定,而Java堆是否规整又由采用的GC是否带有压缩整理功能决定。
为该对象
除如何划分可用空间之外,还有另一个需要考虑的问题是对象创建在虚拟机种非常频繁的行为(高并发),即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能存在现在正在给A分配内存,指针还没来的急修改,对象B又同时使用原来的指针来分配内存的情况。
解决这个问题有两种方案
一种是对分配内存空间的动作做同步处理(实际虚拟机上采用CAS配上失败重试的方式保证更新操作的原子性)。
另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆种预先分配一块私有空间内存,也是就本地线程分配缓存(ThreadLocalAllocationBuffer,TLAB)如果设置了虚拟机参数-XX:UseTLAB,在线程初始化时的同时也会申请一块指定大小的内存,只给当前线程使用,这样线程单独都拥有一个Buffer,如果需要分配内存,就在自己的Buffer上分配,就不会存在竞争关系,可以提升分配效率。当Buffer 容易不够时,再重新从Eden区域(HotSpot JVM 新生代分为三个部分 一个Eden区和两个Survivor区(from/to))申请一块继续使用。
TLAB 的目的是为在新对象分配空间时,让每个Java应用线程在使用自己专属的分配指针来分配空间,减少同步开销。
TLAB 只是让每个线程有私有的分配指针,但底下对象的内存空间还是给所有线程访问的,只是无法在该区域分配(类似于分蛋糕,我们只负责分配蛋糕到你手上了,至于谁吃了我们并不管);当一个TLAB用满(分配指针top撞到分配极限end了),就在新申请一个TLAB;
3. 内存分配完成后,虚拟机需要将分配到内存空间的初始化为零值(如:private int userId = 0 ; 等等)。这一步保证操作了对象的实例字段在Java代码种可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的值。
4. 虚拟机对对象进行必要的设置,例如这个对象的是那个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息放在对象的对象头之中。
5.前面四步工作完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但是从Java程序的视角来看,对象创建才刚刚开始,所有的字段都还是零值,所以一般来说,执行new指令之后接着把对象按照程序猿的意愿初始化,这样一个真正可用的对象才算完全产生出来。
七、对象的内存布局
1. 在HotSpot JVM 中,对象在内存中存储的布局分为3块区域(对象头(header)、实例数据(Instance Data )、对齐填充(Padding)) :
对象头(header):包括两部分信息,
第一部分 用于存储对象自身的运行数据,如哈希码、GC分年龄,锁状态标志、线程持有的锁、线程偏向ID、偏向时间戳等。
第二部分 类型指针,即对象指向它类元数据的指针,JVM 通过这个指针来确定这个对象属于那个类的实例。
对齐填充(Padding):对齐填充不是必然存在的,无特殊涵义,起到占位符的作用。由于HotSpot VM 的自动内存管理系统要求对象必须是8字节的整数倍,当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。
八、对象访问的定位
1.主流的访问对象方式(Java程序需要通过栈上reference数据来操作堆上的具体对象)
对象被移动:GC回收内存进行内存规整时会造成实例数据指针变动。
句柄访问:Java堆中会划出一块内区区域作为句柄池,reference中储存了对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。(优点:使用句柄访问,reference 储存的是稳定句柄地址,在对象被移动时只会更改句柄中对象实例数据指针,而reference不会修改)
直接指针访问(Sun HotSpot 使用):reference中存储的直接就是对象地址。(优点:因为是直接访问所以速度快,节省了一次定位的时间开销,在Java中对象的访问是非常频繁的,积少成多这类开销也是非常可观的执行成本)。
九、堆参数设置和内存溢出模拟
1.堆溢出
案例:
VM arguments:-Xms2m -Xmx2m -XX:+PrintGC
java.lang.OutOfMemoryError: GC overhead limit exceeded 数据缓慢堆积在堆里面直接撑爆内存。GC回收次数超过上限(通常是死循环、递归)
java.lang.OutOfMemoryError: Java heap space 你要分配的数据大小 超过 内存分配的最大值(通常是分配了巨形对象)
2. 栈溢出
案例:
VM arguments:-Xss256K
java.lang.StackOverflowError (栈被栈帧给撑爆了)