Java-虚拟机学习

阅读《深入理解Java虚拟机》记录概要

随笔记录点

Java虚拟机的内存区域

方法区

线程共享,存放着已被虚拟机加载的类信息、常量、静态常量、即使编译后的代码等数据

运行时常量池

属于方法区,存放着编译期间生成的各种字面量和符号引用

虚拟机栈

描述方法执行的内存模型,线程私有。即每个方法执行的时候会产生栈帧,栈帧中存储着局部变量表(基础类型、引用)、操作栈、动态链接、方法出口,一个方法的调用到执行就是一个栈帧在虚拟机栈中的入栈到出栈过程。

对于方法执行过程中线程请求栈的深度大于虚拟机允许的限度时会产生栈溢出异常(StackOverflowError),一般允许扩展深度,当内存不足无法申请到内存以扩展深度时则会发生内存溢出异常(OOM)。

本地方法栈

和虚拟机栈很类似,只不过是服务于本地方法(Native)作用。

线程共享,存放着对象实例,所有的对象和数组都要在堆上分配,也是GC管理的主要区域。方便GC,更为细致可以分为新生代、老年代。Eden、from、to,总之进一步的划分是为了更好的进行垃圾回收。

程序计数器

通过程序计数器来记录程序的执行位置,存储着指令的地址。

线程切换需要回到正确的执行位置,因此每个线程都有各自的程序计数器,它们之间互不影响,独立存储,线程私有。

垃圾回收

进行垃圾回收,需要解决下面三个问题。

  • 哪些是垃圾?
  • 什么时候回收?
  • 怎么回收?

哪些是垃圾——垃圾收集算法

通过算法识别哪些是可回收的对象。

引用计数法

即每个对象被引用了则计数加一,简单高效,但是存在两个对象相互引用的情况,这种情况下引用计数法失效。

根搜索法

通过一些名为“根对象”(GC Roots)进行向下搜索,搜索通过的路径称之为引用链,如果一个对象没有任何的引用链,即“根对象”到这个对象不可达,则证明这个对象是不在使用的,即可以判定为可回收的对象。

Java中“根对象”通常有以下几种:

  • 虚拟机栈中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中引用的对象

四种引用

通过扩展引用的的概念完成更多的功能,四种引用的引用强度依次减弱。

强引用

这个是我们平常见的最多的,通过Object obj = new Object();,这样的方式即类型 引用 = ...,此时的引用就是一个强引用,对于强引用来说,只要他还继续引用着对象,垃圾回收器就永远不会回收这个对象,宁可抛出内存溢出的异常。

软引用

软引用用于引用一些非必须,但是还有作用的对象,在内存不足的情况下,垃圾回收期会回去进行回收软引用的对象。这个可以应用于app某些缓存实现。

例如:

1
2
3
String string = "test";
SoftReference<String> sr= new SoftReference<>(string);
string = null;

sr就是一个软引用,而string是一个强引用,string赋予null,接下来test对象就只被sr软引用了,所以内存不足情况下会被回收。

弱引用

用于引用一些非必须的对象,引用强度弱于软引用,引用的对象会在下一次GC时被回收,使用WeakReference实现。

虚引用

这个几乎就和虚无缥缈一样,它无法获得实例,设置虚引用的目的为了在对象被回收时得到一个通知,使用PhantomReference实现。

回收or死亡

通过根搜索法判定的可回收对象,会进行一次标记,通过是否执行重写finalize方法和是否未执行来进行标记,没有标记的参与回收,重写且为执行过的对象将会在在一个低优先级的线程进行执行这个finalize方法,但是之后还是会面临回收。

方法区的回收

一般来说方法区的生存周期比较长,甚至可以称之为永久代,但是也是存在垃圾回收的。它所主要进行回收的目标则是废弃常量和无用的类。

到底怎么才算是一个无用的类的呢,其中需要满足三点:

  • 该类的所有实例被回收即堆不存在任何该类的实例对象
  • 加载该类的类加载器被回收
  • 该类的Class对象没有被引用,无法反射访问该类方法

满足之后也是可以回收,但是不是一定的。在动态代理、反射等场景下需要进行方法区的回收,保证方法去不会内存溢出。

怎么回收——垃圾回收算法

每个平台、以及不同的虚拟机都有各自不同的具体回收算法实现。其中大概有这么几个基础的思想:

标记-清除算法

如同名字一样非常好理解,先标记,再清除,非常简单,即把垃圾找出来清理就行了。但是这个算法实行起来有可能会产生很多的内存碎片而且效率不高,由于标记清除内存区域断断续续。后续如果有大对象进行申请内存,这个情况下可能就申请不到,就提前进行触发垃圾回收。

image-20200222113330567

复制算法

复制算法通过将内存一分为二,回收时将存活的对象放在其中一半,另一半直接一次性回收,这样的话就不会产生内存碎片,简单高效,但是代价也是有的,内存使用缩小成了原来的一半。

