jvm笔记

在这里插入图片描述

1. JDK、JRE、JVM的区别与联系.

JDK: java development kit, java开发工具包,针对开发者,里面主要包含了jre, jvm, jdk源码包,以及bin文件夹下用于开发,编译运行的一些指令器。
JRE: java runtime environment, java运行时环境,针对java用户,也就是拥有可运行的.class文件包(jar或者war)的用户。里面主要包含了jvm和java运行时基本类库(rt.jar)。rt.jar可以简单粗暴地理解为:它就是java源码编译成的jar包(解压出来看一下),用eclipse开发时,当你ctrl点击发现不能跳转到源文件时,需要把rt.jar对应的源码包加进来,而这里的源码包正是jdk文件夹下的src.zip。
JVM:   就是我们常说的java虚拟机,它是整个java实现跨平台的最核心的部分,所有的java程序会首先被编译为.class的类文件,这种类文件可以在虚拟机上执行。

也就是说class并不直接与机器的操作系统相对应,而是经过虚拟机间接与操作系统交互,由虚拟机将程序解释给本地系统执行。

只有JVM还不能成class的执行,因为在解释class的时候JVM需要调用解释所需要的类库lib,而jre包含lib类库。

JVM屏蔽了与具体操作系统平台相关的信息,使得Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

JDK和JRE区别: 去bin文件夹下你会发现,JDK有javac.exe而JRE里面没有,众所周知javac指令是用来将java文件编译成class文件的,这是你开发去做的事,用户是不会去做的。JDK还有jar.exe, javadoc.exe等等用于开发的可执行指令文件。这也证实了一个是开发环境,一个是运行环境。
JRE和JVM区别: 有些人觉得,JVM就可以执行class了,其实不然,JVM执行.class还需要JRE下的lib类库的支持,尤其是rt.jar。

2. JVM-类加载机制.md

在这里插入图片描述

概述

虚拟机把描述类的数据从CLass文件加载到内存,并对数据进行校验,解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制(懒加载)。

类加载过程

加载–连接–初始化–使用–卸载

1. 加载(Loading)

  • 通过一个类的全限定名来获取定义此类的二进制流
  • 将这个字节流锁代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口。
1.1 加载源

(1)从本地系统直接加载
(2)通过网络下载.class文件
(3)从zip,jar等归档文件中加载.class文件
(4)从专有数据库中提取.class文件
(5)将Java源文件动态编译为.class文件(服务器)

2. 连接

2.1 验证

验证是连接的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全

2.1.1 文件格式验证

(1)是否以魔数0xCAFEBABE开头。
(2)主、次版本号是否在当前虚拟机处理范围之内。
(3)常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
(4)指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
(5)CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
(6)Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

2.1.2 元数据验证

(1)这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)。
(2)这个类是否继承了不允许被继承的类(被final修饰的类)。
(3)如果这个类不是抽象类,是否实现了其父类或接口之中所要求实现的所有方法。
(4)类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。

2.1.3 字节码验证

