查看了张秀宏写的自己动手写Java虚拟机 Write your own Java virtual machine,对java底层有了新的认识,将笔记整理一下
Java代码编译和执行的整个过程包含了以下三个重要的机制:
- 编译机制(编译器内):主要是编译器将.java文件转为.class字节码文件。详细都包括:1.分析和输入到符号表。2.注解处理。3.语义分析和生成class文件。
- 类加载机制(jvm虚拟机):是通过ClassLoader及其子类来完成的。.class文件---->JIT编译器---->目标代码
- 类执行机制(jvm虚拟机):JVM是基于栈的体系结构来执行class字节码的。线程创建后,都会产生程序计数器(PC)和栈(Stack),程序计数器存放下一条要执行的指令在方法内的偏移量,栈中存放一个个栈帧,每个栈帧对应着每个方法的每次调用,而栈帧又是有局部变量区和操作数栈两部分组成,局部变量区用于存放方法中的局部变量和参数,操作数栈中用于存放方法执行过程中产生的中间结果。
java运行区域内存
一、运行时数据区域
java程序在执行的过程中会吧他所管理的内存划分为不同的区域,各有各的用途、创建时间、销毁时间;有的随着进程的启动而存在,有的依赖用户的线程的启动和结束而创建和销毁运行时数据区分为两种大区域:由所有数据共享的数据区和线程隔离的数据区(每个线程都有一个独立的区域,各个县城之间互不干扰);
1.程序计数器:
线程隔离区的一块较小的内存区域,可以看做是当前线程字节码的行号指示器,是唯一一个没有OutOfMemoryError的区域(虚拟机的概念模型中,字节码解释器工作就是通过这个计数器的值来选取下一条要执行的字节码指令)
2.java虚拟机栈:线程隔离区内生命周期与线程相同,
虚拟机栈描述的是java方法执行的内存模型:每个方法在执行的同时都会创建一个针栈用于存储:局部变量表、操作数栈、动态链接、方法出口等信息。每个方法从调用到执行完成的过程,就对应着一个栈针在虚拟机栈中入栈到出栈的过程
局部变量表:
存放了编译器可知的各种基本数据类型(doublean,byte,char,short,int,float,long,double、reference(对象引用类型;不等于对象本身,可能是一个指向对象的指针地址,也可能是一个代表对象的句柄或其他与此对象相关的位置)、returnAddress类型(指向了一条字节码指令的地址))
注:
1.double,long会占用两个局部变量空间(slot),其余的数据类型只占一个
2.局部变量表所需的空间在编译期间完成分配,在运行时,所需空间多少是确定的。
3.异常:
线程请求栈深度大于虚拟机所允许的深度:StackOverflowError
扩展是无法申请到足够的空间:OutOfMemoryError
3.本地方法栈:线程隔离区内与虚拟机栈类似,为虚拟机使用到的Native(本地的)方法服务;
注:
1.虚拟机规范并未对本地方法栈有强制的要求,故有的虚拟机就将本地方法栈和虚拟机栈合二为一如:HotSpot就合二为一;
2.异常:
线程请求栈深度大于虚拟机所允许的深度:StackOverflowError
扩展是无法申请到足够的空间:OutOfMemoryError
4.Java堆:是虚拟机管理的内存最大的一个区域,被所有线程所公用,虚拟机启动的时候创建;
a、几乎所有的对象实例(数组)存放在堆中;
b、垃圾管理的主要区域,亦称GC堆,
c、因垃圾分代收集方法,还可细分为新生区和老年区等;
d、线程共享的java堆,在线程私有的线程使用时会划出线程私有的分配缓冲区;
注:
1.java堆可处于物理上不连续的内存空间中,只要逻辑上连续即可;
2.无论如何划分,目的为更好的回收内存,更好得到分配内存;
3.异常:
扩展是无法申请到足够的空间:OutOfMemoryError
5.方法区:是线程共享的内存区域,用于存储已经被虚拟机加载的类的信息、常量、静态变量、即时编译器编译后的代码等数据。(在堆区,又非堆)
a、HotSpot虚拟机将这里做成了永久区,团队将GC分代收集方法扩展到了这里,可以像java堆一样管理这片区域
注:
1.异常:
扩展是无法申请到足够的空间:OutOfMemoryError
6.运行时常量表:方法区的一部分,Class文件的常量池(Class文件中的信息:类的版本、字段、方法、接口、常量池),
a、用于存放编译期生成的各种字面量和符号引用,将在类加载后进入方法区;
b、并非一定在编译时产生的,在运行时也可能将新的常量放入常量池中;
注:
1.异常:
扩展是无法申请到足够的空间:OutOfMemoryError
7.直接内存:
并不是虚拟机运行时数据区的一部分,在jdK1.4之后引入了基于通道与缓冲区的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在java堆中的directByteBuffer对象对这块内存的引用进行操作。
a、这块内存不会受到java堆内存大小的限制;
b、会受到本机总内存大小的限制
注:
1.异常:
如果各个区域内存之和大于物理内存限制,会导致动态的:OutOfMemoryError
二、内存异常:
1.栈深度异常:StackOverflowError 线程请求栈深度大于虚拟机所允许的深度
2.内存溢出:out of memory 内存溢出异常通俗来讲就是内存不够
3.内存泄漏:memory leak 无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放
发生原因:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露
内存泄漏可以分为4类:
1)常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
2)偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
3)一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
4)隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
三、垃圾收集:Minor GC 年轻代空间GC,Major GC 是清理老年代,Full GC 是清理整个堆空间—包括年轻代和老年代。
垃圾收集三个问题:1、那些内存回收,2、什么时候回收,3、如何回收
1、判断对象已死
a、计数法:对象中加一个计数器,每当有个对方使用时就加一;当引用失效时就减一
b、可达性算法:通过一系列的称为“GC Root”的对象作为起点,从这些起点作为节点开始向下搜索,搜索走过的路程称为引用链,当一个对象到GCroot没有任何引用链相连接的时候,则证明此对象不可用,GC Root对象包括:虚拟机栈引用的对象,方法区中静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象
2、对象生存还是死亡:判断对象已死后,对象并非非死不可,要宣告对象死亡还需经过两个标记过程;
第一次标记:如果对象在进行可达性分析的时候,发现没有雨GC ROOT相连接的引用链就会第一次标记,并进行一次筛选;筛选的条件是此对象是否有必要执行finalize()方法,当finalize()方法没被重写或者已经调用就被视为没必要执行若判断之必要执行finalize()方法的时候,就将对象放到F——Queue的队列中,并有虚拟机自动决定优先级执行finalize()方法(虚拟机并不确定将这个方法完整执行完成)
第二次标记:finalize()执行完成后就会对对象进行第二次标记
注:
finalize()方法只能执行一次,也就是说对象只能在这里自救一次,最后还是消亡;
3、垃圾回收算法:
a、标记-清除算法:首先标记处需要清除事物对象,在标记后统一回收所有被标记的对象;
不足:
1、标记和清除两个效率都不高
2、清理完成后产生大量连续不断的内存碎片
b、复制算法:将内存分为大小相同的两块,当一块用完后将还存活的对象复制到另一块中,然后再把已经使用过的那一块清除掉;
优势:
1、原理清晰,操作简单,运行高效;
不足:
1、这种算法将原有内存缩小为一半,有点浪费;(每次消亡的对象都占大多数,最后存活下来的仅有2%)
c、标记-整理算法:标记后让所有存活的对象相一端移动,然后直接清掉边界外的内存;
d、分代收集算法:根据对象存活事的周期不同将内存分为几块,一般分为新生代和老年代。
新生代:新生代:每次垃圾收集都会有大量的对象死亡,只有少量存活,选用复制法;
老年代:老年代:老年代因为对象存活率高,没有额外空间对他担保,就采用标记-整理法进行回收,
四、内存分配与回收策略:
1、对象优先在Eden(新生区)分配
2、大对象直接直接进入老年代
3、长期存活的对象将进入老年代:
虚拟机给每个对象定义了一个对象年龄计数器;如果对象在新生区产生经过一次GC存活,并且能被Survivor(幸存者)容纳,就会移到Survivor(幸存者)空间中,并且对对象年龄设为1,每GC一次就加一,当对象在Survivor(幸存者)熬过一定程度就会被移到老年代,
4、动态对象年龄判定:
如果Survivor(幸存者)空间中相同年龄所有对象大小的总和大于或等于Survivor(幸存者)空间的一半,大于或等于该年龄值的对象直接进入老年代。
5、空间分配担保:
在发生MinorGC(新生代GC)之前会先检查老年代最大的可用你空间是否大于新生代对象的总空间,若成立则GC.不成立则,检查老年代最大可用空间是否大于历次晋升到老年代的平均大小;大于则新生代GC;小于则Full GC
类加载过程:加载---》验证---》准备---》解析---》初始化
加载:
1:根据类的全路径获取字节流
2:将字节流代表的静态存储结构转化为方法区的运行时数据结构
3:在内存中生成一个代表这个类的java.lang.Class对象,作为方法去这个类的各种数据访问接口
验证:
1:目的是为了确保class文件的字节流中包含的信息符合当前的虚拟机的要求,并且不会危害虚拟机的安全
2:四个阶段:文本验证(文本是否符合Class规范)、元数据验证(字节码描述内容进行语义分析)、字节码验证(通过数据流控制流分析确定程序语义是否合法、逻辑是否符合要求)、符号引用验证(对类自身以外的信息(常量池中的各种符号)进行匹配校验)
准备:(正式为类分配内存,并设置类变量的初始值(所有变量所使用的内存在方法区中进行分配)
1:这里的变量为类变量,即被static修饰的变量;(实例化变量将会在实例化时随着对象一起分配在java堆中
2:这里的初始化通常情况下是数字类型的零值,而不是定义的值;(真正的赋值是在执行了类构造器
3:这时候还未执行任何方法;(如果为final修饰,直接赋值为定义值)
解析:(虚拟机将常量池中的符号引用转化哪位直接引用(指针、相对变量、句柄)的过程)
对象探秘
一、对象创建过程:虚拟机创建的过程
创建对象(new、反序列化、克隆)---------->检查这个指令的参数能否在常量池中定义一个类的引用(否则进行类的加载)-------(是)--->虚拟机为新生对象分配资源---------->分配到的空间初始化值(初始化为零值,
二、对象的内存布局
1、对象头:存放在方法区中
a、存储对象的运行时数据(MarkWord);哈希吗(25bit)、GC分代年龄(4bit)、锁状态标识(2bit)、线程拥有的锁()、偏向线程id()、偏向时间戳()
b、类型指针;即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个类是哪个对象的实例
c、如果对象为java数组还必须有一块存储java数组的长度,
2、实例数据:对象真正存储的有效信息,父类中定义的变量会出现在子类之前
3、对象填充:并不是必然发生,仅仅是起着占位符的作用,(因为虚拟机要求对象存储的起始地址必须是8的整数倍,没有占满的就要补齐)
三、对象的访问定位:Java程序需要通过栈上的reference数据来操作堆上的对象
1、访问方式分为句柄访问和指针直接访问两种:
a、句柄访问:java堆中划分出来一个句柄池,reference存储的是句柄的地址,而句柄中存储的是对象实际数据(实例区)和类型数据(方法区)各自的地址
b、指针直接访问:java堆对象的布局中必须存放数据类型相关的地址,reference直接存储的是对象的地址