image-20200222113833374

现实使用中复制算法常用来进行回收新生代的对象,因为新生代中的对象98%是朝生夕死,所以不需要1:1的分配内存空间,而是分为一个较大的Eden空间和两个Survivor空间,fromto,每次只是用Eden和from空间,回收时,from中的某些对象年龄增长进入老年代,其他部分和Eden中的存活对象拷贝到to空间,然后清理刚才的Eden和from空间。接着to变成了新的from,重复这种方式,保持总有一个survivor空间是空的。

标记-整理算法

复制回收算法对于存活时间比较久的对象进行复制操作,效率将会低下,而且还浪费内存空间。所以说老年代一般不使用这种方法。而标记-整理很好的解决这个问题。

标记-整理即将所有的存活对象进行移动,设置一个边界,所有存活对象朝着这个边界内移动,对于边界外的区域进行回收

image-20200222114813969

分代收集算法

目前的主流虚拟机采用是分代收集,这种算法没啥新的思想,就是将根据对象存活周期划分内存区域,然后对于不同的区域进行采用不同的回收算法。一般分为新生代采用复制回收,老年代采用标记清理和标记整理。

Tips

  1. 对象优先再Eden分配

  2. 大对象直接进入老年代

  3. 长期存活的对象进入老年代

字节码结构

虚拟机类加载机制

类加载生命周期

加载、验证、准备、解析、初始化、使用、卸载,七个步骤

加载

1、读取二进制字节流

2、将字节码表示的类静态存储结构转换到方法去的数据结构

3、从Java堆中生成这个类的Class对象

而这个二进制流可以从本地字节码获取,从zip或jar中获取,或者从网络获取,甚至通过反射的方式动态获取。

验证

1、文件格式验证:字节流是否符合Class文件格式规范

2、元数据验证:字节码描述信息进行语义分析,保证符合Java语言规范

3、字节码验证:数据流和控制流的分析

4、符号引用验证:符号引用转化为直接引用时的信息匹配性

准备

为类变量分配内存和初始值,即静态变量,注意不是实例变量,实例变量是和对象一起在堆中分配的,静态常量是编译时常量也不在这个阶段分配。

解析

将常量池中符号引用转换为直接引用

初始化

类与加载器

两个类判断是否相等,要在是同一个类加载器前提下才有意义。

双亲委派模型

类加载器

  • 启动类加载器(Bootstrap ClassLoader):C++编写,负责加载<java_home>/lib以及-Xbootclasspath路径下的,比如rt.jar。
  • 扩展类加载器(Extension ClassLoader):由ExtClassLoader实现,负责加载<java_home>/lib/ext以及java.ext.dirs系统变量所指的路径下的类库,开发者可以直接使用。
  • 应用程序类加载器(Application ClassLoader):由AppClassLoader实现,一般也成为系统类加载器,负责加载用户路径(classpath)下的类库,默认我最常接触的也就是这个了。

双亲委派模型要求除了启动类加载器,其余的都要求有自己父类加载器,这里的父子关系并不一定是通过继承来实现,或者是组合实现,双亲委派模型的过程是一个类加载在加载类的时候会尝试请求父加载器来加载,如果父加载器无法完成,则交还给它,这样做的好处就是不会重复加载类

虚拟机字节码执行引擎

字节码的执行方式有两种:

1、解释执行(通过解释器)

2、编译执行(通过即时编译器产生本地码)

同时两种方式可能是兼顾使用的

运行时栈帧结构

栈帧时虚拟机进行方法调用和方法执行的数据结构,它是虚拟机栈的栈元素。

栈帧中存储了关于方法的相关信息:

1、局部变量表

2、操作数栈

3、动态连接

4、方法返回地址

5、以及一些附加信息

每一个方法的执行到结束就对应着一个栈帧的入栈和出栈,在程序执行过程中,程序执行到了某个方法,在虚拟机栈表现看来是该方法的栈帧位于了虚拟机栈的栈顶,称之为当前栈帧(Current Stack Frame),与之关联的就是当前方法了。

参看图例:

image-20200223160947482

局部变量表

局部变量以变量槽作为最小单位,简称Slot,一般来说Slot没有具体的大小,根据不同的平台,cpu,都会有所变化,但是可以明确的是它能够放下boolean、byte、char、short、int、floa、reference。用于存放记录方法中的局部变量。

操作数栈

这个就和编译原理里概念很相似的,程序的执行,从操作数栈中进行出栈入栈完成。

动态连接

每个栈帧包含了一个指向常量池中该栈帧所属方法的引用,Class文件的常量池存在非常多的符号引用,普通的调用方法,就是一种静态解析,这是直接引用。而另一部分在运行期间转化直接引用,这就是动态连接,比如我们多态实现,声明类型调用实际类型的实现。

方法返回地址

附加信息