主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:
(1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
(2)保证跳转指令不会跳转到方法体以外的字节码指令上。
(3)保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。

2.1.4 符号引用验证

符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:
(1)符号引用中通过字符串描述的全限定名是否能够找到对应的类。
(2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
(3)符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。

2.2 准备

为类变量分配内存并设置变量的初始值,这些变量使用的内存都将在方法区中进行分配。

整体类型是:
  • byte,其值为8位有符号二进制补码整数,其默认值为零

  • short,其值为16位有符号二进制补码整数,其默认值为零

  • int,其值为32位有符号二进制补码整数,其默认值为零

  • long,其值为64位带符号的二进制补码整数,其默认值为零

  • char,其值为16位无符号整数,表示基本多语言平面中的Unicode代码点,使用UTF-16编码,其默认值为空代码点('\u0000'

浮点类型是:
  • float,其值是浮点值集的元素,或者,如果支持,则为float-extended-exponent值集,其默认值为正零

  • double,其值是double值集的元素,或者,如果支持,则为double-extended-exponent值集,其默认值为正零

所述的值boolean 类型编码的真值truefalse,并且缺省值是false

参考类型和值

有三种reference 类型:类类型,数组类型和接口类型。它们的值分别是对动态创建的类实例,数组或类实例或实现接口的数组的引用。

数组类型由具有单个维度的 组件类型(其长度不是由类型给出)组成。数组类型的组件类型本身可以是数组类型。如果从任何数组类型开始,考虑其组件类型,然后(如果它也是数组类型)该类型的组件类型,依此类推,最终必须达到不是数组类型的组件类型; 这称为数组类型的元素类型。数组类型的元素类型必须是基本类型,类类型或接口类型。

reference值也可以是专用空引用的,没有对象的引用,这将在这里通过来表示null。该null引用最初没有运行时类型,但可以转换为任何类型。reference类型的默认值是null

该规范不要求具体的值编码null

2.3 解析

解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。

解析对象包括:

2.3.1 类或者接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的引用,那虚拟机完成整个解析过程需要以下3个步骤:
(1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。
(2)如果C是一个数组类型,并且数组的元素类型为对象,那将会按照第1点的规则加载数组元素类型。
(3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为了一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具有对C的访问权限。如果发现不具备访问权限,则抛出java.lang.IllegalAccessError异常。

2.3.2 字段解析

首先解析字段表内class_index项中索引的CONSTANT_Class_info符号引用,也就是字段所属的类或接口的符号引用,如果解析完成,将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。
(1)如果C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(2)否则,如果C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(3)否则,如果C 不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(4)否则,查找失败,抛出java.lang.NoSuchFieldError异常。

2.3.3 类方法解析

首先解析类方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个类方法所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续类方法的搜索。
(1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C 是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)如果通过了第一步,在类C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,在类C实现的接口列表以及他们的父接口中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在相匹配的方法,说明类C是一个抽象类这时查找结束,抛出java.lang.AbstractMethodError异常。
(5)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。

2.3.4 接口方法解析

首先解析接口方法表内class_index项中索引的CONSTANT_Class_info符号引用,也就是方法所属的类或接口的符号引用,如果解析完成,将这个接口方法所属的接口用C表示,虚拟机规范要求按照如下步骤对C进行后续接口方法的搜索。
(1)与类解析方法不同,如果在接口方法表中发现class_index中的索引C是个类而不是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
(2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(3)否则,在接口C的父接口中递归查找,直到java.lang.Object类(查找范围包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
(4)否则,宣告方法查找失败,抛出java.lang.NoSuchMethodError。

3. 初始化

  • 遇到newgetstaticputstaticinvokestatic这四个字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这四条命令的最常见的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
  • 使用反射对类进行调用,如果该类没有进行初始化 ,则需要先触发其初始化
  • 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要制定一个需要执行的主类,即:包含main方法的类。虚拟机会先初始化这个类。
不能被初始化的例子
  • 通过子类引用父类的静态字段,子类不会被初始化
  • 通过数组定义来引用类
  • 调用类的常量(常量在编译阶段就存入调用类的常量池中了)
示例1:
public class Fantj {
    public static int high = 180;
    static {
        System.out.println("静态初始化类Fantj ");
        high = 185;
    }
    public Fantj(){
        System.out.println("创建Fantj 类的对象");
    }
}
public class Main {
    public static void main(String[] args) {
        Fantj fantj = new Fantj();
        System.out.println(fantj.high);
    }
}

控制台打印:

静态初始化类Fantj 
创建Fantj 类的对象
185
  1. jvm加载Main类,首先在方法区生成Main类对应的静态变量静态方法常量池代码等,同时在堆里生成Class对象(反射对象),通过该对象可以访问方法区信息,类Fantj也是如此。
  2. main方法执行,一个方法对应一个栈帧,所以Fantj压栈,一开始fantj 是空,Fantj压栈的同时堆中生成Fantj对象,然后把对象地址交付给fantj,此时fantj 就拥有了Fantj对象地址。
  3. fantj.high 来调用方法区的数据。

好了,试试静态方法。给Fantj类加个方法:

    public static void boss(){
        System.out.println("boss静态方法初始化");
    }
public class Main {
    public static void main(String[] args) {
//        Fantj fantj = new Fantj();
//        System.out.println(fantj.high);
        Fantj.boss();
    }
}
静态初始化类Fantj 
boss静态方法初始化

说明了调用静态方法没有对类进行实例化,所以静态类加载会被初始化。

4. 类加载器

JVM的类加载是通过ClassLoader及其子类来完成的,虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

4.1 启动类加载器(Bootstrap ClassLoader):

负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。

4.2 扩展类加载器(Extension ClassLoader):

负责加载 JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统变量指定路径中的类库。

4.3 应用程序类加载器(Application ClassLoader):

负责加载用户路径(classpath)上的类库。

4.4 自定义类加载器
  • 高度灵活
  • 实现热部署
  • 代码加密

JVM通过双亲委派模型进行类的加载,当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

一个小Demo
/**
 * 只加载当前包下的类,不是当前包下的交给上面的类加载器
 * Created by Fant.J.
 */
public class MyClassLoader {
        ClassLoader classLoader = new ClassLoader() {

            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                //拿出类的简单名称
                String filename = name.substring(name.lastIndexOf(".") + 1) + ".class";

                InputStream ins = getClass().getResourceAsStream(filename);

                if (ins == null){
                    return super.loadClass(name);
                }
                try {
                    byte [] buff = new byte[ins.available()];
                    ins.read(buff);
                    // 将字节码转换成类对象
                    return defineClass(name,buff,0,buff.length);
                } catch (Exception e) {
                    //
                    throw new ClassNotFoundException();
                }
            }
        };
}

在自定义的加载器中,将本包以外的类的加载工作都交给父类加载器:return super.loadClass(name);

public abstract class Test {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader myClassLoader = new MyClassLoader();
        Object o = myClassLoader.classLoader.loadClass("com.jvm.classload.Son").newInstance();
        System.out.println("Son类对象:"+o.getClass());
        System.out.println("Son父类对象:"+o.getClass().getSuperclass());

        Object c = myClassLoader.classLoader.loadClass("com.jvm.classload.MyClassLoader").newInstance();
        System.out.println("c对象的类加载器:"+c.getClass().getClassLoader());
        System.out.println("myClassLoader对象的类加载器:"+myClassLoader.getClass().getClassLoader());
    }
}

控制台输出:

Son类对象:class com.jvm.classload.Son
Son父类对象:class com.jvm.classload.Parent
c对象的类加载器:com.jvm.classload.MyClassLoader$1@4dc63996
myClassLoader对象的类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2

我们可以发现,同样是MyClassLoader 这个对象,但是他们的类加载器不同,那他们就是不同的对象。

拓展

  1. 通过一个类的全限定名来获取描述此类的二进制字节流
  2. 只有被同一个类加载器加载的类才可能会相等。相同的字节码被不同的类加载器加载的类不相等。
  3. 类的加载,会将其所有的父类都加载一遍,直到java.lang.Object。(因为Object是所有类的父类)

5. 双亲委派模型

通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

各个类加载器之间的层次关系被称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其余的类加载器都应该有自己的父类加载器,而这种父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)。

双亲委派模型是Java设计者推荐给开发者的类加载器的实现方式,并不是强制规定的。大多数的类加载器都遵循这个模型。

其实,该模型就是防止内存中出现多份同样的字节码 。

3. JVM-内存管理.md

JVM将内存主要划分为:方法区、虚拟机栈、本地方法栈、堆、程序计数器。

JVM运行时数据区.

关系图:

程序计数器

记录当前线程锁执行的字节码的行号。

  1. 程序计数器是一块较小的内存空间。
  2. 处于线程独占区。
  3. 执行java方法时,它记录正在执行的虚拟机字节码指令地址。执行native方法,它的值为undefined
  4. 该区域是唯一一个没有规定任何OutOfMemoryError的区域

虚拟机栈

存放方法运行时所需的数据,成为栈帧。其实它很简单!它里面存放的是一个函数的上下文,具体存放的是执行的函数的一些数据。执行的函数需要的数据无非就是局部变量表(保存函数内部的变量)、操作数栈(执行引擎计算时需要),方法出口等等。

  • 栈帧:执行引擎每调用一个方法时,就为这个函数创建一个栈帧,并加入虚拟机栈。换个角度理解,每个函数从调用到执行结束,其实是对应一个栈帧的入栈和出栈。

  • 局部变量表: 存放编译期间可知的各种基本数据类型、引用类型、return Address类型

    • 局部变量表的内存空间在编译期间就完成分配,运行期间不会改变。

相关报错:
StackOverflowError:当栈帧大于我们设置的栈大小,就会出现栈溢出(递归没有出口等因素)
OutOfMemoryError:

本地方法栈

本地方法栈与虚拟机栈所发挥的作用很相似,他们的区别在于虚拟机栈为执行Java代码方法服务,而本地方法栈是为Native方法服务。与虚拟机栈一样,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。

堆内存

存储对象实例。

Java堆可以说是虚拟机中最大一块内存了。它是所有线程所共享的内存区域,几乎所有的实例对象都是在这块区域中存放。当然,睡着JIT编译器的发展,所有对象在堆上分配渐渐变得不那么“绝对”了。

Java堆是垃圾收集器管理的主要区域。由于现在的收集器基本上采用的都是分代收集算法,所有Java堆可以细分为:新生代和老年代。在细致分就是把新生代分为:Eden空间、From Survivor空间、To Survivor空间。当堆无法再扩展时,会抛出OutOfMemoryError异常。

分配堆内存指令参数:-Xms -Xmx

方法区

存储运行时常量池,已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。(类版本、字段、方法、接口)。

运行时常量池:占用方法区中的一块。

方法区是各个线程共享区域,很容易理解,我们在写Java代码时,每个线程度可以访问同一个类的静态变量对象。

由于使用反射机制的原因,虚拟机很难推测那个类信息不再使用,因此这块区域的回收很难。另外,对这块区域主要是针对常量池回收,值得注意的是JDK1.7已经把常量池转移到堆里面了。同样,当方法区无法满足内存分配需求时,会抛出OutOfMemoryError。
制造方法区内存溢出,注意,必须在JDK1.6及之前版本才会导致方法区溢出,原因后面解释,执行之前,可以把虚拟机的参数-XXpermSize和-XX:MaxPermSize限制方法区大小。

List<String> list =new ArrayList<String>();
int i =0;
while(true){
    list.add(String.valueOf(i).intern());
} 

运行后会抛出java.lang.OutOfMemoryError:PermGen space异常。
解释一下,String的intern()函数作用是如果当前的字符串在常量池中不存在,则放入到常量池中。上面的代码不断将字符串添加到常量池,最终肯定会导致内存不足,抛出方法区的OOM。

下面解释一下,为什么必须将上面的代码在JDK1.6之前运行。我们前面提到,JDK1.7后,把常量池放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码:

String str1 =new StringBuilder("fant").append("j").toString();
System.out.println(str1.intern()==str1);

String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);

这段代码在JDK1.6和JDK1.7运行的结果不同。JDK1.6结果是:false,false ,JDK1.7结果是true, false。原因是:JDK1.6中,intern()方法会吧首次遇到的字符串实例复制到常量池中,返回的也是常量池中的字符串的引用,而StringBuilder创建的字符串实例是在堆上面,所以必然不是同一个引用,返回false。

在JDK1.7中,intern不再复制实例,常量池中只保存首次出现的实例的引用,因此intern()返回的引用和由StringBuilder创建的字符串实例是同一个。为什么对str2比较返回的是false呢?这是因为,JVM中内部在加载类的时候,就已经有"java"这个字符串,不符合“首次出现”的原则,因此返回false。

更深入的了解常量池和intern:
/**
 * Created by Fant.J.
 */
public class Test {
    public static void main(String[] args) {
        String a = "fantj";
        String b = "fantj";
        //a和b 会存到常量池里,常量池类似一个set集合,不允许有重复的值,所以加入第二个重复的值会返回已存在值的索引
        System.out.println(a == b);
        //new操作会实例化一个对象,会把他放到堆中。
        String c = new String("fantj");
        //所以a和c比较,a在常量池,c在堆,索引肯定不同,结果自然不同,返回false
        System.out.println(a == c);
        //a和c.intern比较,intern会把c搬到常量池,所以加入第二个重复的值会返回已存在值的索引,返回true
        System.out.println(a == c.intern());
    }
}

有注释,仔细看注释。

4. JVM-程序计数器

什么是程序计数器

程序计数器(program counter register)只占用了一块比较小的内存空间,至于小到什么程度呢,这样说吧,有时可以忽略不计的。

程序计数器可以看作是当前线程所执行的字节码文件(class)的行号指示器。也就是我们javap -c xxx.class 反编译生成的指令行号。

在虚拟机的世界中,字节码解释器就是通过改变计数器的值来选取下一条执行的字节码指令,分支、循环、跳转、异常处理、线程恢复都需要借助它来实现的。

java虚拟机多线程是通过线程间轮流切换来分配给处理器执行时间;在确定时间节点,一个处理器(一核)只会执行一个线程的指令;为保证线程切换回来后能恢复到原执行位置,各个线程间计数器互相不影响,独立存储(称之为 线程私有 的内存);

谁在操作程序计数器

jvm中有字节码执行引擎,它会在执行代码的时候对程序计数器进行记录。

程序计数器特点

  1. 线程隔离。每个线程都有属于自己的程序计数器。
  2. 执行native方法的时候,程序计数器值为空。也很好理解,因为它直接通过JNI调用本地C/C++库,没有经过字节码引擎处理。
  3. 占用内存很小,可以忽略不计。
  4. 永远不会发生OOM(因为设计就没有规定)

5. JVM-运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

每一个栈帧都包括了局部变量表操作数栈动态连接方法返回地址一些额外的附加信息。在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现

一个线程中的方法调用链可能会很长,很多方法都同时处理执行状态。对于执行引擎来讲,活动线程中,只有虚拟机栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),这个栈帧所关联的方法称为当前方法(Current Method)。执行引用所运行的所有字节码指令都只针对当前栈帧进行操作。栈帧的概念结构如下图所示:

1. 局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。在编译Class文件时,就在方法的Code属性的max_locals数据项中已经确定了该方法需要分配的局部变量表的最大容量。

变量槽 (Variable Slot)是局部变量表的最小单位,没有强制规定大小为 32 位,虽然32位足够存放大部分类型的数据。一个 Slot可以存放 booleanbytecharshortintfloatreferencereturnAddress 8种类型。其中 reference 表示对一个对象实例的引用,通过它可以得到对象在Java 堆中存放的起始地址的索引和该数据所属数据类型在方法区的类型信息。returnAddress则指向了一条字节码指令的地址。 对于64位的 long 和 double 变量而言,虚拟机会为其分配两个连续的 Slot 空间

虚拟机通过索引定位的方式使用局部变量表。之前我们知道,**局部变量表存放的是方法参数和局部变量。当调用方法是非static 方法时,局部变量表中第0位索引的 Slot 默认是用于传递方法所属对象实例的引用,即 “this” 关键字指向的对象。**分配完方法参数后,便会依次分配方法内部定义的局部变量。

Slot复用验证

为了节省栈帧空间,局部变量表中的 Slot 是可以重用的。当离开了某些变量的作用域之后,这些变量对应的 Slot 就可以交给其他变量使用。这种机制有时候会影响垃圾回收行为。

public class Main {
    public static void main(String[] args) {
        byte[] placeholder = new byte[64*1024*1024];
        System.gc();
    }
}
[GC (System.gc())  69468K->66384K(188416K), 0.0016481 secs]
[Full GC (System.gc())  66384K->66280K(188416K), 0.0079337 secs]
public class Main {
    public static void main(String[] args) {
        {
            byte[] placeholder = new byte[64*1024*1024];
        }
        int a = 0;
        System.gc();
    }
}
[GC (System.gc())  69468K->66368K(188416K), 0.0012876 secs]
[Full GC (System.gc())  66368K->744K(188416K), 0.0055897 secs]

可以看到,当我吧byte的声明单独放到代码块中,然后再执行作用域之外的代码的时候,gc对slot进行了回收。

注意:jvm不会给局部变量赋初始值,只给全局变量赋初始值。

2. 操作数栈

操作数栈(Operand Stack)也常称为操作栈,是一个后入先出栈。在Class 文件的Code 属性的 max_stacks 指定了执行过程中最大的栈深度。Java 虚拟机的解释执行引擎称为”基于栈的执行引擎“,这里的栈就是指操作数栈。

方法执行中进行算术运算或者是调用其他的方法进行参数传递的时候是通过操作数栈进行的。

jvm对操作数栈的优化

在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递

3. 动态链接

每个栈帧都包含一个执行运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。

Class 文件中存放了大量的符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析。另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接

4. 方法返回地址

当一个方法开始执行以后,只有两种方法可以退出当前方法

  1. 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的PC计数器可以作为返回地址
  2. 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定

当方法返回时,可能进行3个操作

  1. 恢复上层方法的局部变量表和操作数栈
  2. 把返回值压入调用者调用者栈帧的操作数栈
  3. 调整 PC 计数器的值以指向方法调用指令后面的一条指令

5. 附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

6. JVM-方法调用

方法调用不是方法执行,方法调用是让jvm确定调用哪个方法,所以,程序运行时的它是最普遍、最频繁的操作。jvm需要在类加载期间甚至运行期间才能确定方法的直接引用。

解析

所有方法在Class文件都是一个常量池中的符号引用,类加载的解析阶段会将其转换成直接引用,这种解析的前提是:要保证这个方法在运行期是不可变的。这类方法的调用称为解析。

jvm提供了5条方法调用字节码指令:

  • invokestatic:调用静态方法
  • invokespecial:调用构造器方法、私有方法和父类方法
  • invokevirtual:调用所有的虚方法。
  • invokeinterface:调用接口方法,会在运行时期再确定一个实现此接口的对象
  • invokedynamic: 现在运行时期动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条指令,分派逻辑都是固化在虚拟机里面的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。InvokeDynamic指令详细请点击InvokeDynamic指令

invokestaticinvokespecial指令调用的方法,都能保证方法的不可变性,符合这个条件的有静态方法私有方法实力构造器父类方法4类。这些方法称为非虚方法。

public class Main {
    public static void main(String[] args) {
        //invokestatic调用
        Test.hello();
        //invokespecial调用
        Test test = new Test();
    }
    static class Test{
        static void hello(){
            System.out.println("hello");
        }
    }
}

解析调用一定是一个静态的过程,在编译期间就可以完全确定,在类装载的解析阶段就会把涉及的符号引用全部转化为可确定的直接引用,不会延迟到运行期去完成。而分派调用可能是静态的也可能是动态的,根据分派一句的宗量数可分为单分派和多分派。因此分派可分为:静态单分派、静态多分派、动态单分派、动态多分派。

静态分派(方法重载)

所有依赖静态类型来定位方法执行版本的分派动作成为静态分派。

public class Test {
    static class Phone{}
    static class Mi extends Phone{}
    static class Iphone extends Phone{}

    public void show(Mi mi){
        System.out.println("phone is mi");
    }
    public void show(Iphone iphone){
        System.out.println("phone is iphone");
    }
    public void show(Phone phone){
        System.out.println("phone parent class be called");
    }

    public static void main(String[] args) {
        Phone mi = new Mi();
        Phone iphone = new Iphone();

        Test test = new Test();
        test.show(mi);
        test.show(iphone);
        test.show((Mi)mi);
    }
}

执行结果:

phone parent class be called
phone parent class be called
phone is mi

我们把上面代码中的Phone称为变量的静态类型或者叫外观类型,吧MiIphone称为实际类型,静态类型仅仅在使用时发生变化,编译可知;实际类型在运行期才知道结果,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

所以,jvm重载时是通过参数的静态类型而不是实际类型作为判定依据。下图可以证明:

根据上面的代码也可以看出,我们可以使用强制类型转换来使静态类型发生改变。

动态分派(方法覆盖)

public class Test2 {
    static abstract class Phone{
        abstract void show();
    }
    static class Mi extends Phone{
        @Override
        void show() {
            System.out.println("phone is mi");
        }
    }
    static class Iphone extends Phone{
        @Override
        void show() {
            System.out.println("phone is iphone");
        }
    }

    public static void main(String[] args) {
        Phone mi = new Mi();
        Phone iphone = new Iphone();
        mi.show();
        iphone.show();
        mi = new Iphone();
        mi.show();
    }
}
phone is mi
phone is iphone
phone is iphone

这个结果大家肯定都能猜到,但是你又没有想过编译器是怎么确定他们的实际变量类型的呢。这就关系到了invokevirtual指令,该指令的第一步就是在运行期确定接受者的实际类型。所以两次调用invokevirtual指令吧常量池中的类方法符号引用解析到了不同的直接引用上。

invokevirtual指令的运行时解析过程大致分为以下几个步骤。

(1)找到操作数栈顶的第一个元素(对象引用)所指向的对象的实际类型,记作C;
(2)如果在类型C中找到与常量中的描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError。
(3)否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证。
(4)如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常

动态类型语言支持

动态语言的关键特征是它的类型检查的主体过程是在运行期间而不是编译期。相对的,在编译期间进行类型检查过程的语言(java、c++)就是静态类型语言。

运行时异常:代码只要不运行到这一行就不会报错。
连接时异常:类加载抛出异常。

那动态、静态类型语言谁更好?

它们都有自己的优点。静态类型语言在编译期确定类型,可以提供严谨的类型检查,有很多问题编码的时候就能及时发现,利于开发稳定的大规模项目。动态类型语言在运行期确定类型,有很大的灵活性,代码更简洁清晰,开发效率高。

public class MethodHandleTest {
    static class ClassA {  
        public void show(String s) {
            System.out.println(s);  
        }  
    }  
    public static void main(String[] args) throws Throwable {  
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();  
        // 无论obj最终是哪个实现类,下面这句都能正确调用到show方法。
        getPrintlnMH(obj).invokeExact("fantj");
    }  
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
        // MethodType:代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。   
        MethodType mt = MethodType.methodType(void.class, String.class);
        // lookup()方法来自于MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。   
        // 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接收者,也即是this指向的对象,这个参数以前是放在参数列表中进行传递,现在提供了bindTo()方法来完成这件事情。   
        return lookup().findVirtual(reveiver.getClass(), "show", mt).bindTo(reveiver);
    }  
}
fantj

无论obj是何种类型(临时定义的ClassA抑或是实现PrintStream接口的实现类System.out),都可以正确调用到show()方法。

仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别

  • ReflectionMethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()findVirtual()findSpecial()正是为了对应于invokestaticinvokevirtual & invokeinterfaceinvokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
  • Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection重量级,而MethodHandle轻量级
  • 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
  • MethodHandleReflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。
invokedynamic指令

参考原文:https://blog.csdn.net/a_dreaming_fish/article/details/50635651

一开始就提到了JDK 7为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令invokedynamic,但前面一直没有再提到它,甚至把之前使用MethodHandle的示例代码反编译后也不会看见invokedynamic的身影,它到底有什么应用呢?

某种程度上可以说invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有四条invoke*指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。而且,它们两者的思路也是可类比的,可以想象作为了达成同一个目的,一个用上层代码和API来实现,另一个是用字节码和Class中其他属性和常量来完成。因此,如果前面MethodHandle的例子看懂了,理解invokedynamic指令并不困难。
  每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法上。我们还是照例拿一个实际例子来解释这个过程吧。如下面代码清单所示:

public class InvokeDynamicTest {
    public static void main(String[] args) throws Throwable {  
        INDY_BootstrapMethod().invokeExact("icyfenix");  
    }
    public static void testMethod(String s) {
        System.out.println("hello String:" + s);  
    }
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws Throwable {
        return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
    }
    private static MethodType MT_BootstrapMethod() {  
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", null);
    }
    private static MethodHandle MH_BootstrapMethod() throws Throwable {
        return lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());  
    }
    private static MethodHandle INDY_BootstrapMethod() throws Throwable {  
        CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(), "testMethod", MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));  
        return cs.dynamicInvoker();  
    }
}
hello String:icyfenix

