title: 深入Java虚拟机
date: 2018-10-17 19:08:15
tags:jvm
第一章.自动内存管理机制
1.运行时数据区域
1.程序计数器(存档)
-
是一块较小的空间,它可以看作是当前线程所执行的字节码的行号指示器,字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支,循环,线程恢复等基础功能都要依赖这个计数器来完成。
-
Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器。
2.Java虚拟机栈
-
线程私有的,虚拟机栈是描述Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(栈帧是方法运行时的一种基础数据结构),用于存储局部变量表,操作数栈,动态链表,方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
-
局部变量表中存放了编译器可知的各种基本数据类型,对象引用类型和retuanAddress类型
数据类型 字节数 位数 Boolean 1 1 byte 1 16 char 2 16 short 2 16 int 4 32 float 4 32 double 8 64 long 8 64
3.本地方法栈
-
本地方法栈和虚拟机栈所发挥的功能是相似的,它们的区别在于,虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
-
Java不是完美的,Java的不足除了体现在运行速度上要比传统的C++慢许多之外,Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java程序的功能,被native关键字修饰的方法可以用C++语言重写。
4.Java堆
-
是Java所管理的内存中最大的一块,也是被所有线程共享的内存区域,它的目的就是存放对象实例。
-
也是垃圾收集器管理的主要区域,也称之为“GC堆”。(Garbage Collection Heap)
-
从内存回收角度来讲,Java堆可以可以分为新生代和老年代,再细分有Eden空间,From Survivor空间,To Survivor空间等。
-
从内存分配角度来看,Java堆可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
-
进一步的划分是为了更好的回收内存。
5.方法区(永久代)
-
是各各线程共享的内存区域,它用于存储已经被虚拟机加载的类信息,常量,静态变量等数据。
6.运行时常量池
-
运行时常量池是方法区的一部分,Class文件中除了有类的版本,字段,方法,接口等描述信息外,还有常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。
7.直接内存
-
直接内存并不是虚拟机运行时数据区的一部分,但这部分内存也被频繁使用,也可能导致OutOfMemoryError。
2.HotSpot虚拟机对象探秘
1. 对象的创建
-
虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有,那必须先执行相应的类加载过程。
-
类加载检查通过后,接下来虚拟机将为新生对象分配内存。
-
假设Java堆中的内存是绝对规整的,则用指针碰撞方法分配
-
若Java堆内存不是规整的,则用空闲列表分配,
-
选择哪种分配方式是由java堆是否规整决定的,而java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。因此,使用Serial,ParNew等带(标记-整理算法)Compact过程的收集器时,用指针碰撞方式分配,而CMS这种基于(标记-清除算法会产生空间碎片)用空闲列表。
-
内存分配完成后,虚拟机需要将分配到的内存空间初始化位零,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到的这些数据类型字段就是对应的零值。接下来需要对对象进行必要的设置,如这个对象是哪个类的实例,如何找到这个类的元数据信息,对象的哈希码等信息都存放在对象的对象头。
-
执行完new指令后会接着执行init方法,把对象按照我们需要的意愿进行初始化。
2. 对象的内存布局
-
对象在内存中存出的布局可分为三个区域:
-
对象头:分两部分,第一部分存储对象自身运行时数据,第二部分时类型指针,即对象指向它的类元数据的指针,虚拟机通过它来确定这个对象时哪个类的实例。
-
实例数据:对象存储的真正的有效信息(各种字段内容)。
-
对齐填充:占位符作用,当实例数据部分没有对齐时,补充填充(对象的大小必须是8字节的整数倍)
-
3.对象的访问定位
-
使用句柄
-
直接指针
第三章.垃圾收集器
1.概述
-
哪些内存需要回收
-
什么时候回收
-
如何回收
2.对象已死吗
1.引用计数算法
-
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值一就加1,当引用失效时,计数器值就减1,任何时刻计数器为0的对象就是不可能再被使用的
-
java虚拟机中并没有采用引用计数算法来进行内存管理,主要原因是它很难解决对象之间相互循环引用的问题。
2.可达性分析算法(Java)
-
通过称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC roots 没有引用链相通时,则证明此对象是不可用的,所以他们将会被判定为是可回收的对象。
3.引用
-
强引用:代码中普遍存在的,类似 Object o = new Object 这类,只要强引用存在,垃圾收集器永远不会回收被引用的对象。
-
软引用:一些还有用但非必要的对象,对于软引用关联的对象,在系统中将要发生内存溢出异常之前,将会把这些对象列为回收范围之中进行第二次回收。
-
弱引用:非必需对象,在垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
-
虚引用:最弱的引用关系
4.finalize自救方法
-
真正宣告一个对象死亡,至少要经历两次标记
-
如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize方法。当对象没有覆盖finalize方法,或者finalize方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
-
如果一个对象被判定为有必要执行finalize方法,那么这个对象会被放在一个F-Queue的队列之中,并稍后由虚拟机自动建立的一个低优先级线程去执行它。(触发)
-
finalize方法时对象逃脱死亡命运的最后一次机会,如果对象在finalize方法中成功拯救自己——只要重新和引用链上的任何一个对象建立关联即可,那么在第二次标记时它将会被移出即将回收的集合,反之就被回收。
3.垃圾收集算法
1.标记-清除算法
-
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
-
效率问题:标记和清除的效率都不高
-
空间问题:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能导致以后在程序运行时需要分配大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集。
2.复制算法
-
将内存按容量划分为大小相等两块,每次使用其中的一块,当这一块快用完时,就将还存活着的对象复制到另一块上面,然后把已使用过的内存空间一次清理。
-
分配时不用考虑内存碎片等复杂情况,实现简单,
-
缺点是内存缩小为原来的一半,代价高。当对象存活率较高的时候要对对象进行较多的复制,效率会变低。
3.标记-整理算法
-
首先标记出所有需要回收的对象,然后让所有·存活的对象都向同一端移动,然后清除掉端边界以外的内存
4.分代收集算法
-
老年代:对象存活率高,没有额外空间对他进行分配担保,用标记-清除或者标记-整理
-
新生代:有大批对象死去,少量存活,用复制算法