请像对待每一行代码一样对待每篇文章 —- 鹰嘴豆
学习目标
开始学习
jvm虚拟机在执行java程序时它所管理的内存划分成几个数据区域,按照线程共享还是私有 分为方法区、堆(线程共享)和虚拟机栈,本地方法栈,程序计数器(线程私有)
程序计数器,这是线程私有,记录当前线程执行字节码时的行号。多核处理器同一时间只会执行一个线程中的指令,在线程切换过程中,这个程序计算器就是记录上一次线程切换时的指令执行位置。如果当前线程执行的是一个方法那么这个程序计数器记录的是正在执行的字节码指令的地址,如果执行的是native方法,这个程序计数器值为空
java虚拟机栈,这个也是线程私有,每个方法都会对应一个java虚拟机栈,这个栈记录的就是方法中的局部变量,操作数栈,动态连接,方法出口等
信息。一个方法从开始执行到结束对应的就是这个java虚拟机栈的出栈和入栈。我们经常会将java内存粗略的划分为堆和栈,这里的栈就是指的是Java虚拟机栈中的局部变量表,因此用堆和栈划分是很粗略的
局部变量表,存放的是编译期可以知道的基本数据类型、对象引用,局部变量表中的元素可能指向某个对象的指针也可能直接指向某个对象的句柄
64位的long和double分配的是2个局部变量空间,其他数据类型分配1个局部变量空间,局部变量表具体的大小在编译器就能确定 ,方法运行期间是不会改变这个局部变量表的大小
本地方法栈,它和java虚拟机栈的作用非常相似,区别在于java虚拟机栈为虚拟机执行java方法服务,本地方法栈为native方法服务,现在java所用的虚拟机Sun HotSpot将本地方法栈和虚拟机栈合二为一,有些虚拟机是划分成两块的
java堆,线程共享,在jvm启动时会创建这个java堆,它也是虚拟机所管理的内存中最大的一块,所有的对象实例和数组一般在这个java堆中分配。java堆是垃圾收集器管理的主要区域。根据不同的角度可以对java堆做下面几点划分来更好的内存回收
从内存回收角度来说,java堆划分成:新生代和老年代,具体又可以划分Eden空间,from survivor空间,to survivor空间
从内存角度来说可以划分多个线程私有的分配缓冲区(TLAB)
方法区,线程共享,主要存储的是class类信息,常量,静态变量,以及即时编译器编译后的代码等数据
- 运行时常量池,是方法区的一部分,一个class类文件除了包含类信息,字段、方法、接口等描述外还有一项信息就是常量池,用来存放编译时产生的各种字面量以及符号引用,在类加载后这些内容会存放在运行时常量池中。运行时常量池存放的内容不只是编译器产生的常量,也存放在运行期间产生的新的常量,比如String.intern()方法产生的常量
直接内存,这部分不是虚拟机内存数据区的一部分,NIO中的通道和缓冲区的I/O方式,直接通过native函数库直接在堆外分配堆外内存,然后在java堆中的DirectByteBuffer对象作为这个堆外内存的引用,提高性能
对象的创建过程(以Sun HotSpot虚拟机为例)
遇到new指令是从常量池中查找这个指令的参数,判断是否能定位到一个类的符号引用,如果定位到了表示之前这个类已经加载,解析和初始化过了,如果没有定位到就需要先进行类的加载过程
类加载完以后,需要给新生的对象分配内存,这个对象的大小可以在类加载的时候确定。给对象分配内存有两种方法
一种是指针碰撞(所使用的内存和空闲内存排列两边,中间有一个指针分割),只要往空闲的内存移动这个指针到对象相等的距离即可。
另外一种就是空闲列表,虚拟机维护一个列表存放所有空闲的内存块,只要找到合适这个对象大小的内存分配对象实例即可
在多线程下这两种分配都会导致线程不安全,所采用做法就是CAS自旋保证原子性,或者给每个线程在堆中分配不同的线程私有的分配缓冲(Thread Local Allocation Buffer),分配完以后再同步锁定
对象分配好以后,对象所用的内存空间初始化为零
虚拟机给对象设置对象头,保存类的元数据、对象哈希码,GC分代年龄等信息
对象的初始化,执行
方法
对象存储空间分成:对象头、实例数据、对齐填充。对象头保存的是哈希码、GC分代年龄,锁状态标志,线程持有的锁,偏向线程ID、偏向时间戳,长度是32位或者64位
java栈中的对象引用,是如何访问堆中创建的对象的,有两种方式
句柄,在堆内存中创一个句柄池,对象引用指向的是句柄,而在句柄中存放的是对象实例数据和对象类型数据
直接指针,对象引用指向的是在堆中创建的对象地址,而堆中的对象存在一个指向对象类型数据的指针方法方法区的对象类型
Sun HotSpot采用的是直接指针的访问,两者区别在于直接指针速度快,句柄保证对象引用的是稳定的句柄(对象被移动时不会改变对象引用所指向的句柄,而改变的是句柄的实例数据指针)
垃圾回收
垃圾回收器是自动化,什么时候需要理解垃圾回收机制? 当排查内存溢出,内存泄漏,垃圾回收成为系统瓶颈的时候需要好好的考虑优化
判断对象存活有两种算法
引用计数算法,给对象添加一个引用计数器,当对象被引用时计数器+1,当引用失效时计数器-1,这种算法高效但是不能解决循环引用问题
可达性分析算法,java中将虚拟基栈所引用的对象、方法区的静态变量所引用的对象、方法区常量所引用的对象,本地方法栈中的native方法引用的对象作为GC Roots, 只要GC Roots不能到达某个对象,就表示这个对象是可回收对象,包括这个对象下面引用的对象都是可回收对象
引用分类, 引用如果仅仅分成被引用和不被引用两种太过狭隘,jdk提供更多的分类
强引用,只要引用存在永远不会被GC回收
软引用,在内存溢出之前回收掉软引用对象,如果回收之后还没有足够空间才抛出内存溢出异常
弱引用,存活到下次垃圾回收,不管内存是否充足都会被回收弱引用对象
虚引用,目的是对象被回收时收到一个通知
对象不存在引用意味着被GC回收,当时这种回收不是立即执行而是被判缓刑,我们可以在重写finalize方法,并在方法中重新给对象关联引用,这时候对象就不会被回收了
对象创建优先发生在新生代的Eden,如果是大对象则直接进入老年代,当Eden没有足够空间分配时虚拟机会发起一次Minor GC,在MinorGC发生时,如果Eden中的对象能在两个survivor内存空间中存放就会被移动到Survivor空间中,并没有被回收就给对象的年龄增加1,如果对象年龄超过一定阀值就会移动到老年代。如果Eden对象大于Survivor容纳的大小,则直接移动到老年代中。
java虚拟机提供的一些命令作为线上问题的排查
jps,查询所有虚拟机执行的主类的名称以及唯一ID,用ps也能查询本地虚拟机的唯一ID,但是如果存在多个虚拟机就得使用jsp所执行的主类名称来判断唯一ID
jstat,统计虚拟机运行的状态信息,包括,类装载、内存、垃圾收集、JIT编译等数据
jinfo,实时查看和调整虚拟机各项参数
jmap,内存映射工具,可以获取内存dump文件,还可以获取finalize执行队列,java堆和永久代信息
jhat,将jmap生成的dump文件用jhat分析,并生成一个http服务器,在网页上显示具体分析的结果,以包的形式展示
jstack,生成当前时刻的线程快照,根据jstack可以查看各个线程的调用堆栈,就可以知道没有响应的线程在后台做什么事情
jdk可视化工具,VisualVM
在jvm虚拟机中可以运行其他语言,都是通过对应语言的编译器编译成class字节码文件放到java虚拟机中运行
class文件格式采用无符号数和表组成的伪结构(类似于c中的结构体),来作为数据存储的基本类型
无符号数,用u1, u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的数据类型,可以用来描述,数字,索引引用,数量值,utf8编码构成的字符串值
表里面是无符号数和其他表复合组成,复合就是意味着可以描述class的层次结构的信息(复合中套复合,层次排列存储class信息)
表是以命名以_info为结尾,class文件格式可以用一张有无符号数和表组成的组合表表示,它们的顺序中的字节代表的含义都是严格限定的,不允许改变,如下图
4个字节的魔术来确定是那种class是否是虚拟机接收文件,主要考虑安全不被修改,扩展名是可以被外部修改,里面的class信息是不能被修改的
次版本号和主版本号,java版本号从45开始,java1.1之后每个大版本就是在45的版本号加一,jdk6就是50,jdk7就是51,jdk8就是52,这52表示的就是主版本号的数值,高版本的jdk可以兼容执行低版本编译的class,但是不能执行比它更高jdk版本编译的class
常量池,class文件的资源仓库,占用class文件空间最大的数据项目之一,常量池数量不固定需要用2个字节的无符号数表示常量池数量,这个计数不是从0开始而是从1开始,主要空出0表示字段不索引任何一个常量池项目。而其他接口索引集合、字段表集合,方法表集合索引从0开始。常量池主要存放:字面量、符号引用。
符号引用,jvm虚拟机在javac编译class时不经过像c那样的连接过程,而是在jvm加载class时动态连接,也就是说jvm在编译class是保存的只是符号引用,在jvm运行时才能从常量池里面获取对应的符号引用,得到真正的内存地址
符号引用有三类常量,类和接口的全限定名、字段名称和描述符、方法名称和描述符
utf8缩略编码 有三个范围,\u0001-\u007f之间字符用1个字节,\u0080 - \u07ff用2个字节, \u0800 - \uffff范围的字符用3个字节(也就是普通的utf8编码)
访问标志位是在常量池后面,描述类的访问限定,可以通过|来进行访问限定的组合
类索引、父类索引、接口索引集合,分别表示当前命名,继承的父类以及实现的接口集合
字段表集合,用一个无符号数表示字段的长度,和一个字段表存储接口或者类中声明的变量,字段包含类级变量以及实例变量
接口必须包含public、static、final三个访问标志
public、private、protected互斥
final、volatile互斥
全限定名,表示包+类名称;简单名称,表示没有类型或者参数修饰的方法名称和字段名称。描述符,描述字段数据类型、方法的参数列表(数量、顺序、类型)和返回值
用单个基本类型的首字母大写来表示基本类型
对象类型用字符L+对象全限定名来表示
数组类型在每一维度用前置的[表示例如,String[][]用:[[Ljava/lang/String, int[]用:[I表示
用描述符描述方法时按照先参数列表后返回值顺序描述
参数列表按照参数顺序放置()中描述
返回值放置()后描述
void inc(int[], d)可以描述成([ID)V
java语言字段不能重载,不管数据类型、修饰符是否相同,必须使用不同名称,对于字节码来说,如果两个字段描述符不一致,字段重名合法
方法表集合,表包含无符号数:访问标志、名称索引、描述符索引,也组合表:属性表集合
java方法中的代码经过编译会存放到属性表的一个名为Code的属性中
描述符指向常量池的utf8编译的字符串的索引
属性表集合,在class文件、字段表、方法表中都可以携带自己的属性表集合
Code属性,方法体中的代码经过jvm编译以后存放在这个Code属性中,接口和抽象类中的方法不存在这个Code,其他方法都放在这个Code中。Code属性有自己的结构,如max_statck 、max_locals、code_length、code(表示jvm一个字节的指令,如load_0 、 return等指令)、
slot是虚拟机为局部变量分配内存空间最小的单位,double/long用两个slot表示,其他类型用1个slot表示,包括对象引用类型
方法参数、异常处理器参数(catch块定义的异常),方法体中定义的局部变量都需要使用局部变量表存放
jvm指令有存放第一个局部变量表,第二个局部变量表 …. 第四个局部变量表,对应的指令有iload_1:将第二个局部变量表的数据推送到栈顶, istore_1将栈顶数据保存到第二个局部变量表中。 第1个局部变量表预留保存的是this(iload_0表示第一个局部变量表),这里面的局部边变量表指的就是栈中的局部变量表(jvm虚拟机栈)
Exceptions属性
列举方法可能抛出的受检查异常
它的属性表中有一个属性是exception_index_table表,里面指向的是CONSTANT_Class_info常量的索引
LineNumberTable属性,描述java源代码行号与字节码行号之间的对应关系,可以在抛出异常的时候看到具体对应的源码的行号
LocalVariableTable属性
jvm虚拟机栈中的局部变量表中的变量与java源码中定义的变量的一个映射 。如果java编译时使用-g:none取消那么,别人调用这个方法是,所有参数名称都是丢失的,只会用arg0,arg1表示
属性结果中有个local_variable_info表存储局部变量的生命周期,名字的CONSTANT_UTF8_INFO的常量池索引,以及在局部变量表中的slot的位置
SourceFile记录class文件的源码文件名称
ConstantValue属性
通知虚拟机自动为静态变量赋值
非static变量在实例构造函数
中赋值,static final变量且数据类型是基本类型或者String的话用ConstantValue赋值,否则使用 方法赋值
InnerClasses属性
- 记录内部类与宿主类之间的关联
Deprecated及Synthetic属性
- 记录@deprecated的类、字段或者方法
Signature属性
- java泛型实现是通过擦除来实现伪泛型,在Code属性字节码中,泛型信息编译之后,类型变、参数化类型都会被擦除,运行期间这些字节码通过反射是不能获取到这些泛型信息的。Signature就是弥补这个缺陷,记录这些擦除的泛型的参数化类型
字节码指令
字节码指令中的本地变量指的是什么?
1个字节组成的操作码和操作数
加载和存储指令用于数据在栈帧中的局部变量、操作数栈之间的来回传输,具体的图还需要彻底理解才能画
类加载,其它语言的连接工作发生在编译器,java的加载、连接、初始化工作发生在程序运气期间,这样的好处在于,提高灵活性,在运行时再指定具体实现类
类的生命周期包括:加载,连接(验证、准备、解析)、初始化、使用、卸载
有五种情况必须进行类的初始化
new 、getstatic、putstatic invokestatic遇到这四条指令是,如果类没有进行初始化就需要先触发其初始化(加载、连接当前在初始化前完成),概括来说就是使用new关键字实例化对象、读取或者设置一个类的静态变量、调用一个类的静态方法。对于静态字段,只有直接定义这个字段的类才会被初始化,例如子类调用父类的静态字段,只有父类才会被初始化
使用java.lang.relect包的方法对类进行反射,如果类没有被初始化,则需要对该类先进行初始化
当初始化一个类的时候,发现其父类没有进行过初始化,则需要先触发对其父类的初始化
当虚拟机启动,用户需要zhiding一个main函数所在的类,虚拟机会先初始化这个类
jdk1.7支持的动态语言,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时,且这个方法句柄对应的类没有进行过初始化,则需要先初始化
new SuperClass[10];它不会触发SuperClass的初始化,但是会触发[LSuperClass的类的初始化,这个类是虚拟机自动生成、直接继承与Object的子类,创作动作有字节码指令newarray触发
加载,类加载的一个阶段,通过类的全限定名获取二进制的字节流,将字节流的静态存储结构转换为方法区运行时的结构,生成一个代表这个类的对象作为方法区类各种数据的访问入口
类准备阶段,类属性变量在方法区中分配内存,并初始化,初始化的值为0,赋值指令发生在类构造器方法内
解析阶段,解析方法区中的常量池内的符号引用替换成直接引用的过程,直接引用直接指向对象的指针
初始化,使用
,有编译器自动收集类中所有类变量赋值,或者静态块的语句按顺序合并产生 , 不需要显示调用父类构造器,在执行子类构造器之前就已经保证执行好父类构造器了,这和实例构造器有所不同( )。先执行父类构造器再执行子类构造器
类的加载放到jvm虚拟机外处理,用户可以自定义类加载器去加载类的二进制字节码,加载器存在命名空间,只有同一个加载器加载的类才有可能相等
类加载器的双亲委派模型,从java虚拟机角度来说,有一种启动类加载器(Bootstrap ClassLoader)由c++语言实现,在java中是native方法,另外一种加载器就是由java实现独立在虚拟机外部,并都是继承自java.lang.ClassLoader
启动类加载器,加载
\lib目录中能被虚拟机识别的的类库到虚拟机内存中 扩展类加载器,加载
\lib\ext目录中的所有类库 应用程序类加载器,是ClassLoader.getSystemClassLoader()的返回值,也叫系统类加载器,负责加载用户类路径上的所指定的类库
一些自定义的类加载器
双亲委派模型
一个加载器接收到加载请求时,会先委派给父加载器加载,每一层的加载器都是如此,一直到启动类加载器去加载资源,如果父加载器加载不到资源,子加载器才会尝试自己去加载
使用双亲委派模型能够防止一些系统类库被自定义的类加载器加载引起混乱
什么是虚拟机
虚拟机是相对物理机而言的概念,物理机的执行引擎是直接建立在处理器,硬件,指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的,自定义指令集,能够执行不被硬件直接支持的指令集格式
虚拟机字节码执行引擎的概念模型是所有java虚拟机执行引擎的统一外观
学习总结
slot的理解
局部变量的最小的内存储存单位,基本类型中除了long和double的slot长度是两个,其他一律是一个slot
加载和存储指令用于数据在栈帧中的局部变量、操作数栈之间的来回传输
贡献者列表
编辑人 | 编辑时间 | 编辑内容 |
---|---|---|
鹰嘴豆 | 2019/2/21 | 初稿 |