BootstrapMethod(),它的字节码很容易读懂,所有逻辑就是调用MethodHandles$Lookup的findStatic()方法,产生testMethod()方法的MethodHandle,然后用它创建一个ConstantCallSite对象。最后,这个对象返回给invokedynamic指令实现对testMethod()方法的调用,invokedynamic指令的调用过程到此就宣告完成了。

7. JVM-视角看对象创建

从jvm处理对象的流程来看,大概分成三步骤:1.如何创建。2.什么是最佳存储模型。3.如何访问。本文将按照这三个流程进行讲解。

一、对象的创建过程

1. 拿到内存创建指令

当虚拟机遇到内存创建的指令的时候(new 类名),来到了方法区,找 根据new的参数在常量池中定位一个类的符号引用。

2. 检查符号引用

检查该符号引用有没有被加载、解析和初始化过,如果没有则执行类加载过程,否则直接准备为新的对象分配内存

3. 分配内存

虚拟机为对象分配内存(堆)分配内存分为指针碰撞和空闲列表两种方式;分配内存还要要保证并发安全,有两种方式。

3.1. 指针碰撞

所有的存储空间分为两部分,一部分是空闲,一部分是占用,需要分配空间的时候,只需要计算指针移动的长度即可。

3.2. 空闲列表

虚拟机维护了一个空闲列表,需要分配空间的时候去查该空闲列表进行分配并对空闲列表做更新。

