JVM总结
2020-12-09 17:09:05 0 举报
AI智能生成
JVM
作者其他创作
大纲/内容
直接内存
直接内存并不属于虚拟机运行时数据的一部分,也不是《java虚拟机规范》中定义的内存区域。但是它也被频繁使用,也会出现OOM
本机直接内存不受java堆大小的影响
但是受本机总内存大小(物理内存、SWAP分区或者分页文件)以及处理器寻址空间的限制
一般在设置虚拟机参数的时候会设置-Xmx等参数,但是忘记了直接内存,导致各个内存区域总和大于物理内存限制,造成直接内存抛出OOM
NIO(New iput/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数直接分配对外内存,通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一定的场景下显著提升性能,因为避免了在Java堆和Navtive堆中来回复制数据
HotSpot虚拟机
对象
对象创建
1.检查类是否加载
当Java虚拟机遇到一条字节码 new指令时,首先检查该指令的参数是否能在常量池定位到一个类的符号引用,并且检查这个符号引用所代表的类是否被加载,解析和初始化过,如果没有,就必须先执行相应的类加载过程
2.为新生对象分配内存
对象所需要的内存大小,在类加载完成后便可以完全确认,为对象分配空间的任务实际上便等于把一块确定大小的内存从java堆中划分出来。
分配方式
指针碰撞(Bump The pointer)
假设java堆是绝对规整的,所有使用过的内存都被放在一边,空闲的内存放在一边,中间放着一个作为分界点的指示器,那所分配的内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小的距离
空闲列表 Free List
如果java堆不是绝对规整的,已经使用过的内存和空闲内存相互交错在一起,那就没有办法使用指针碰撞,需要虚拟机维护一个列表,记录哪些内存是可用的,在分配内存的时候从列表中找出一块足够大小的内存划分给对象实例,并更新列表上的记录
决定哪种分配范式由java堆是否规整来决定,java堆是否规整由垃圾收集器是否带有 空间压缩整理(Compact) 的能力决定。
Serial、ParNew等垃圾收集器,带压缩整理过程,采用的是 指针碰撞,简单快速
CMS 基于清除算法的收集器,理论上只能采用复杂的空间列表来分配内存
线程安全问题
问题描述
对象创建在虚拟机中是非常频繁的,即使是仅仅修改指针所指向的位置,在并发的情况下也有可能出现对象A分配内存,指针没来得及及时修改,对象B又同时使用了原来的指针
解决方案
对分配内存空间的动作进行同步处理。实际上虚拟机是采用CAS配上失败重试的方式来保证更新操作的原子性
先把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在JAVA堆中都预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区分配,只用本地缓冲区用完了,分配新的缓存区时才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UserTLAB参数来设定
3.初始化分配到的内存
虚拟机必须把分配到的内存空间(不包括对象头)都初始化为零值如果使用了TLAB这项工作也可以提前至TLAB分配时顺便进行
这步操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,使程序能访问到这些字段的数据类型所对应的的零值
4.对对象进行必要的设置
对象的哈希码(实际上要延迟到真正调用了Object::hashCode()方法时才计算),对象的GC分代年龄等信息。这些对象存放在对象头(Object Header)中
上述操作完成之后,从虚拟机的角度来看,新的对象已经产生了。但是从java程序角度来看,对象创建才刚开始--构造函数,即class文件中的<init>()方法还没执行,所有的字段都是默认的零值,对象需的要其他资源和状态信息还没有按照预定的意图构造好。(一般来说根据字节码流中new 指令后面 是否跟随 invokespecial 指令所决定的,java编译器在遇到new关键字的地方同时生成这两条字节码指令,但如果直接通过其他方式产生的则不一定如此。),new指令之后会执行 init方法 按照程序员的意愿对对象进行初始化
对象的内存布局
对象头(Header)
用于存储对象自身的运行时数据
如 HashCode,Gc分代年龄,锁状态标志,线程持有的锁,偏向线程Id,偏向时间戳等
它为了在极小的空间下容量更多的数据,根据对象的状态复用自己的存储空间,所以被设计成一个有着动态定义的数据结构
类型指针
即对象指向他的类型元数据的指针
java虚拟机通过这个指针来确定该对象是哪个类的实例,并不是所有的虚拟机实现都必须在对象数据上保留类型指针。如果对象是一个java数组,那么在对象头还必须有一块用于记录数组长度的数据。因为虚拟机通过普通java对象的元数据信息确定java对象的大小,但是数组的长度是不确定的。将无法通过元数据的信息判定数组的大小
实例数据 (Instance Data)
是对象真正存储的有效信息,即我们在程序代码里所定义的各种类型的字段内容.
这部分数据的存储数据受虚拟机分配策略参数 -XX:FieldAllocationStyle 和 java源码中的定义顺序影响
hotSpot默认的分配顺序为long/doubles,ints,shorts/chars,bytes/booleans,oops(Ordinary Object Poiinters,OOPS)。 相同宽度的字段总是被分配存放到一起,在满足这个条件下,父类定义的变量会出在子类之前,如果hotspot的 +XX:CompacatFoelds 参数为true(默认为true),子类之中较窄的变量也允许插入父类变量的空隙之中,这样能节省空间
对齐填充(Padding)
并非必然存在
并没有特殊含义, 仅作为占位符
由于hotspot的自动内存管理系统要求对象起始地址必须是8字节的整倍数(任何对象的大小都必须是8字节的整倍数)
对象头部分已经被精心设计成8字节的整倍数因此如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全
对象的访问定位
java程序会通过栈上的reference数据来操作堆上的具体对象
主流的访问方式
句柄访问:在java堆中可能划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址
直接指针:reference中保存的就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销
两种方式对比
句柄的好处在于reference存储的是稳定句柄地址,即使对象被移动也只会改变句柄中的是实例数据指针,而reference本身不会改变
直接指针的好处在于速度更快,节省了一次指针定位的时间开销,在java中对象访问非常频繁,这类访问开销积少成多也是非常可观的执行成本
hotSpot使用直接指针的方式进行对象访问
算法细节实现
根节点枚举
安全点
安全区域
记忆集与卡表
写屏障
伪共享(False Sharing)
并发的可达性分析
垃圾回收
分代收集
分代收集理论
弱分代假说:绝大多数对象都是朝生夕灭
强分代假说:熬过很多次垃圾收集过程的对象就越难以消亡
这两个分代假说奠定了常用垃圾收集器的一致设计原则:收集器应该将java堆分成不同的区域,然后将回收对象根据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不同的区域之中存储
名词解释
部分收集(Partial GC): 指目标不是完整收集整个java堆的垃圾收集,分为新生代收集和老年代收集
新生代收集(Minor GC/Young GC): 指目标只是新生代的垃圾收集
老年代收集(Major GC/Old GC): 指目标只是老年代的垃圾收集
混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为
整堆收集(Full GC): 收集整个java堆和方法区的垃圾收集
跨代引用假说
对象不是独立的,对象之间会存在跨代引用
跨代引用相对于同代引用来说仅占极少数 :存在互相引用的两个对象是应该倾向于同时生存或者同时消亡的
记忆集
不应该少量的跨代引用而去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构 “记忆集(Remembered Set)”
这个结构把老年代分为若干个小块,表示出老年代哪一块内存会存在跨代引用,此后当发生 Minor GC 时,只有包含了快带引用的小块内存里的对象才会被加入到GC Roots进行扫描
记忆集是一种抽象数据结构
垃圾回收算法
标记-清除算法
出现最早也是最基础的算法
流程
首先标记出所有需要回收的对象
在标记完成后,统一回收掉所有被标记的对象
可以反过来,标记存活的对象,再统一回收所有未被标记的对象(标记的过程就属于对象是否属于垃圾的判定过程)
缺点
执行效率不稳定:如果java堆中包含了大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清楚动作,导致标记和清楚这两个过程的执行效率都随对象数量的增长而降低
内存空间的碎片化问题:标记、清理之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后再程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作
标记-复制算法
是为了解决标记-清除算法面对大量可回收对象时执行效率低的问题
半区复制(Semispace Conying):
流程
它把内容按照容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就把活着的对象复制到另一块上面,然后一次性把已经使用的这快空间清理掉,如果大量对象存活,复制这些对象将产生大量的内存间复制的开销,但是对于多数对象都是可回收的情况,算法需要复制的就是占少数的存活对象,而且每次都是针对半区进行内存回收,分配内存时也就不用考虑有空间碎片的负责情况,只要一动堆顶指针,按顺序分配即可
优点
实现简单,运行高效
缺点
可用内存缩小到原来的一半
新生代的对象有98%熬不过第一轮收集,因此并不需要按照1:1的比例来划分新生代的内存空间
Appel式回收
基于标记复制算法实现的更加优化的半区复制分代策略
HotSpot中的 Serial,ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局
策略详细
1.把新生代分为一块较大的Eden空间和两块较小的Survivor空间
2.每次分配内存只使用Eden和其中一块Survivor,发生垃圾收集时,把Eden和Survivor中依然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已经使用过的那块Survivor空间
3.虚拟机默认Eden和Survivor的比例是8:1:1
4.当一个Survivor空间无法不足以容纳一次Minor GC之后存活的对象时,就需要以来其他内存区域(大多数是老年代)进行分配担保(Handle Promotion),即如果一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将通过分配担保机制直接进入老年代
标记-整理算法
流程
先标记需要回收的对象,然后把存活的对象都向一边移动,然后直接清理掉边界以外的内存
与标记-清除的区别
“标记-清除”算法是一种非移动式的回收算法,而“标记-整理”算法是一种移动式的
如果移动存活对象,尤其是在老年代这种每次回收,都会存在大量存活对象的区域,移动存活对象并且更新所有引用这些对象的地方是一件极为负重的操作,而且这种对象移动操作必须暂停用户应用程序才能进行,
但是如果跟“标记-清除”算法那样完全不考虑移动和整理存活对象的话,弥散于堆中的存活对象导致的空间碎片化问题就只能依靠更为负责的 内存分配器 和 内存访问器来解决。碧如通过 “分区空闲分配链表”来解决内存分配问题,内存的访问是用户程序最频繁的操作,假如在这个环节上增加额外的负担,肯定会字节影响用户程序的吞吐量
移动则内存回收时会更复杂,不移动则内存分配时会更复杂
停顿时间上对比:不移动存活对象停顿时间会更短,甚至不停顿,移动则需要停顿,存活对象越多停顿时间越长
吞吐量上对比:移动对象更划算,移动存活对象,会使得收集器的效率会快一些,但是,因为内存分配和访问相比垃圾收集频率要高得多,这部分的耗时增加,总吐吞量是下降的。
混合算法
流程
让虚拟机平时多数时间都采用 “标记-清除”算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度大到已经影响对象分配时,再采用“标记-整理”算法收集一次,以获得规整的内存空间
CMS收集器面临空间碎片过多时就是采用这种算法
运行时数据区域划分
程序计数器
线程私有
当前线程执行字节码行号指示器
不会出现内存溢出
如果执行的是本地方法则为Undefined
本地方法栈
线程私有
功能与java虚拟机栈类似
虚拟机用到的本地方法服务
java虚拟机栈
线程私有
随线程生,随线程灭
每个方法被调用时,虚拟机都会同步创建一个栈帧
存储的数据
局部变量表
存放的数据
可知的基础数据类型
boolean、byte、char、short、int、float、long、double
对象引用类型
一个指向对象起始地址的引用指针 或者 一个代表对象的句柄或者其它与此对象相关的位置
returnAddress类型
指向了一条字节码指令的地址
存储空间单位
以局部变量槽(Slot)来表示
64位长度的long 和 double类型的数据会占用两个变量槽。其余的数据类型只占用一个槽
局部变量表所需的内存空间在编译期间完成分配
帧内分配多大的局部变量空间是完全确定的。在方法运行期间不会改变局部变量表的大小
这个大小是指:所占用的变量槽的数量
虚拟机真正使用的内存大小,比如一个变量槽占用32比特或者64比特或者更多,这个根据具体的虚拟机实现来决定的
方法出口
动态链接
操作数帧
一个方法从调用到结束的过程叫:入栈和出栈
异常
如果线程请求的栈深度大于虚拟机锁允许的深度,就会抛出Stack Overflow Error异常
如果虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出 OutOfMemoryError异常
HotSpot虚拟机的栈容量是不可以动态扩展的,在HotSpot虚拟机上是不会由于虚拟机栈无法扩展而导致OOM,但是如果线程申请栈空间失败了就会出现OOM
堆
线程共享
占用内存最多
在虚拟机启动的时候创建
存放对象实例
所有对象实例都在这里分配内存
堆是垃圾收集管理的内存区域,也叫GC堆
大小
可固定
可扩容,大部分是可扩容
异常
java堆中没有完成实例分配,并且堆也不能再扩展时,java虚拟机就会抛出OOM
参数
-Xmx 堆最大值
-Xms 堆最小值
-XX:+HeapDumpOnOutOf-MemoryError 虚拟机出现内存溢出情况是Dupm出当前内存堆转储快照方便以后分析
方法区
线程共享
存储数据
已经被虚拟机加载的 类型信息
常量
静态变量
即时编译器编译后的代码缓存
运行时常量池
保存的数据
class文件中的常量池表(Constant Pool Table),用于存放编译期间生成的各种字面量与符号引用。这部分内容将在类加载完成后存放到方法区的运行时常量池
除了保存class文件中描述的符号引用外,还会把符号引用翻译出来直接引用也储存在运行时常量池中
异常
运行时常量池是方法区的一部分,受到方法区内存的限制,当常量池无法申请到内存时会抛出OOM
动态性
运行时常量池相对于class文件常量池的一个重要特征就是具备动态性
java语言并不要求常量一定只有在编译期间才能产生
并非预置在class文加您中常量池的内容才能进入方法区的运行时常量池
运行期间的也可以将新的常量放入其。这种特性被开发人员用的多的地方是String的intern()方法
字符常量池
HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容
虽然《java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但它却有一个别名叫“非堆” 目的是与java堆区分开来
实现方式
1.8之前 永久代
1.8之后 元空间
0 条评论
下一页