可以看出,内存分配方式是由java堆是否规整决定的,java堆的规整是由垃圾回收机制来决定的

3.2.5 安全性问题的思考

假如分配内存策略是指针碰撞,如果在高并发情况下,多个对象需要分配内存,如果不做处理,肯定会出现线程安全问题,导致一些对象分配不到空间等。

下面是解决方案:

3.3 线程同步策略

也就是每个线程都进行同步,防止出现线程安全。

3.4. 本地线程分配缓冲

也称TLAB(Thread Local Allocation Buffer),在堆中为每一个线程分配一小块独立的内存,这样以来就不存并发问题了,Java 层面与之对应的是 ThreadLocal 类的实现

4. 初始化
  1. 分配完内存后要对对象的头(Object Header)进行初始化,这新信息包括:该对象对应类的元数据、该对象的GC代、对象的哈希码。
  2. 抽象数据类型默认初始化为null,基本数据类型为0,布尔为false。。。
5. 调用对象的初始化方法

也就是执行构造方法。

二、对象的内存模型

头信息

在对象头中有两类信息:标志信息(Mark Word)和类型指针(Kclass Pointer)

  1. 标识信息用来存放对象一些固有属性的状态,这些属性从对象创建就有,而不是 Java 的使用者定义的:
  • 哈希码:对象的唯一标识符
  • 对象的分代年龄:与垃圾回收有关
  • 线程持有的锁
  • 锁的状态
  • 偏向线程 ID、偏向时间戳
  • 数组长度:如果该对象是数组,会有数组长度信息
  1. 类型指针是指向方法区中类元信息的指针。
实例信息(instanceData)

实例的信息存放的是一些对 Java 使用者真正有效的信息,也就是类中定义的各个字段,其中还包括从父类继承的字段。hotspot把相同宽度的类型分配在一起。

内存的对齐填充(Padding)

对其填充这段内存段存在与否取决于前面两部分的长度,为了保证对象内存模型的长度为 8 字节的整数倍,这也是虚拟机自动内存管理的要求(对象起始地址必须是8的整数倍)。

三、对象的访问定位

对象创建起来之后,就会在虚拟机栈中维护一个本地变量表,用于存储基础类型和基础类型的值,引用类型与引用类型的值。
其中引用类型的值就是堆中对象地址。如何引用堆中地址有两种方式:

  • 句柄:在堆中维护一个句柄池,句柄中包含了对象地址,当对象改变的时候,只需改变句柄,不需要改变栈中本地变量表的引用
  • 直接指针:对象的地址直接存储在栈中,这样做的好处就是访问速度变快(Hotspot采用该方式)

8. JVM-垃圾回收机制

如何判定对象为垃圾对象

在堆里面存放着Java世界中几乎所有的对象实例, 垃圾收集器在对堆进行回收前, 第一件事就是判断哪些对象已死(可回收).

引用计数法

在JDK1.2之前,使用的是引用计数器算法。
在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1,当引用计数器被减为零的时候,标志着这个对象已经没有引用了,可以回收了!


**问题:**如果在A类中调用B类的方法,B类中调用A类的方法,这样当其他所有的引用都消失了之后,A和B还有一个相互的引用,也就是说两个对象的引用计数器各为1,而实际上这两个对象都已经没有额外的引用,已经是垃圾了。但是该算法并不会计算出该类型的垃圾。

可达性分析法

在主流商用语言(如Java、C#)的主流实现中, 都是通过可达性分析算法来判定对象是否存活的: 通过一系列的称为 GC Roots 的对象作为起点, 然后向下搜索; 搜索所走过的路径称为引用链/Reference Chain, 当一个对象到 GC Roots 没有任何引用链相连时, 即该对象不可达, 也就说明此对象是不可用的, 如下图:虽然E和F相互关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

注: 即使在可达性分析算法中不可达的对象, VM也并不是马上对其回收, 因为要真正宣告一个对象死亡, 至少要经历两次标记过程: 第一次是在可达性分析后发现没有与GC Roots相连接的引用链, 第二次是GC对在F-Queue执行队列中的对象进行的小规模标记(对象需要覆盖finalize()方法且没被调用过).

在Java, 可作为GC Roots的对象包括:
  1. 方法区: 类静态属性引用的对象;
  2. 方法区: 常量引用的对象;
  3. 虚拟机栈(本地变量表)中引用的对象.
  4. 本地方法栈JNI(Native方法)中引用的对象。

如何回收

回收策略

垃圾收集策略有分代收集和分区收集。

分代收集算法
标记-清除算法(老年代)

该算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象(可达性分析), 在标记完成后统一清理掉所有被标记的对象.

该算法会有两个问题:

  1. 效率问题,标记和清除效率不高。
  2. 空间问题: 标记清除后会产生大量不连续的内存碎片, 空间碎片太多可能会导致在运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集。

所以它一般用于"垃圾不太多的区域,比如老年代"。

复制算法(新生代)

该算法的核心是将可用内存按容量划分为大小相等的两块, 每次只用其中一块, 当这一块的内存用完, 就将还存活的对象(非垃圾)复制到另外一块上面, 然后把已使用过的内存空间一次清理掉.

优点:不用考虑碎片问题,方法简单高效。
缺点:内存浪费严重。

现代商用VM的新生代均采用复制算法, 但由于新生代中的98%的对象都是生存周期极短的, 因此并不需完全按照1∶1的比例划分新生代空间, 而是将新生代划分为一块较大的Eden区和两块较小的Survivor区(HotSpot默认Eden和Survivor的大小比例为8∶1), 每次只用Eden和其中一块Survivor. 当发生MinorGC时, 将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor上, 最后清理掉Eden和刚才用过的Survivor的空间. 当Survivor空间不够用(不足以保存尚存活的对象)时, 需要依赖老年代进行空间分配担保机制, 这部分内存直接进入老年代。

复制算法的空间分配担保:
在执行Minor GC前, VM会首先检查老年代是否有足够的空间存放新生代尚存活对象, 由于新生代使用复制收集算法, 为了提升内存利用率, 只使用了其中一个Survivor作为轮换备份, 因此当出现大量对象在Minor GC后仍然存活的情况时, 就需要老年代进行分配担保, 让Survivor无法容纳的对象直接进入老年代, 但前提是老年代需要有足够的空间容纳这些存活对象. 但存活对象的大小在实际完成GC前是无法明确知道的, 因此Minor GC前, VM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小, 如果条件成立, 则进行Minor GC, 否则进行Full GC(让老年代腾出更多空间).
然而取历次晋升的对象的平均大小也是有一定风险的, 如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然可能导致担保失败(Handle Promotion Failure, 老年代也无法存放这些对象了), 此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间).


标记-整理算法(老年代)

标记清除算法会产生内存碎片问题, 而复制算法需要有额外的内存担保空间, 于是针对老年代的特点, 又有了标记整理算法. 标记整理算法的标记过程与标记清除算法相同, 但后续步骤不再对可回收对象直接清理, 而是让所有存活的对象都向一端移动,然后清理掉端边界以外的内存.

方法区回收(永久代)

在方法区进行垃圾回收一般”性价比”较低, 因为在方法区主要回收两部分内容: 废弃常量无用的类.

回收废弃常量与回收其他年代中的对象类似, 但要判断一个类是否无用则条件相当苛刻:

  1. 该类所有的实例都已经被回收, Java堆中不存在该类的任何实例;
  2. 该类对应的Class对象没有在任何地方被引用(也就是在任何地方都无法通过反射访问该类的方法);
  3. 加载该类的ClassLoader已经被回收.
    但即使满足以上条件也未必一定会回收, Hotspot VM还提供了-Xnoclassgc参数控制(关闭CLASS的垃圾回收功能). 因此在大量使用动态代理、CGLib等字节码框架的应用中一定要关闭该选项, 开启VM的类卸载功能, 以保证方法区不会溢出.
分区收集

分区算法则将整个堆空间划分为连续的不同小区间, 每个小区间独立使用, 独立回收. 这样做的好处是可以控制一次回收多少个小区间

在相同条件下, 堆空间越大, 一次GC耗时就越长, 从而产生的停顿也越长. 为了更好地控制GC产生的停顿时间, 将一块大的内存区域分割为多个小块, 根据目标停顿时间, 每次合理地回收若干个小区间(而不是整个堆), 从而减少一次GC所产生的停顿

垃圾回收器

Serial

Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 它在进行垃圾收集时,会暂停所有的工作进程,用一个线程去完成GC工作

特点:简单高效,适合jvm管理内存不大的情况(十兆到百兆)。

Parnew

ParNew收集器其实是Serial的多线程版本,回收策略完全一样,但是他们又有着不同。

我们说了Parnew是多线程gc收集,所以它配合多核心的cpu效果更好,如果是一个cpu,他俩效果就差不多。(可用-XX:ParallelGCThreads参数控制GC线程数)

Cms

CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上的并发收集器, 虽然现在已经有了理论意义上表现更好的G1收集器, 但现在主流互联网企业线上选用的仍是CMS(如Taobao),又称多并发低暂停的收集器。

由他的英文组成可以看出,它是基于标记-清除算法实现的。整个过程分4个步骤:

  1. 初始标记(CMS initial mark):仅只标记一下GC Roots能直接关联到的对象, 速度很快
  2. 并发标记(CMS concurrent mark: GC Roots Tracing过程)
  3. 重新标记(CMS remark):修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
  4. 并发清除(CMS concurrent sweep: 已死对象将会就地释放)

可以看到,初始标记、重新标记需要STW(stop the world 即:挂起用户线程)操作。因为最耗时的操作是并发标记和并发清除。所以总体上我们认为CMS的GC与用户线程是并发运行的。

**优点:**并发收集、低停顿

缺点:

  1. CMS默认启动的回收线程数=(CPU数目+3)*4
    当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低.
  2. 无法清除浮动垃圾(GC运行到并发清除阶段时用户线程产生的垃圾),因为用户线程是需要内存的,如果浮动垃圾施放不及时,很可能就造成内存溢出,所以CMS不能像别的垃圾收集器那样等老年代几乎满了才触发,CMS提供了参数-XX:CMSInitiatingOccupancyFraction来设置GC触发百分比(1.6后默认92%),当然我们还得设置启用该策略-XX:+UseCMSInitiatingOccupancyOnly
  3. 因为CMS采用标记-清除算法,所以可能会带来很多的碎片,如果碎片太多没有清理,jvm会因为无法分配大对象内存而触发GC,因此CMS提供了-XX:+UseCMSCompactAtFullCollection参数,它会在GC执行完后接着进行碎片整理,但是又会有个问题,碎片整理不能并发,所以必须单线程去处理,所以如果每次GC完都整理用户线程stop的时间累积会很长,所以XX:CMSFullGCsBeforeCompaction参数设置隔几次GC进行一次碎片整理(默认为0)。
G1

同优秀的CMS垃圾回收器一样,G1也是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方也推荐使用G1来代替选择CMS。G1最大的特点是引入分区的思路,弱化分代的概念,合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷。

因为每个区都有E、S、O代,所以在G1中,不需要对整个Eden等代进行回收,而是寻找可回收对象比较多的区,然后进行回收(虽然也需要STW操作,但是花费的时间是很少的),保证高效率。

新生代收集

G1的新生代收集跟ParNew类似,如果存活时间超过某个阈值,就会被转移到S/O区。

年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域的大小

老年代收集

分为以下几个阶段:

  1. 初始标记 (Initial Mark: Stop the World Event)
    在G1中, 该操作附着一次年轻代GC, 以标记Survivor中有可能引用到老年代对象的Regions.
  2. 扫描根区域 (Root Region Scanning: 与应用程序并发执行)
    扫描Survivor中能够引用到老年代的references. 但必须在Minor GC触发前执行完
  3. 并发标记 (Concurrent Marking : 与应用程序并发执行)
    在整个堆中查找存活对象, 但该阶段可能会被Minor GC中断
  4. 重新标记 (Remark : Stop the World Event)
    完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).
  5. 清理 (Cleanup : Stop the World Event and Concurrent)
    在含有存活对象和完全空闲的区域上进行统计(STW)、擦除Remembered Sets(使用Remembered Set来避免扫描全堆,每个区都有对应一个Set用来记录引用信息、读写操作记录)(STW)、重置空regions并将他们返还给空闲列表(free list)(Concurrent)

详情请看参考文档

9. JVM-Class文件结构&字节码指令

class文件结构

Class文件存储的内容称为字节码(ByteCode),包含了JVM指令集和符号表以及若干其他辅助信息。

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符,整个Class文件中存储的内容几乎全部是程序运行的必要的数据,没有空隙存在。

当遇到8位字节以上的空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件中有两种数据类型,分别是无符号数和表。

无符号数

无符号数属于基本的数据类型,以u1、u2、u4、u8来表示一个字节、两个字节…的无符号数;无符号数用来描述数字、索引引用、数量值或UTF-8编码构成的字符串值。

表是由多个无符号数或其他表作为数据项构成的复合数据类型,一般以"_info"结尾,用来描述class文件的数据结构。

特点:节省存储空间,提高处理性能

ClassFile { 
    u4 magic; 
    u2 minor_version; 
    u2 major_version; 
    u2 constant_pool_count; 
    cp_info constant_pool[constant_pool_count-1]; 
    u2 access_flags; 
    u2 this_class; 
    u2 super_class; 
    u2 interfaces_count; 
    u2 interfaces[interfaces_count]; 
    u2 fields_count; 
    field_info fields[fields_count]; 
    u2 methods_count; 
    method_info methods[methods_count]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}
  • 魔数
  • Class文件版本
  • 常量池
  • 访问标志
  • 类索引,父类索引,接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合

u2表示无符号数2个字节
u4表示无符号数4个字节

Class文件设计理念和意义

1. 魔数magic

魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。魔数值固定为0xCAFEBABE,不会改变。

证明magic作用

创建一个class文件 magic.class ,内容是magic test,直接运行java magic操作:

84407@FantJ MINGW64 ~/Desktop
$ java magictest
▒▒▒▒: ▒▒▒▒▒▒▒▒ magictest ʱ▒▒▒▒ LinkageError
        java.lang.ClassFormatError: Incompatible magic value 1886741100 in class file magictest

报错意思是:magic矛盾,然后给了个magic value的十进制数,那么可以识别的magic十进制应该是多少呢。


应该是3405691582

那么,然后我用javac编译的正常java文件生成class文件,用binary viewer 查看:

minor_version、major_version

魔数往后后面四位:表示字节码版本,分别表示Class文件的副、主版本。当今用的最广的几个版本:
jdk1.8:52
jdk1.7:51
jdk1.6:50


对应版本号是52,是jdk1.8

版本向下兼容

2. constant_pool_count

常量池计数器,值等于constant_pool表中的成员数加1,占用两个字节

3. constant_pool[]常量池

Java虚拟机指令执行时依赖常量池(constant_pool)表中的符号信息。

所有的常量池项都具有如下通用格式:

cp_info {
    u1 tag; 
    u1 info[]; 
}

info[]项的内容tag由的类型所决定。tag有效的类型和对应的取值在下表列出

常量类型
CONSTANT_Class7
CONSTANT_Fieldref9
CONSTANT_Methodref10
CONSTANT_InterfaceMethodref11
CONSTANT_String8
CONSTANT_Integer3
CONSTANT_Float4
CONSTANT_Long5
CONSTANT_Double6
CONSTANT_NameAndType12
CONSTANT_Utf81
CONSTANT_MethodHandle15
CONSTANT_MethodType16
CONSTANT_InvokeDynamic18
3.1 CONSTANT_Class_info结构

表示类或接口

CONSTANT_Class_info {
    u1 tag; 
    u2 name_index;
}

name_index必须是对常量池的一个有效索引

3.2 CONSTANT_Fieldref_info, CONSTANT_Methodref_info和CONSTANT_InterfaceMethodref_info结构

字段:

CONSTANT_Fieldref_info {
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

方法:

CONSTANT_Methodref_info { 
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

接口方法:

CONSTANT_InterfaceMethodref_info {
    u1 tag; 
    u2 class_index; 
    u2 name_and_type_index; 
}

class_index必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Class_info结构,表示一个类或接口,当前字段或方法是这个类或接口的成员。

CONSTANT_Methodref_info结构的class_index项的类型必须是类(不能是接口)。CONSTANT_InterfaceMethodref_info结构的class_index项的类型必须是接口(不能是类)。CONSTANT_Fieldref_info结构的class_index项的类型既可以是类也可以是接口。

name_and_type_index必须是对常量池的有效索引,表示当前字段或方法的名字和描述符。
在一个CONSTANT_Fieldref_info结构中,给定的描述符必须是字段描述符。而CONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info中给定的描述符必须是方法描述符。

3.3 CONSTANT_String_info结构

用来表示String的结构

CONSTANT_String_info {
    u1 tag;
    u2 string_index;
}

string_index必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一组Unicode码点序列,这组Unicode码点序列最终会被初始化为一个String对象。

3.4CONSTANT_Integer_info和CONSTANT_Float_info结构

表示4字节(int和float)的数值常量:

CONSTANT_Integer_info {
    u1 tag; 
    u4 bytes; 
} 
CONSTANT_Float_info { 
    u1 tag; 
    u4 bytes;
}
3.5CONSTANT_Long_info和CONSTANT_Double_info结构

表示8字节(long和double)的数值常量

CONSTANT_Long_info {
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
} 

CONSTANT_Double_info { 
    u1 tag; 
    u4 high_bytes; 
    u4 low_bytes; 
}
3.6 CONSTANT_NameAndType_info结构

表示字段或方法,但是和前面介绍的3个结构不同,CONSTANT_NameAndType_info结构没有标识出它所属的类或接口

CONSTANT_NameAndType_info { 
    u1 tag; 
    u2 name_index; 
    u2 descriptor_index;
}

name_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构要么表示特殊的方法名,要么表示一个有效的字段或方法的非限定名(Unqualified Name)。

descriptor_index项的值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,这个结构表示一个有效的字段描述符或方法描述符。

3.7 CONSTANT_Utf8_info结构

用于表示字符串常量的值

CONSTANT_Utf8_info {
    u1 tag; 
    u2 length; 
    u1 bytes[length]; 
}

CONSTANT_Utf8_info结构中的内容是以length属性确定长度的

3.8 CONSTANT_MethodHandle_info结构

表示方法句柄

CONSTANT_MethodHandle_info {
    u1 tag;
    u1 reference_kind;
    u2 reference_index;
}

reference_kind项的值必须在1至9之间(包括1和9),它决定了方法句柄的类型。

  1. 如果reference_kind项的值为1(REF_getField)、2(REF_getStatic)、3(REF_putField)或4(REF_putStatic),那么常量池在reference_index索引处的项必须是CONSTANT_Fieldref_info结构,表示由一个字段创建的方法句柄。
  2. 如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或8(REF_newInvokeSpecial),那么常量池在reference_index索引处的项必须是CONSTANT_Methodref_info结构,表示由类的方法或构造函数创建的方法句柄。
  3. 如果reference_kind项的值是9(REF_invokeInterface),那么常量池在reference_index索引处的项必须是CONSTANT_InterfaceMethodref_info结构,表示由接口方法创建的方法句柄。
  4. 如果reference_kind项的值是5(REF_invokeVirtual)、6(REF_invokeStatic)、7(REF_invokeSpecial)或9(REF_invokeInterface),那么方法句柄对应的方法不能为实例初始化()方法或类初始化方法()。
  5. 如果reference_kind项的值是8(REF_newInvokeSpecial),那么方法句柄对应的方法必须为实例初始化()方法。
3.9 CONSTANT_MethodType_info结构

表示方法类型

CONSTANT_MethodType_info { 
    u1 tag; 
    u2 descriptor_index; 
}
3.10 CONSTANT_InvokeDynamic_info结构

表示invokedynamic指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name)、参数和请求返回类型、以及可以选择性的附加被称为静态参数(Static Arguments)的常量序列。

CONSTANT_InvokeDynamic_info { 
    u1 tag; 
    u2 bootstrap_method_attr_index; 
    u2 name_and_type_index; 
}

bootstrap_method_attr_index项的值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引。

name_and_type_index项的值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符。

4. access_flags:访问标志

访问标志,access_flags是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags的取值范围和相应含义见下表。

标记名含义
ACC_PUBLIC0x0001可以被包的类外访问。
ACC_FINAL0x0010不允许有子类。
ACC_SUPER0x0020当用到invokespecial指令时,需要特殊处理的父类方法。
ACC_INTERFACE0x0200标识定义的是接口而不是类。
ACC_ABSTRACT0x0400不能被实例化。
ACC_SYNTHETIC0x1000标识并非Java源码生成的代码。
ACC_ANNOTATION0x2000标识注解类型
ACC_ENUM0x4000标识枚举类型

5. this_class:类索引

this_class的值必须是对constant_pool表中项目的一个有效索引值。

是一个对constant_pool表中项目的一个有效索引值,表示指向常量池的第几个位置。

6. super_class:父类索引

表示这个Class文件所定义的类的直接父类,如果Class文件的super_class的值为0,那这个Class文件只可能是定义的是java.lang.Object类,只有它是唯一没有父类的类

是一个对constant_pool表中项目的一个有效索引值,表示指向常量池的第几个位置。

7. interfaces_count:接口计数器

表示有这个类有几个接口。

8. interfaces[]:接口表

成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。

是一个对constant_pool表中项目的一个有效索引值,表示指向常量池的第几个位置。

表示当前类或接口的直接父接口数量

9. fields_count:字段计数器

表示当前Class文件fields[]数组的成员个数

10. fields[]:字段表

每个成员都必须是一个fields_info结构的数据项,描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。

用于表示当前类或接口中某个字段的完整描述

field_info {
    u2 access_flags; 
    u2 name_index;      //对常量池的一个有效索引
    u2 descriptor_index;     //对常量池的一个有效索引
    u2 attributes_count;     //当前字段的附加属性的数量
    attribute_info attributes[attributes_count];
}

access_flags项的值是用于定义字段被访问权限和基础属性的掩码标志。access_flags的取值范围和相应含义见下表所示:

标记名说明
ACC_PUBLIC0x0001public,表示字段可以从任何包访问。
ACC_PRIVATE0x0002private,表示字段仅能该类自身调用。
ACC_PROTECTED0x0004protected,表示字段可以被子类调用。
ACC_STATIC0x0008static,表示静态字段。
ACC_FINAL0x0010final,表示字段定义后值无法修改。
ACC_VOLATILE0x0040volatile,表示字段是易变的。
ACC_TRANSIENT0x0080transient,表示字段不会被序列化。
ACC_SYNTHETIC0x1000表示字段由编译器自动产生。
ACC_ENUM0x4000enum,表示字段为枚举类型。

attributes表的每一个成员的值必须是attribute结构,一个字段可以有任意个关联属性。

11. methods_count:方法计数器

methods_count的值表示当前Class文件methods[]数组的成员个数,Methods[]数组中每一项都是一个method_info结构的数据项。

12. methods[]:方法表

method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法。methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

methods[]数组中的每个成员都必须是一个method_info结构的数据项,用于表示当前类或接口中某个方法的完整描述。

method_info { 
    u2 access_flags; 
    u2 name_index; 
    u2 descriptor_index; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count]; 
}

access_flags项的值是用于定义当前方法的访问权限和基本属性的掩码标志,access_flags的取值范围和相应含义见下表所示。

标记名说明
ACC_PUBLIC0x0001public,方法可以从包外访问
ACC_PRIVATE0x0002private,方法只能本类中访问
ACC_PROTECTED0x0004protected,方法在自身和子类可以访问
ACC_STATIC0x0008static,静态方法
ACC_FINAL0x0010final,方法不能被重写(覆盖)
ACC_SYNCHRONIZED0x0020synchronized,方法由管程同步
ACC_BRIDGE0x0040bridge,方法由编译器产生
ACC_VARARGS0x0080表示方法带有变长参数
ACC_NATIVE0x0100native,方法引用非java语言的本地方法
ACC_ABSTRACT0x0400abstract,方法没有具体实现
ACC_STRICT0x0800strictfp,方法使用FP-strict浮点格式
ACC_SYNTHETIC0x1000方法在源文件中不出现,由编译器产生

name_indexdescriptor_index 两属性是对常量池的一个有效索引
attributes_count的项的值表示这个方法的附加属性的数量。
attributes 表的每一个成员的值必须是attribute结构,一个方法可以有任意个与之相关的属性。

13. attributes_count:属性计数器

attributes表中每一项都是一个attribute_info结构的数据项。

attributes_count的值表示当前Class文件attributes表的成员个数。

14. attributes[]:属性表

attributes表的每个项的值必须是attribute_info结构,在Class文件格式中的ClassFile结构、field_info结构,method_info结构和Code_attribute结构都有使用,所有属性的通用格式如下:

attribute_info {
    u2 attribute_name_index; 
    u4 attribute_length; 
    u1 info[attribute_length];
}

attribute_name_index必须是对当前Class文件的常量池的有效16位无符号索引。表示当前属性的名字。

attribute_length项的值给出了跟随其后的字节的长度,这个长度不包括attribute_name_indexattribute_name_index项的6个字节。

14.1 ConstantValue属性

ConstantValue属性是定长属性,位于field_info结构的属性表中。如果该字段为静态类型(即field_info结构的access_flags项设置了ACC_STATIC标志),则说明这个field_info结构表示的常量字段值将被分配为它的ConstantValue属性表示的值,这个过程也是类或接口申明的常量字段(Constant Field)初始化的一部分。这个过程发生在引用类或接口的类初始化方法执行之前。

ConstantValue_attribute { 
    u2 attribute_name_index; 
    u4 attribute_length; 
    u2 constantvalue_index; 
}

attribute_name_index项的值,必须是一个对常量池的有效索引。
attribute_length项的值固定为2。
constantvalue_index项的值,必须是一个对常量池的有效索引。

14.2 Code属性

Code属性是一个变长属性,位于method_info结构的属性表。一个Code属性只为唯一一个方法、实例类初始化方法或类初始化方法保存Java虚拟机指令及相关辅助信息。所有Java虚拟机实现都必须能够识别Code属性。如果方法被声明为native或者abstract类型,那么对应的method_info结构不能有明确的Code属性,其它情况下,method_info有必须有明确的Code属性。

Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length; 
    u2 max_stack;
    u2 max_locals;
    u4 code_length; 
    u1 code[code_length]; 
    u2 exception_table_length; 
    {   u2 start_pc;
        u2 end_pc; 
        u2 handler_pc; 
        u2 catch_type; 
    } exception_table[exception_table_length]; 
    u2 attributes_count; 
    attribute_info attributes[attributes_count];
}

attribute_name_index项的值必须是对常量池的有效索引
attribute_length项的值表示当前属性的长度,不包括开始的6个字节。
max_stack项的值给出了当前方法的操作数栈在运行执行的任何时间点的最大深度。
max_locals项的值给出了分配在当前方法引用的局部变量表中的局部变量个数,包括调用此方法时用于传递参数的局部变量。long和double型的局部变量的最大索引是max_locals-2,其它类型的局部变量的最大索引是max_locals-1.
code_length项给出了当前方法的code[]数组的字节数,code_length的值必须大于0,即code[]数组不能为空。
code[]数组给出了实现当前方法的Java虚拟机字节码。
exception_table_length项的值给出了exception_table[]数组的成员个数量。
exception_table[]数组的每个成员表示code[]数组中的一个异常处理器(Exception Handler)。exception_table[]数组中,异常处理器顺序是有意义的(不能随意更改)。
start_pcend_pc两项的值表明了异常处理器在code[]数组中的有效范围。
handler_pc项表示一个异常处理器的起点
如果catch_type项的值不为0,那么它必须是对常量池的一个有效索引
attributes_count项的值给出了Code属性中attributes表的成员个数。
属性表的每个成员的值必须是attribute结构。一个Code属性可以有任意数量的可选属性与之关联。

14.3 StackMapTable属性

StackMapTable属性是一个变长属性,位于Code属性的属性表中。这个属性会在虚拟机类加载的类型阶段被使用。

StackMapTable_attribute { 
    u2 attribute_name_index;
    u4 attribute_length; 
    u2 number_of_entries; 
    stack_map_frame entries[number_of_entries];
}

attribute_name_index项的值必须是对常量池的有效索引
attribute_length项的值表示当前属性的长度,不包括开始的6个字节。
number_of_entries项的值给出了entries表中的成员数量。Entries表的每个成员是都是一个stack_map_frame结构的项。
entries表给出了当前方法所需的stack_map_frame结构。

…更多的属性就不在这一一贴了,太多了,需要的时候查官方文档即可:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4

字节码指令

java虚拟机指令由一个字节长度的,代表某种特定操作含义的数字(称之为操作码),以及随后的代表此操作所需参数的操作数而构成。

操作码的长度为1个字节,所以最大只有256条

常量入栈指令

局部变量值转载到栈中指令

将栈顶值保存到局部变量中指令

wide指令

通用(无类型)栈操作指令

类型转换指令

整数运算

浮点运算

逻辑运算——移位运算

逻辑运算——按位布尔运算

控制流指令——条件跳转指令

控制流指令——比较指令

控制流指令——无条件跳转指令

控制流指令——表跳转指令

控制流指令——异常和finally

对象操作指令

数组操作指令

方法调用指令

方法返回指令

线程同步指令

指令参考:https://blog.csdn.net/web_code/article/details/12164733

一个简单的demo分析

Test.java
public class Test {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int c = a+b;
        System.out.println(c);
    }
}
javap -v Test.class
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #26.#27        // java/io/PrintStream.println:(I)V
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10    //把10扩展成int入栈
         2: istore_1      //将栈顶int类型值保存到局部变量1中
         3: bipush        20     //把20扩展成int入栈
         5: istore_2     //将栈顶int类型值保存到局部变量2中
         6: iload_1      //从局部变量1中装载int类型值入栈  
         7: iload_2     //从局部变量2中装载int类型值入栈  
         8: iadd       // 将栈顶两int类型数相加,结果入栈。
         9: istore_3     //将栈顶int类型值保存到局部变量3中
        10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;获取静态字段的值。#2表示常量池的索引
        13: iload_3
        14: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V 运行时方法绑定调用方法。
        17: return      //void函数返回。

JVM-ZGC

低延迟垃圾收集器

Shenandoah和ZGC为什么被称为低延迟GC,因为它几乎整个工作过程全部都是并发的,只有初始标记、最终标记这些阶段有短暂的停顿,这部分停顿的时间基本上是固定的,与堆的容量、堆中对象的数量没有正比例关系。实际上,它们都可以在任意可管理的(譬如现在ZGC只能管理4TB以内的堆)堆容量下,实现垃圾收集的停顿都不超过十毫秒这种以前听起来是天方夜谭、匪夷所思的目标。这两款目前仍处于实验状态的收集器,被官方命名为“低延迟垃圾收集器”。

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),三者共同构成了一个“不可能三角”。

1. Shenandoah垃圾回收器

比起稍后要介绍的有着Oracle正朔血统的ZGC,Shenandoah反而更像是G1的下一代继承者。使用转发指针(Forwarding Pointer,也常被称为Indirection Pointer)来实现对象移动与用户程序并发的一种解决方案。

那Shenandoah相比起G1又有什么改进呢?

虽然Shenandoah也是使用基于Region的堆内存布局,同样有着用于存放大对象的HumongousRegion,默认的回收策略也同样是优先处理回收价值最大的Region……但在管理堆内存方面,它与G1至少有三个明显的不同之处,最重要的当然是支持并发的整理算法,G1的回收阶段是可以多线程并行的,但却不能与用户线程并发,这点作为Shenandoah最核心的功能稍后笔者会着重讲解。其次,Shenandoah(目前)是默认不使用分代收集的,换言之,不会有专门的新生代Region或者老年代Region的存在,没有实现分代,并不是说分代对Shenandoah没有价值,这更多是出于性价比的权衡,基于工作量上的考虑而将其放到优先级较低的位置上。最后,Shenandoah摒弃了在G1中耗费大量内存和计算资源去维护的记忆集,改用名为“连接矩阵”(Connection Matrix)的全局数据结构来记录跨Region的引用关系,降低了处理跨代指针时的记忆集维护消耗,也降低了伪共享问题。

Shenandoah收集器的工作过程大致可以划分为以下九个阶段
  1. 初始标记 这个阶段仍是“Stop The World”的,但停顿时间与堆大小无关,只与GC Roots的数量相关

  2. 并发标记 与G1一样,遍历对象图,标记出全部可达的对象,这个阶段是与用户线程一起并发的,时间长短取决于堆中存活对象的数量以及对象图的结构复杂程度。

  3. 最终标记 与G1一样,处理剩余的SATB扫描,并在这个阶段统计出回收价值最高的Region,将这些Region构成一组回收集(Collection Set)。最终标记阶段也会有一小段短暂的停顿。

  4. 并发清理 这个阶段用于清理那些整个区域内连一个存活对象都没有找到的Region

  5. 并发回收 在这个阶段,Shenandoah要把回收集里面的存活对象先复制一份到其他未被使用的Region之中。复制对象这件事情如果将用户线程冻结起来再做那是相当简单的,但如果两者必须要同时并发进行的话,就变得复杂起来了。其困难点是在移动对象的同时,用户线程仍然可能不停对被移动的对象进行读写访问,移动对象是一次性的行为,但移动之后整个内存中所有指向该对象的引用都还是旧对象的地址,这是很难一瞬间全部改变过来的。对于并发回收阶段遇到的这些困难,Shenandoah将会通过读屏障和被称为“Brooks Pointers”的转发指针来解决(讲解完Shenandoah整个工作过程之后笔者还要再回头介绍它)。并发回收阶段运行的时间长短取决于回收集的大小

  6. 初始引用更新 并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址,这个操作称为引用更新。

  7. 并发引用更新 真正开始进行引用更新操作,这个阶段是与用户线程一起并发的,时间长短取决于内存中涉及的引用数量的多少。并发引用更新与并发标记不同,它不再需要沿着对象图来搜索,只需要按照内存物理地址的顺序,线性地搜索出引用类型,把旧值改为新值即可。

  8. 最终引用更新 解决了堆中的引用更新后,还要修正存在于GC Roots中的引用。这个阶段是Shenandoah的最后一次停顿,停顿时间只与GC Roots的数量相关。

  9. 并发清理 经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,最后再调用一次并发清理过程来回收这些Region的内存空间,供以后新对象分配使用。

Shenandoah收集器性能

2016年做该测试时的Shenandoah并没有完全达成预定目标,停顿时间比其他几款收集器确实有了质的飞跃,但也并未实现最大停顿时间控制在十毫秒以内的目标,而吞吐量方面则出现了很明显的下降。

Shenandoah的性能在日益改善,逐步接近“Low-Pause”的目标。此外,RedHat也积极拓展Shenandoah的使用范围,将其Backport到JDK 11甚至是JDK 8之上,让更多不方便升级JDK版本的应用也能够享受到垃圾收集器技术发展的最前沿成果。

2. ZGC收集器

ZGC是一款在JDK 11中新加入的具有实验性质[插图]的低延迟垃圾收集器,是由Oracle公司研发的。2018年Oracle创建了JEP 333将ZGC提交给OpenJDK,推动其进入OpenJDK 11的发布清单之中。

ZGC和Shenandoah的目标是高度相似的,都希望在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。但是ZGC和Shenandoah的实现思路又是差异显著的。

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

并发整理算法的实现

Shenandoah使用转发指针和读屏障来实现并发整理,ZGC虽然同样用到了读屏障,但用的却是一条与Shenandoah完全不同,更加复杂精巧的解题思路。

ZGC收集器有一个标志性的设计是它采用的染色指针技术(Colored Pointer),直接把标记信息记在引用对象的指针上。指针对于计算机来讲,它也是一个信息的载体,但是目前而言,内存中的理论可访问信息是远大于实际需求的,尽管Linux高18位不能用来寻址,但剩余的46位也足以满足需求,所以ZGC团队就将指针信息载体进行染色,将其高4位用来存储四个记号信息,通过这些标志位,虚拟机可以直接从指针中看到其引用对象的三色标记状态、是否进入了重分配集(即被移动过)、是否只能通过finalize()方法才能被访问到。

由于这些标志位进一步压缩了原本就只有46位的地址空间,也直接导致ZGC能够管理的内存不可以超过4TB(2的42次幂)。

染色指针的三大优势:
  1. 染色指针可以使得一旦某个Region的存活对象被移走之后,这个Region立即就能够被释放和重用掉,而不必等待整个堆中所有指向该Region的引用都被修正后才能清理。

  2. 染色指针可以大幅减少在垃圾收集过程中内存屏障的使用数量,设置内存屏障,尤其是写屏障的目的通常是为了记录对象引用的变动情况,如果将这些信息直接维护在指针中,显然就可以省去一些专门的记录操作。

  3. 染色指针可以作为一种可扩展的存储结构用来记录更多与对象标记、重定位过程相关的数据,以便日后进一步提高性能。

Java虚拟机作为一个普普通通的进程,这样随意重新定义内存中某些指针的其中几位,操作系统是否支持?处理器是否支持?

这里面的解决方案要涉及虚拟内存映射技术。把染色指针中的标志位看作是地址的分段符,那只要将这些不同的地址段都映射到同一个物理内存空间,经过多重映射转换后,就可以使用染色指针正常进行寻址了。

image

ZGC的运作过程大致可划分为以下四个大的阶段
  1. 并发标记(Concurrent Mark):并发标记是遍历对象图做可达性分析的阶段,与G1、Shenandoah不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。

  2. 并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,ZGC划分Region的目的并非为了像G1那样做收益优先的增量回收,而实用范围更大的扫描成本换取省去G1中记忆集的维护成本。此外,在JDK12的ZGC中开始支持的类卸载以及弱引用的处理,也是在这个阶段中完成的。

  3. 并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。得益于染色指针的支持,ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。

  4. 并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用。ZGC的并发重映射并不是一个必须要“迫切”去完成的任务,因为前面提到ZGC有"自愈"能力,最坏也就多跳转一层,这时候,一旦所有指针都被修正之后,原来记录新旧对象关系的转发表就可以释放掉了。

ZGC收集器性能
  1. ZGC的设计理念是迄今垃圾收集器研究的最前沿成果,可是,必定要有优有劣才会称作权衡,ZGC的这种选择[插图]也限制了它能承受的对象分配速率不会太高,目前唯一的办法就是尽可能地增加堆容量大小,获得更多喘息的时间。

  2. ZGC还有一个常在技术资料上被提及的优点是支持“NUMA-Aware”非统一内存访问架构的内存分配。由于摩尔定律逐渐失效,现代处理器因频率发展受限转而向多核方向发展,这就造成了如果要访问被其他处理器核心管理的内存,就必须通过Inter-Connect通道来完成,这要比访问处理器的本地内存慢得多,ZGC收集器会优先尝试在请求线程当前所处的处理器的本地内存上分配对象,以保证高效内存访问。

  3. 在ZGC的“弱项”吞吐量方面,以低延迟为首要目标的ZGC已经达到了以高吞吐量为目标Parallel Scavenge的99%,直接超越了G1。

  4. ZGC均能毫不费劲地控制在十毫秒之内

image

image

JVM-三种常量池

常量池

我的理解是Class文件常量池先被加载到堆里,然后解析完后放到运行时常量池中,然后将运行时常量池存放到元空间。

Java中的常量池分为三种类型:

  • 静态常量池(也称class文件常量池)(The Constant Pool)
  • 运行时常量池(The Run-Time Constant Pool)
  • String常量池
静态常量池

存在于class文件中

所处区域:堆

诞生时间:编译时

内容概要:符号引用和字面量

class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用

运行时常量池

存在于元空间

诞生时间:JVM运行时

内容概要:class文件元信息描述,编译后的代码数据,引用类型数据,类文件常量池。

String常量池

存在于堆中

在HotSpot VM里实现的string pool功能的是一个StringTable类,它是一个哈希表,里面存的是驻留字符串(也就是我们常说的用双引号括起来的)的引用(而不是驻留字符串实例本身),也就是说在堆中的某些字符串实例被这个StringTable引用之后就等同被赋予了”驻留字符串”的身份。这个StringTable在每个HotSpot VM的实例只有一份,被所有的类共享。

JVM-内存泄漏

首先OOM原因有很多:

  • java.lang.OutOfMemoryError: Java heap space 。如果没有代码无限制new对象,一般可通过JVM调参解决。
  • java.lang.OutOfMemoryError: PermGen space 。 可采用-XX:MaxPermSize调节大小
  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit 尝试分配比堆大的数组
  • java.lang.OutOfMemoryError: request bytes for . Out of swap space? 本机swap空间不足
  • java.lang.OutOfMemoryError: (Native method)
  1. 先根据报错确定原因。初步定位是不是调参可以解决的。
  2. 调参解决不了, 分析thread dump、heap dump。 jmap -dump:live打印堆日志 jstack -l 打印线程日志jstat -gc <pid> <period> <times>查看gc信息
  3. 使用MAT、jhat、等分析堆日志。 也可以借助jconsole 分析线程死锁、内存使用等。

元空间:1.8后取消了永久代(方法区),改为元空间,类的信息存放在元空间,元空间不使用堆内存,使用的是本地内存,理论上讲本地内存有多大,元空间就有多大。

从JVM视角分析try-catch性能

实验1:简单认识

随便写一个简单的程序

public class Test {
    public static void main(String[] args) {
        int a = 0,b=2;
        try {
            a = b/0;
        }catch (Exception e){
            a = 1;
        }
    }
}

看一下字节码指令过程:

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_0        push 0
         1: istore_1        pop并保存到局部变量1
         2: iconst_2       push 2
         3: istore_2       pop并保存到局部变量2
         4: iload_2        从局部变量里拿出并push
         5: iconst_0        push 0 
         6: idiv          栈顶两数相除
         7: istore_1
         8: goto          14
        11: astore_3
        12: iconst_1
        13: istore_1
        14: return
      Exception table:
         from    to  target type
             4     8    11   Class java/lang/Exception
      LineNumberTable:
        line 8: 0
        line 10: 4
        line 13: 8
        line 11: 11
        line 12: 12
        line 14: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       2     3     e   Ljava/lang/Exception;
            0      15     0  args   [Ljava/lang/String;
            2      13     1     a   I
            4      11     2     b   I
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 11
          locals = [ class "[Ljava/lang/String;", int, int ]
          stack = [ class java/lang/Exception ]
        frame_type = 2 /* same */
}

可以看到有个异常表:

      Exception table:
         from    to  target type
             4     8    11   Class java/lang/Exception

from表示try catch的开始地址
to表示try catch的结束地址
target表示异常的处理起始位
type表示异常类名称

代码运行时出错时,会先判断出错位置是否在from - to的范围,如果是,则从target标志位往下执行,如果没有出错,直接gotoreturn。可以看出,如果代码不出错的话,性能几乎是不受影响的,和正常的代码执行是一样的。

那异常处理耗时是什么个概念呢?

实验二:异常处理耗时测试

public class Test {
    public static void main(String[] args) {
        int a = 0,b=2;
        long startTime = System.nanoTime();
        for (int i = 10; i>0;i--){
            try {
                a = b/i;
            }catch (Exception e){
                a = 1;
            }finally {

            }
        }
        long runTime = System.nanoTime()-startTime;
        System.out.println(runTime);
    }
}

我只需要把i>0改成i>=0,程序遍会进行一次异常处理,因为除数不能为0.

我在修改之前(无异常运行),运行的结果是1133
修改之后(会出现除数为0异常),运行结果是44177

当然,这个结果和cpu的算力有关,多次运行结果相差无几。

所以,可以看出一旦程序进入到catch里,是非常耗资源的。

那try catch 在for循环外面或者里面,哪个更好呢?

实验三:for循环在try里面

public class Test {
    public static void main(String[] args) {
        int a = 0,b=2;
        long startTime = System.nanoTime();
        try {
            for (int i = 10; i>=0;i--){
                    a = b/i;
            }
        }catch (Exception e){
            a = 1;
        }finally {
            long runTime = System.nanoTime()-startTime;
            System.out.println(runTime);
        }
    }
}

运行多次的控制台输出:

46820     48708 54749  47953   46820  45310
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: istore_2
         4: bipush        10
         6: istore_3
         7: iload_3
         8: iflt          21
        11: iload_2
        12: iload_3
        13: idiv
        14: istore_1
        15: iinc          3, -1
        18: goto          7
        21: goto          35
        24: astore_3
        25: iconst_1
        26: istore_1
        27: goto          35
        30: astore        4
        32: aload         4
        34: athrow
        35: return
      Exception table:
         from    to  target type
             4    21    24   Class java/lang/Exception
             4    21    30   any
            24    27    30   any
            30    32    30   any

实验四:try在for循环外面

public class Test {
    public static void main(String[] args) {
        int a = 0,b=2;
        long startTime = System.nanoTime();
        for (int i = 10; i>=0;i--){
            try {
                a = b/i;
            }catch (Exception e){
                a = 1;
            }finally {

            }
        }
                long runTime = System.nanoTime()-startTime;
                System.out.println(runTime);
    }
}

控制台打印:

42289  47953  49463  45688  45310
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=6, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_2
         3: istore_2
         4: bipush        10
         6: istore_3
         7: iload_3
         8: iflt          36
        11: iload_2
        12: iload_3
        13: idiv
        14: istore_1
        15: goto          30
        18: astore        4
        20: iconst_1
        21: istore_1
        22: goto          30
        25: astore        5
        27: aload         5
        29: athrow
        30: iinc          3, -1
        33: goto          7
        36: return
      Exception table:
         from    to  target type
            11    15    18   Class java/lang/Exception
            11    15    25   any
            18    22    25   any
            25    27    25   any

综合实验三和实验四,我们发现无论从运行时长还是从字节码指令的角度看,它两的性能可以说是一样的。并没有你感觉到的for循环里放try代码会冗余、资源消耗加倍的问题。

但是从运行逻辑来看,两个是有点不同的,实验三中,因为for在try catch里,所以jvm在编译的时候,把异常处理放在for循环后面才进行。即:第24-27行;实验四中,异常处理是在for循环内部的,即:第18-22行。大同小异。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值