• 【Java虚拟机】万字长文,搞定JVM方方面面!


    1.JVM内存结构

    1.1.JVM内存结构图

    在这里插入图片描述

    在这里插入图片描述

    1.2.程序计数器

    Program Counter Register程序计数器(寄存器)

    在这里插入图片描述

    • **作用:**记住下一条JVM指令的执行地址。
    • 特点:
      • 线程私有化,每个线程独有一个程序计数器。
      • 不会存在内存溢出。

    在这里插入图片描述

    1.3.虚拟机栈

    Java Virtual Machine Stacks(java虚拟机栈)

    • 每个线程运行时所需要的内存,称为虚拟机栈。
    • 每个栈由多个栈帧(Frame)组成,对应这每次方法调用时所占的内存。
    • 每个线程只能有一个活动栈帧,对应这当前正在执行的那个方法。

    在这里插入图片描述

    在这里插入图片描述

    • 问题分析
      • 垃圾回收是否涉及栈内存?
        • 栈内存主要是方法执行的内存,每个方法执行完成后,会自动的弹出栈,所以无需垃圾回收机制,垃圾回收只是在堆内存中无用的对象中使用。
      • 栈内存分配的越大越好吗?
        • 不是栈内存根据操作系统来分配就好,如果栈内存分配的过大的话,会影响同时调用的线程数,比如500M的内存,栈内存分配1M,这样就可以有500个线程执行,如果分配2M就只能有250个线程执行。
      • 方法内的局部变量是否为线程安全的?
        • 如果方法内局部变量没有逃离方法的作用访问,他是线程安全的,因为是每个线程独立的变量。
        • 如果是局部变量引用了对象,并且逃离了方法的作用范围,其他线程可以得到这个值,这会就要考虑线程安全性问题。

    在这里插入图片描述

    2、栈内存溢出

    • 栈帧过多导致栈内存溢出
    • 栈帧过大导致栈内存溢出

    (1)演示栈内存溢出场景,方法递归调用

    /**
     * 演示栈内存溢出
     */
    public class Demo1 {
        private static int count = 0;
        public static void main(String[] args) {
            try{
                //方法入口调用
                addCount();
            }catch (Throwable e){
                e.printStackTrace();
                System.out.println(count);
            }
        }
    
        /**
         * 方法递归调用,让其栈内存溢出
         */
        private static void addCount(){
            count++;
            addCount();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    (2)如何设置栈内存的大小

    在这里插入图片描述

    在这里插入图片描述

    (2)演示json格式转换栈内存溢出

    • 准备员工和部门的实体
    /**
     * 部门对象
     */
    public class Dept {
        //部门名称
        private String name;
        //员工集合
        private List<Emp> empList;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public List<Emp> getEmpList() {
            return empList;
        }
    
        public void setEmpList(List<Emp> empList) {
            this.empList = empList;
        }
    
        @Override
        public String toString() {
            return "Dept{" +
                    "name='" + name + '\'' +
                    ", empList=" + empList +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    /**
     * 员工对象
     */
    public class Emp {
        //员工姓名
        private String name;
        //部门对象
        private Dept dept;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Dept getDept() {
            return dept;
        }
    
        public void setDept(Dept dept) {
            this.dept = dept;
        }
    
        @Override
        public String toString() {
            return "Emp{" +
                    "name='" + name + '\'' +
                    ", dept=" + dept +
                    '}';
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 主方法测试
    public class Main {
        public static void main(String[] args) throws JsonProcessingException {
            Dept dept = new Dept();
            dept.setName("Market");
    
            Emp emp1 = new Emp();
            emp1.setName("张山");
            emp1.setDept(dept);
    
            Emp emp2 = new Emp();
            emp2.setName("李四");
            emp2.setDept(dept);
    
            dept.setEmpList(Arrays.asList(emp1,emp2));
    
            ObjectMapper mapper = new ObjectMapper();
    
            System.out.println(mapper.writeValueAsString(dept));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    • 问题分析:
    问题定位:
    
    {name :'Marker' , empList:[ {name:'张三'},dept:{name: 'Market',empList:[{name:'张三'},dept:{name:'张三',empList:[...]}]}]}
    
    Dept对象中有Emp的List集合,每个Emp中又持有Dept对象,Dept对象中又持有Emp的List集合,无限循环导致方法调用栈内存溢出。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 解决办法:
    //在员工实体中把Dept对象加入@JsonIgnore,JsonIgnore是在json序列化时将pojo类中的一些属性忽略掉,标记在属性或者方法上,返回的json数据即不包含该属性。
    @JsonIgnore
    private Dept dept;
    
    • 1
    • 2
    • 3

    3、线程运行诊断

    案例:cpu占用过多

    定位:

    • 用top定位哪个进程对cpu的占用使用过高
    • ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
    • jstack 进程id
      • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号
    1.4.本地方法栈

    ​ 本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。

    Navtive 方法是 Java 通过 JNI 直接调用本地 C/C++ 库,可以认为是 Native 方法相当于 C/C++ 暴露给 Java 的一个接口,Java 通过调用这个接口从而调用到 C/C++ 方法。当线程调用 Java 方法时,虚拟机会创建一个栈帧并压入 Java 虚拟机栈。然而当它调用的是 native 方法时,虚拟机会保持 Java 虚拟机栈不变,也不会向 Java 虚拟机栈中压入新的栈帧,虚拟机只是简单地动态连接并直接调用指定的 native 方法。

    在这里插入图片描述

    • 本地方法栈是一个后入先出(Last In First Out)栈。
    • 由于是线程私有的,生命周期随着线程,线程启动而产生,线程结束而消亡。
    • 本地方法栈会抛出 StackOverflowErrorOutOfMemoryError 异常。
    1.5.Java堆

    1、堆简介

    • Heap(堆):通过new关键字,创建对象都会使用堆内存。
    • 特点:
      • 它是线程共享的,堆中对象都需要考虑线程安全的问题。
      • 有垃圾回收机制
    • 对于多数应用来说,Java堆(Java Heap)是Java虚拟机管理的最大一块内存。Java堆被所有线程共享,在虚拟机启动的时候创建。此内存区域的唯一目的就是存放对象实例。

    2、堆内存溢出

    (1)示例代码

    /**
     * 演示堆内存溢出
     */
    public class Demo1 {
    
        public static void main(String[] args) {
            int i = 0;
            try{
                List<String> list = new ArrayList<>();
                String a = "hello";
                while(true){
                    list.add(a);
                    a = a + a;
                    i++;
                }
            }catch (Throwable e){
                e.printStackTrace();
                System.out.println(i);
            }
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    (2)分析原因

    在这里插入图片描述

    3、堆内存诊断

    (1)jps工具

    • 查看当前系统种有哪些java进程

    (2)jmap工具

    • 查看堆内存占用情况

    (3)jconsole工具

    • 图形化界面,多功能检测
    public class Demo2 {
        public static void main(String[] args) throws InterruptedException {
            System.out.println("1...");
            Thread.sleep(30000);
            byte[] array = new byte[1024 * 1024 * 10];
            System.out.println("2...");
            Thread.sleep(30000);
            array = null;
            System.gc();
            System.out.println("3...");
            Thread.sleep(10000000L);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    查看但钱进程使用的堆内存情况:jmap -heap 进程号

    在这里插入图片描述

    使用jconsole图形化界面分析

    在这里插入图片描述

    使用jvisualvm图形化界面分析

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-在这里插入图片描述

    1.6.方法区

    1、方法区简介

    方法区(Method Area)也是所有线程共享的内存区域,用于存储虚拟机加载的类信息、常量、静态变量、即时编译器编译的代码等数据。也称作Non-Heap非堆内存。

    Java虚拟机规范堆方法区的限制非常宽松,可以选择不实现垃圾收集,但是这部分区域的回收确实是有必要的。

    平时,说到永久带(PermGen space)的时候往往将其和方法区不加区别。这么理解在一定角度也说的过去。因为,《Java虚拟机规范》只是规定了有方法区这么个概念和它的作用,并没有规定如何去实现它。那么,在不同的 JVM 上方法区的实现肯定是不同的了。

    同时,大多数用的JVM都是Sun公司的HotSpot。在HotSpot上把GC分代收集扩展至方法区,或者说使用永久代来实现方法区。

    在JDK1.8及以后版本,永久带被移除,新出现的元空间(Metaspace)替代了它。元空间属于Native Memory Space

    • JVM1.6的方法区与JVM1.8的方法区对比

    在这里插入图片描述

    2、方法区内存溢出

    • 示例代码:
    public class Demo4 extends ClassLoader{ //继承ClassLoader可以用来加载类的二进制字节码
    
        public static void main(String[] args) {
            int j = 0;
            Demo4 demo4 = new Demo4();
            try{
                for (int i = 0; i < 100000; i++,j++) {
                    //ClassWriter作用是生成类的二进制字节码
                    ClassWriter cw = new ClassWriter(0);
                    //版本号,public,类名,父类,接口
                    cw.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC,"Class"+i,null,"java/lang/Object",null);
                    //返回byte[]
                    byte[] code = cw.toByteArray();
                    //执行了类的加载
                    demo4.defineClass("Class"+i,code,0,code.length); //Class对象
                }
            }finally {
                System.out.println(j);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (1)1.8以前会导致永久代内存溢出

    演示永久代内存溢出:java.lang.OutOfMemoryError: PermGen space
    设置堆内存: -XX:MaxPermSize=8m
    
    • 1
    • 2

    在这里插入图片描述

    (2)1.8以后会导致元空间内存溢出

    演示元空间内存溢出:java.lang.OutOfMemoryError: Metaspace
    设置堆内存: -XX:MaxMetaspaceSize=8m
    注意:元空间依赖于系统内存的大小,所以一般很难演示出元空间溢出
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    3、常量池

    • 以一段代码进行分析:
    //二进制字节码存放的有 :类的基本信息、常量池、类方法定义、也包含了虚拟机指令
    public class Demo5 {
        public static void main(String[] args) {
            System.out.println("I am LiXiang");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • javap -v Demo5.class 查看Demo5编译后的指令,java文件编译后为二进制字节码文件

    (1)类的基本信息

    在这里插入图片描述

    (2)常量池

    在这里插入图片描述

    (3)类方法定义、虚拟机指令

    在这里插入图片描述

    (4)java程序编译成字节码文件的执行过程

    在这里插入图片描述

    0: getstatic    #2  // Field java/lang/System.out:Ljava/io/PrintStream;
    
    获取一个静态变量,对应常量池的 #2步骤
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    再继续寻找常量池的#21、#22步骤
    
    • 1

    在这里插入图片描述

    再继续寻找常量池的#28、#29、30步骤
    
    • 1

    在这里插入图片描述

    3: ldc    #3   // String I am LiXiang
    
    读取字符串,对应常量池#3,#23
    
    • 1
    • 2
    • 3

    在这里插入图片描述
    在这里插入图片描述

    5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    方法执行,调用#4、#24、#25、#31、#32、#33
    
    • 1
    • 2

    在这里插入图片描述

    在这里插入图片描述

    4、运行时常量池

    运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后进入方法区的运行时常量池中存放。

    1.7.StringTable

    1、常量池与串池的关系

    字符串池在JDK1.7之后存在于堆中的一块区域,String s1 = "abc"这样声明的字符串会放入字符串池中,String s1 = new String("abcd")会在字符串池有一个"abcd"的字符串对象,堆中也有1个,2个不同。

    • 字符串池可以避免重复创建字符串对象
    • 常量池中的字符串仅是符号,第一次用到时才变为对象
    • 它的结构为hash表结构,相同的字符串只存在一份

    示例代码:

    public class Demo6 {
    
        //串池是HashTable结构,不能扩容,串池中相同元素只会被添加一次
        //常量池中的信息,都会被加载到运行时常量池中,这时a b ab 都是常量池的符号,还没有变为java字符串对象
        //ldc #2 会把a符号变为"a"字符串对象,并且放入StringTable串池 StringTable["a"]
        //ldc #3 会把b符号变为"b"字符串对象,并且放入StringTable串池 StringTable["a","b"]
        //ldc #4 会把ab符号变为"ab"字符串对象,并且放入StringTable串池 StringTable["a","b","ab"]
    
        public static void main(String[] args) {
            String s1 = "a";
            String s2 = "b";
            String s3 = "ab";
            String s4 = s1+s2;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    2、字符串变量拼接

        public static void main(String[] args) {
            String s1 = "a";
            String s2 = "b";
            String s3 = "ab";
            String s4 = s1+s2;
    
            System.out.println(s3 == s4);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    在这里插入图片描述

    3、编译器优化

        public static void main(String[] args) {
            String s1 = "a";
            String s2 = "b";
            String s3 = "ab";
            String s4 = "a"+"b";
    
            System.out.println(s3 == s4);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    在这里插入图片描述

    4、StringTable特性

    • 常量池中的字符仅是符号,第一次用到的时候才变为对象。
    • 利用串池的机制,来避免重复创建字符串对象。
    • 字符串变量拼接的原理是StringBuilder(1.8)。
    • 字符串常量拼接的原理是编译期优化。
    • 可以用intern方法,主动的将串池中还没有的字符串放入串池。

    5、intern()方法将对象放入串池

    public class Demo8 {
        //["a","b"]
        public static void main(String[] args) { //1983
    
            String s = new String("a") + new String("b"); //new String("ab")
    
            //堆 new String("a") new String("b") new String("ab")
            String s2 = s.intern();//将这个字符串尝试放入串池,如果有则并不会被放入,如果没有则放入串池。会把串池中的对象返回
    		
            //注意:intern()方法在1.6版本中是将堆中的数据拷贝一份,所以再用s取比较的时候就会出现false
            
            //s2与串池中的ab是相等的
            System.out.println("s2 == ab"+s2 == "ab");
            //同样s被放入串池中,所以与串池中的ab也是相等的
            System.out.println("s == ab"+s == "ab");
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    public class Demo9 {
        public static void main(String[] args) {
            String s1 = "a";
            String s2 = "b";
            String s3 = "a"+"b";
            String s4 = s1+s2;
            String s5 = "ab";
            String s6 = s4.intern();
    
            System.out.println(s3 == s4); //false,s4 = new String("ab")存放在堆中,s3存放在串池中
            System.out.println(s3 == s5); //true,s3存放在串池中,s5直接拿串池中的数据
            System.out.println(s3 == s6); //1.8中为true,1.6为false,因为1.6复制一份副本存放在串池中
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这里插入图片描述

    public class Demo9 {
        public static void main(String[] args) { 
    		String x2 = new String("c") + new String("d");
            x2.intern();
            String x1 = "cd";
            System.out.println(x1 == x2); //true,因为x2堆中的对象已经放入串池中,s1为串池中对象
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    public class Demo9 {
        public static void main(String[] args) { 
            String x1 = "cd";
    		String x2 = new String("c") + new String("d");
            x2.intern();
            System.out.println(x1 == x2); //false
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    6、StringTable的位置

    StringTable在1.6时存放在永久代中,在1.8中存放在堆中

    • 下面这段代码分别在1.6与1.8中执行

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    7、StringTable垃圾回收

    字符串常量也会触发垃圾回收机制

    • 初始化为1771个字符串常量

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    8、StringTable调优

    通过调整StringTable中桶的个数来提高读取效率。默认是60013个。

    调整:-XX:StringTableSize=桶个数

    桶的个数调大会明显提升读取速度。

    1.8.直接内存

    1、直接内存简介

    Direct Memory

    • 常见于NIO操作时,用于数据缓冲区
    • 分配回收成本较高,但读写性能高
    • 不受JVM内存回收管理

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    2、直接内存,内存溢出问题

    • 一直向List中添加100兆的数据

    在这里插入图片描述
    在这里插入图片描述

    3、直接内存释放原理

    (1)演示直接内存释放过程

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    (2)直接内存释放原理

    Java提供了Unsafe类用来进行直接内存的分配与释放

    Unsafe无法直接使用,需要通过反射来获取

    在这里插入图片描述

    (3)分析ByteBuffer.allocateDirect()怎末进行直接内存的创建与释放的

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    • 使用了Unsafe对象完成直接内存的分配回收,并且回收需要主动调用freeMemory方法
    • ByteBuffer的实现类内部,使用了Cleaner(虚引用)来检测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner的 clean方法调用freeMemory来释放直接内存。

    4、禁用显示回收堆直接内存的影响

    • 关闭GC显示调用:-XX:+DisableExplicitGC

    关闭显示调用GC,会导致直接内存无法释放的问题,我们可以通过Unsafe来释放内存。

    2.对象创建解析

    2.1.对象创建的流程

    1、对象创建的流程

    • 虚拟机遇到一条new指令时,首先检查这个对应的类能否在常量池中定位到一个类的符号引用。

    • 判断这个类是否已被加载、解析和初始化。

    • 为这个新生对象在Java堆中分配内存空间,其中Java堆分配内存空间的方式主要有以下两种

    • 指针碰撞

      • 分配内存空间包括开辟一块内存和移动指针两个步骤。
      • 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
    • 空闲列表

      • 分配内存空间包括开辟一块内存和修改空闲列表两个步骤。
      • 非原子步骤可能出现并发问题,Java虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。
    • 将分配到的内存空间都初始化为零值

    • 设置对象头相关数据

      • GC分代年龄
      • 对象的哈希码hashCode
      • 元数据信息
    • 执行对象方法

    • 图解

    在这里插入图片描述

    2、指针碰撞和空闲列表图解

    在这里插入图片描述

    2.2.对象的结构

    在这里插入图片描述

    2.3.对象的访问方式

    当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作栈上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用是第二种):

    1、使用句柄访问对象。即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针。

    2、直接指针访问对象。即reference中存储的就是对象地址,相当于一级指针。

    在这里插入图片描述

    对比

    • 垃圾回收分析:句柄方式访问对象当垃圾回收移动对象时,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址,直接指针访问对象垃圾回收时需要修改reference中存储的地址。

    • 访问效率分析,直接指针访问对象优于句柄方式访问对象,因为直接指针访问对象只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。

    3.JVM垃圾回收

    3.1.垃圾回收概述

    1、什么是垃圾(Garbage)

    • 垃圾是指运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。
    • 如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占用的内存空间会一直保留到应用程序结束,有可能影响其他对象的使用或者导致内存溢出

    2、为什么需要GC

    • 对于高级语言来说,不进行垃圾回收,内存迟早会被消耗完。

    • 释放没用的独享,垃圾回收也可以清除内存里的记录碎片,碎片整理将所有占用的堆内存放到堆的一段,以便于JVM将整理出的内存分配给的新的对象

    • 随着应付业务越来越庞大、复杂、用户越来越多,没有GC就不能保证应用程序的正常进行,经常造成STW的GC又跟不上实际的需求,所以才会不短地尝试对GC进行优化。

    • 3、Java垃圾回收机制

    • 自动内存管理,降低内存泄露和内存溢出的风险

    • 自动内存管理,减轻了Java程序员的内存管理负担,可以更专注于业务的开发

    3.2.引用计数法

    1、引用计数法概念

    • 引用计数法就是如果一个对象没有被任何引用指向,则可视之为垃圾。这种方法的缺点就是不能检测到环的存在。
    • 首先需要声明,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存。

    2、什么是引用计数算法

    • 给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;
    • 当引用失效时,计数器值减1.任何时刻计数器值为0的对象就是不可能再被使用的。

    3、那为什么主流的Java虚拟机里面都没有选用这种算法呢?

    其中最主要的原因是它很难解决对象之间相互循环引用的问题。

    在这里插入图片描述

    3.3.可达性分析算法

    1、可达性分析算法简介

    可达性分析算法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为回收对象,要至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

    通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为“引用链”,当一个对象到 GC Roots 没有任何的引用链相连时(从 GC Roots 到这个对象不可达)时,证明此对象不可用。

    在这里插入图片描述

    2、可作为GC Roots的对象

    (1)虚拟机栈(栈帧中的本地变量表)中引用的对象。(引用栈帧中的本地变量表的所有对象)

    (2)方法区中静态属性引用的对象(引用方法区该静态属性的所有对象)

    (3)方法区中常量引用的对象(引用方法区中常量的所有对象)

    (4)本地方法栈中(Native方法)引用的对象(引用Native方法的所有对象)

    在这里插入图片描述

    3.4.Java五种引用类型

    1、什么是引用

    • 每种编程语言都有自己操作内存中元素的方式,C、C++采用指针,而在Java中则是通过引用。
    • 在Java中一切都被视为对象,但是我们操作的标识符实际上是对象的一个引用(reference)。
    //创建一个引用,引用可以独立存在,并不一定需要与一个对象关联
    String s;
    
    • 1
    • 2
    • 通过这个引用指向某个对象,之后便可以通过这个引用来实现操作对象了。
    String str = new String("abc");
    System.out.println(str.toString());
    
    • 1
    • 2
    • jdk1.2之前,Java中的定义很传统,如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表着一个引用。

    在这里插入图片描述

    在这里插入图片描述

    2、强引用

    • Java中默认声明的就是强引用,比如:
    Object obj = new Object(); //只要obj还指向Object对象,Object对象就不会被回收
    obj = null; //手动设置成null
    
    • 1
    • 2

    强引用只能够通过GC Root的引用链找到就不会被回收,也就是说强引用只有当GC Roots全部断开时才会被回收。

    案例:设置JVM参数,起始堆内存为2m:-Xms2m 最大堆内存为3m:-Xmx3m

    public class Demo13 {
    
        public static void main(String[] args) {
            testStrongReference();
        }
    
        private static void testStrongReference(){
            //当new byte为1M 的时候,程序运行正常
            byte [] buff = new byte[1024 * 1024 *1];
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时,JVM也会直接抛出OOM(OutOfMemoryError),不回去回收。如果想中断强引用与对象之间的关系,可以显示的将强引用赋值成null,这样,JVM就可以在适当的时刻回收对象了。

    3、软引用

    • 软引用是用来描述一些非必需但是仍有用的对象。**在内存足够的时候,软引用对象不会被回收,只有在内存不足的时候,系统才会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出OOM的异常。**这种特性一般被用来实现缓存技术。

    • 在JDK1.2之后,用java.lang.ref.SoftReference类来表示软引用。

    案例:创建多个对象,软引用指向,不会出现内存溢出,当内存不足时,会自动的回收掉

    public class Demo13 {
    
        private static List<Object> list = new ArrayList<>();
        public static void main(String[] args) {
            testStrongReference();
        }
    
        private static void testStrongReference(){
    
            for (int i = 0; i < 10; i++) {
    
                byte [] buff = new byte[1024 * 1024]; 
                //指向软引用
                SoftReference<byte[]> sr = new SoftReference<>(buff);
                list.add(sr);
            }
    
            System.gc(); //显示调用gc
    
            for (int i = 0; i < list.size(); i++) {
                Object obj = ((SoftReference) list.get(i)).get();
                System.out.println(obj);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    在这里插入图片描述

    案例:软引用配合引用队列使用,当内存不足时,会自动的回收掉

    public class Demo14 {
        private static final int _1GB = 1024*1024;
        public static void main(String[] args) {
    
            List<WeakReference<byte[]>> list = new ArrayList<>();
    
            //引用队列
            ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
            for (int i = 0; i < 5; i++) {
                //关联了引用队列,当软引用所关联的byte数组被回收时,软引用自己会加入到引用队列中
                WeakReference<byte[]> reference = new WeakReference<>(new byte[_1GB],queue);
    
                System.out.println(reference.get());
    
                list.add(reference);
    
                System.out.println(list.size());
            }
    
            //从队列获取无用的软引用对象并移除
            Reference<? extends byte[]> poll = queue.poll();
    
            while (poll!=null){
                list.remove(poll);
                poll=queue.poll();
            }
    
            System.out.println("循环结束");
            for (WeakReference<byte[]> reference : list) {
                System.out.println(reference.get());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这里插入图片描述

    这里就说明了在内存不足的情况下,软引用将会被自动回收。

    但是值的注意的一点,即使有byte[] buff引用指向对象,且buff是一个strong reference,但是SoftReference sr指向的对象仍然被回收了,这是因为Java的编译器发现了在之后的代码中,buff已经没有被引用了,所有自动进行了优化。

    4、弱引用

    弱引用的引用强度比软引用更弱一些,**无论内存是否足够,只要JVM开始进行垃圾回收,那些被弱引用关联的对象都会被回收。**在JDK1.2之后,用java.lang.ref.WeakReference来表示弱引用。

    案例:测试弱引用,无论内存是否充足,都会被回收掉

    public class Demo13 {
    
        private static List<Object> list = new ArrayList<>();
        public static void main(String[] args) {
            testStrongReference();
        }
    
        private static void testStrongReference(){
            for (int i = 0; i < 10; i++) {
    
                byte [] buff = new byte[1024 * 1024];
                //指向软引用
                WeakReference<byte[]> str = new WeakReference<>(buff);
                list.add(str);
            }
    
            System.gc(); //显示调用gc
    
            for (int i = 0; i < list.size(); i++) {
                Object obj = ((WeakReference) list.get(i)).get();
                System.out.println(obj);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    5、虚引用

    虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能被回收,在JDK1.2之后,用PhantomReference类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个get()方法,而且它的get()方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和ReferenceQueue引用对列一起使用。

    例如:创建ByteBuffer的时候会创建一个名为Cleaner的虚引用对象,当ByteBuffer没有被强引用所引用就会被jvm垃圾回收,虚引用Cleaner就会被放入引用队列,会有专门的线程扫描引用队列,被发现后会调用直接内存地址的方法将直接内存释放掉,保证直接内存不会导致内存泄露

    public class PhantomReference<T> extends Reference<T> {
        /**
         * Returns this reference object's referent.  Because the referent of a
         * phantom reference is always inaccessible, this method always returns
         * null.
         *
         * @return  null
         */
        public T get() {
            return null;
        }
        public PhantomReference(T referent, ReferenceQueue<? super T> q) {
            super(referent, q);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    6、终结器引用

    创建的时候会关联一个引用队列,当对象没有被强引用所引用时,对象被垃圾回收时,会将终结器引用放入到一个引用队列中(被引用的对象暂时还没有被垃圾回收),有专门的线程(优先级较低,可能会造成对象迟迟不被回收)扫描引用队列并调用finallize()方法,第二次GC的时候才能回收掉被引用的对象。

    7、引用队列(ReferenceQueue)

    引用队列可以与软引用、弱引用以及虚引用一起配合使用,当垃圾回收器准备回收一个对象时,如果发现他还有引用,那么就会在回收对象之前,把这个引用加入到与之关联的引用队列中去。程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施。

    8、Reference对象状态变更

    在这里插入图片描述

    3.5.垃圾回收算法

    1、清除标记算法

    定义:Mark Sweep

    在这里插入图片描述

    ​ 标记清除算法会把不在GC Root链上的引用进行标记,然后清除,它的速度非常的快,但是有一个问题,它清除后的空间没有进行整理,会造成内存碎片,比如说我清理了一个2mb、一个3mb、一个5mb的内存对象,但是现在有一个对象需要8mb,由于没有进行内存整理,这个8mb的对象不能引用刚刚三个任何一个,所以只能等待,而且效率上不是很高。

    2、清除整理算法

    定义:Mark Compact

    在这里插入图片描述

    ​ 标记整理算法和标记清除算法差不多,只不过在清除标记出的内存空间后,会进行整理,所以相对的效率会低一点,但是不会产生内存碎片,上面说到的8mb的内存就可以申请到,将2mb、3mb、5mb整理成10mb的内存空间。

    3、复制算法

    定义:Copy

    在这里插入图片描述

    ​ 复制算法分为两块相等的内存空间,from存放对象,to为空内存空间,当标记出需要回收的地址空间时,会把当前在GC Root链上的对象放到to内存空间上,将from上的无用的地址空间全部删除,然后再to与from空间进行对调,这样做的好处是不会产生内存碎片,但是需要占用双倍的内存空间。

    3.6.分代垃圾回收

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述

    • 对象首先分配再伊甸园区域。
    • 新生代空间不足时,触发Minor GC,伊甸园和from中存活的对象copy到to中,存活的对象年龄加1并且交换from to幸存区。
    • Minor GC会引发stop the world,暂停其他用户线程,等垃圾回收结束之后,用户线程才能恢复运行。
    • 当对象寿命超过阈值时,会晋升至老年代,最大寿命是15(4bit)。
    • 当老年代空间不足时,会先尝试触发minor gc,如果之后空间仍然不足,那么就会触发Full GC,STW 的时间会更长。
    3.7.JVM相关参数
    含义参数
    堆初始大小-Xms
    堆最大大小-Xmx或-XX:MaxHeapSize=size
    新生代大小-Xmn或(-XX:NewSize=size + -XX:MaxNewSize=size)
    幸存区比例(动态)-XX:InitialSurvivorRatio=ratio和-XX:+UseAdaptiveSizePolicy
    幸存区比例-XX:SurvivorRatio=ratio
    晋升阈值-XX:MaxTenuringThreshold=threshold
    晋升详情-XX:+PrintTenuringDistribution
    GC详情-XX:+PrintGCDetails -verbose:gc
    FullGC前MinorGC-XX:+ScavengeBeforeFullGC

    1、JVM相关参数

    (1)-Xms:初始堆大小

    • 默认值为物理内存的1/64(<1GB),默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限度。

    (2)-Xmx:最大堆大小

    • 默认值为物理内存的1/4(<1GB),默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限度。

    (3)-Xmn:年轻代大小

    • 整个堆大小=年轻代大小+老年代大小。

    (4)-XX:NewSize:设置年轻代大小

    (5)-XX:MaxNewSize:年轻代最大值

    (6)-XX:PermSize:设置持久代初始值

    • 默认值为物理内存的1/64。

    (7)-XX:MaxPermSize:设置持久代最大值

    • 默认值为物理内存的1/4。

    (8)-Xss:每个线程的堆栈大小

    • JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256k。

    (9)-XX:ThreadStackSize:Thread Stack Size

    (10)-XX:NewRatio:年轻代(包括Eden和两个Survivor区)与老年代的比值

    • -XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5。
    • Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

    (11)-XX:SurvivorRatio:Eden区与Survivor区的大小比值

    • 设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10。

    (12)-XX:LargePageSizeInBytes:内存页的大小不可设置过大, 会影响Perm的大小

    (13)-XX:+DisableExplicitGC:关闭System.gc()

    • 这个参数需要严格的测试。

    (14)-XX:+AggressiveOpts:加快编译

    (15)-XX:+UseBiasedLocking:锁机制的性能改善

    (16)-Xnoclassgc:禁用垃圾回收

    (17)-XX:SoftRefLRUPolicyMSPerMB:每兆堆空闲空间中SoftReference的存活时间

    • 默认值为1s。

    (18)-XX:PretenureSizeThreshold:对象超过多大是直接在老年代分配

    • 默认值为0,不在老年代分配,单位字节 新生代采用Parallel Scavenge GC时无效 另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象。

    (19)-XX:+CollectGen0First:FullGC时是否先YGC

    • 默认值为false。

    2、并行收集器相关参数

    (1)-XX:+UseParallelGC:Full GC采用parallel MSC (此项待验证)

    • 选择垃圾收集器为并行收集器.此配置仅对年轻代有效.即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集.(此项待验证)。

    (2)-XX:+UseParNewGC:设置年轻代为并行收集

    • 可与CMS收集同时使用 JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值。

    (3)-XX:ParallelGCThreads:并行收集器的线程数

    • 此值最好配置与处理器数目相等 同样适用于CMS。

    (4)-XX:+UseParallelOldGC:年老代垃圾收集方式为并行收集(Parallel Compacting)

    • 这个是JAVA 6出现的参数选项。

    (5)-XX:MaxGCPauseMillis:每次年轻代垃圾回收的最长时间(最大暂停时间)

    • 如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。

    (6)-XX:+UseAdaptiveSizePolicy:自动选择年轻代区大小和相应的Survivor区比例

    • 设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。

    (7)-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比

    • 公式为1/(1+n)

    (8)-XX:+ScavengeBeforeFullGC:Full GC前调用YGC

    • 默认值为rue

    3、CMS相关参数

    (1)-XX:+UseConcMarkSweepGC:使用CMS内存收集

    • 测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明.所以,此时年轻代大小最好用-Xmn设置。

    (2)-XX:+AggressiveHeap

    • 试图是使用大量的物理内存 长时间大内存使用的优化,能检查计算资源(内存, 处理器数量) 至少需要256MB内存 大量的CPU/内存, (在1.4.1在4CPU的机器上已经显示有提升)

    (3)-XX:CMSFullGCsBeforeCompaction:多少次后进行内存压缩

    • 由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理。

    (4)-XX:+CMSParallelRemarkEnabled:降低标记停顿

    (5)-XX+UseCMSCompactAtFullCollection:在FULL GC的时候, 对年老代的压缩

    • CMS是不会移动内存的, 因此, 这个非常容易产生碎片, 导致内存不够用, 因此, 内存的压缩这个时候就会被启用。 增加这个参数是个好习惯。 可能会影响性能,但是可以消除碎片。

    (6)-XX:+UseCMSInitiatingOccupancyOnly:使用手动定义初始化定义开始CMS收集

    • 禁止hostspot自行触发CMS GC。

    (7)-XX:CMSInitiatingOccupancyFraction=70:使用cms作为垃圾回收 使用70%后开始CMS收集

    (8)-XX:CMSInitiatingPermOccupancyFraction:设置Perm Gen使用到达多少比率时触发

    (9)-XX:+CMSIncrementalMode:设置为增量模式

    • 用于单CPU情况。

    4、辅助信息

    (1)-XX:+PrintGC

    • 输出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

    (2)-XX:+PrintGCDetails

    • 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

    (3)-XX:+PrintGC:PrintGCTimeStamps

    • 可与-XX:+PrintGC -XX:+PrintGCDetails混合使用 输出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

    (4)-XX:+PrintGCApplicationStoppedTime:打印垃圾回收期间程序暂停的时间.可与上面混合使用

    • 输出形式:Total time for which application threads were stopped: 0.0468229 seconds。

    (5)-XX:+PrintHeapAtGC:打印GC前后的详细堆栈信息

    (6)XX:+PrintTenuringDistribution:查看每次minor GC后新的存活周期的阈值

    3.8.GC分析

    测试代码:添加JVM参数 -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc

    public class Demo16 {
    
        private static final int _512KB = 512 * 1024;
        private static final int _1MB = 1024 * 1024;
        private static final int _6MB = 6 * 1024 * 1024;
        private static final int _7MB = 7 * 1024 * 1024;
        private static final int _8MB = 8 * 1024 * 1024;
        public static void main(String[] args) {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    public class Demo16 {
    	//测试Minor GC回收,当添加的数据在伊甸园的空间范围内
        private static final int _512KB = 512 * 1024;
        private static final int _7MB = 7 * 1024 * 1024;
        public static void main(String[] args) {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_7MB]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    public class Demo16 {
    	//当添加的数据超过新生代的总容量时或对象达到年龄阈值,老年代空间足够的情况下,会向老年代中存放
        private static final int _512KB = 512 * 1024;
        private static final int _7MB = 7 * 1024 * 1024;
        public static void main(String[] args) {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_7MB]);
            list.add(new byte[_512KB]);
            list.add(new byte[_512KB]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这里插入图片描述

    public class Demo16 {
    
        //当添加的数据大于新生代的整体容量时,老年代空间足够的情况下,会直接存放在老年代,不会触发Minor GC
        private static final int _8MB = 8 * 1024 * 1024;
        public static void main(String[] args) {
            ArrayList<byte[]> list = new ArrayList<>();
            list.add(new byte[_8MB]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    3.9.垃圾回收器

    1、串行(Serial/Serial Old垃圾回收器)

    • 单线程

    • 堆内存较小,适合个人电脑

    • 指定JVM参数:-XX:+UseSerialGC = Serial + SerialOld

      • 这里要注意:Serial指定新生代用的垃圾回收算法是复制,SerialOld指定的是老年代用的垃圾回收算法是标记整理
    • 图解

    在这里插入图片描述

    • Serial垃圾收集器的特点
      • “Stop The World”,它进行垃圾收集时,必须暂停其他所有线程,直到他收集结束。在用户不可见的情况下把用户正常工作的线程全部停掉。
      • 使用场景:多用于桌面应用,Client端的垃圾回收器。
      • 桌面应用内存小,进行垃圾回收的时间比较短,只要不频繁发生停顿就可以接收。

    2、ParNew 收集器

    • ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括 Serial 收集器可用的所有控制参数(例如:-XX: SurvivorRatio、-XX: PretenureSize’ Threshold、-XX: HandlePromotionFailure 等)、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一样,在实现上,这两种收集器也共用了相当多的代码
    • parnew垃圾收集器的特点?

      • ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相比并没有太多创新之处,但它却是许多运行在 Server 模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
      • 使用-XX: ParallelGCThreads 参数来限制垃圾收集的线程数
      • 多线程操作存在上下文切换的问题,所以建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产生上下文切换消耗
    • 并发与并行的概念讲解 CMS垃圾回收器

      • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

      • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能

        会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个 CPU 上

    3、吞吐量优先(Parallel Scavenge垃圾收集器)

    • 多线程
    • 堆内存较大,多核cpu
    • 单位时间内,STW的时间最短
    • 指定JVM参数:
      • -XX:+UseParallelGC (新生代吞吐量回收器,复制算法) -XX:UseParallelOldGC(老年代吞吐量回收器,标记整理算法)
      • -XX:ParallelGCThreads=n (控制线程数)
      • -XX:MaxGCPauseMillis=ms (最大暂停的毫秒数,默认值是200ms,即STW的时间)
      • -XX:+UseAdaptiveSizePolicy (自适应的新生代大小调整策略)
      • -XX:GCTimeRatio=ratio (设置垃圾回收时间的占比,默认ratio是99 ,也就是说 1/1+99 = 0.01,100分钟里只能有1分钟进行垃圾回收)
    • 图解

    在这里插入图片描述

    4、响应时间优先(CMS垃圾回收器)

    • 多线程
    • 堆内存较大,多核cpu
    • 单次的STW的时间最短
    • 指定JVM参数
      • -XX:+UseConcMarkSweepGC (老年代的垃圾回收 并发,采用标记清除算法,当CMS垃圾回收处理剩余过多的垃圾碎片时,就会退化成单线程的SerialOld垃圾回收器) -XX:+UseParNewGC(新生代的)
      • -XX:ParallelGCThreads=n (并行的垃圾回收线程数)
      • -XX:CMSInitiatingOccupancyFraction=percent (执行CMS内存达到一定比值进行垃圾回收)
      • -XX:ConcGCThreads=threads (并发线程数,设置为并行的线程数的四分之一)
      • -XX:CMSScavengeBeforeRemark (在重新标记之前,先对新生代进行垃圾回收)
    • 图解

    在这里插入图片描述

    • CMS垃圾回收执行流程
      • 初始标记(CMS initial mark) -----标记一下 GC Roots 能直接关联到的对象,速度很快
      • 并发标记(CMS concurrent mark --------并发标记阶段就是进行 GC RootsTracing 的过程
      • 重新标记(CMS remark) -----------为了修正并发标记期间因用户程序导致标记产生变动的标记记录
      • 并发清除(CMS concurrent sweep)
    3.10.G1垃圾回收

    1、G1垃圾回收阶段

    定义:Garbage First

    • 2004 论文发布
    • 2009 JDK6u14加入体验
    • 2012 JDK7u4官方支持
    • 2017 JDK9默认

    适用场景

    • 同时注重吞吐量(Throughput)和低延迟(Low latency),默认是暂停200ms
    • 超大堆内存,会将堆划分成多个大小相等的Region
    • 整体上是标记+整理算法,两个区域之间用的是复制算法

    相关JVM参数

    • -XX:+UseG1GC (使用G1回收器)
    • -XX:G1HeapRegionSize=size (设置Region空间大小)
    • -XX:MaxGCPauseMillis=time (设置STW暂停时间)

    在这里插入图片描述

    2、Young Collection

    在这里插入图片描述

    3、Young Collection + CM

    • 在Young GC时会进行GC Root的初始标记
    • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定。
    • -XX:InitiatingHeapOccupancyPercent=percent (默认45%)

    在这里插入图片描述

    4、Mixed Collection

    • 会对伊甸园、幸存区、老年代进行全面的垃圾回收
    • 最终标记(Remark)会STW
    • 拷贝存活(Evacuation)会STW
    • -XX:MaxGCPauseMillis=ms

    在这里插入图片描述

    5、G1收集器的运作大致可分为以下几个步骤

    • 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象。
    • 并发标记(Concurrent Marking):从GC Root开始对堆中对象尽心可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
    • 最终标记(Final Marking):为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录。虚拟机将这段时间对象变化记录在线程 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs的数据合并到 Remembered Set 中。
    • 筛选回收(Live Data Counting and Evacuation)
    3.11.Full GC触发
    • SerialGC
      • 新生代内存不足发生的垃圾收集 - Minor GC
      • 老年代内存不足时发生垃圾收集 - Full GC
    • ParallelGC
      • 新生代内存不足发生的垃圾收集 - Minior GC
      • 老年代内存不足时发生的垃圾收集 - Full GC
    • CMS
      • 新生代内存不足发生的垃圾收集 - Minor GC
      • 老年代内存不足时,只有在并发失败时产生Full GC,没有并发失败时,产生Minor GC
    • G1
      • 新生代内存不足时发生的垃圾收集 - Minor GC
      • 老年代内存不足时,当老年代占堆内存的百分之45的时候,会触发Full GC回收,或者当垃圾产生的速度大于垃圾回收的速度时,会产生Full GC。
    3.12.新生代跨代引用

    1、什么是跨代引用

    跨代引用是指新生代中存在对老年代对象的引用,或者老年代中存在对新生代的引用。

    在这里插入图片描述

    2、跨代引用的问题

    ​ YGC时,为了找到年轻代中存活的对象,不得不遍历整个老年代,反之亦然。这种方案存在在极大的性能浪费。因为跨代引用是极少的,为了找出那么一丁点跨代引用,却遍历了整个老年代。

    • 解决方案:记忆集(Card Table)

    记忆集就是用来记录跨代引用的表,通过引入记忆集避免遍历老年代。以YGC为例说明,要回收年轻代,只需要引用年轻代对象的GC Root+记忆集,就可以判断出Young区对象是否存活,不必在遍历老年代。

    • 缺点

    存在滞后性,浪费一定的空间,如上图所示,YGC时实际上无引用对象实际是可以被回收的,但是由于老年代中被引用,所以无法被回收。

    3.13.GMS与G1重新标记

    在这里插入图片描述

    在这里插入图片描述

    工作流程上来看,CMS的重新标记,和G1的最终标记之前都是并发标记。

    既然时同时运行,用户程序就可能修改对象的引用关系,修改对象引用关系就可能影响GC回收。

    所以,CMS重新标记,G1最终标记都是为了解决一件事,那就是并发过程中用户程序修改了对象引用关系后,如何让GC收集器仍旧能正确回收垃圾对象的问题。

    在这里插入图片描述

    3.14.JVM部分新特性

    1、JDK 8u20字符串去重

    • 优点:节省了大量内存
    • 缺点:略微多占用了cpu时间,新生代回收时间略微增加
    • 开启字符串去重的JVM参数:-XX:+UseStringDeduplation 默认是开启的

    在这里插入图片描述

    • 将所有新分配的的字符串放入一个队列
    • 当新生代回收时,G1并发检查是否有字符串重复
    • 如果它们值一样,让它们引用同一个char[]
    • 注意:与String.intern()不一样
      • String.intern()关注的是字符串对象
      • 而字符串去重关注的是char[]
      • 在JVM内部,使用了不同的字符串表

    2、JDK 8u40并发标记类卸载

    ​ 所有对象都经过并发标记后,就能直到哪些类不在被使用,当一个类加载器的所有类都不在使用,则卸载它所加载的所有类。

    -XX:+ClassUnloadingWithConcurrentMark 默认启用

    3、JDK 8u60回收巨型对象

    • 一个对象大于region的一半时,称之为巨型对象
    • G1不会对巨型对象进行拷贝
    • 巨型对象回收时被优先考虑
    • G1会跟踪老年代所有的incoming引用,这样老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时被处理掉。

    4、JDK9并发标记起始时间的调整

    • 并发标记必须在堆空间沾满前完成,否则退化成Full GC
    • JDK9之前需要使用 -XX:InitiatingHeapOccupancyPercent
    • JDK9可以动态调整
      • -XX:InitiatingHeapOccupancyPercent用来设置初始值
      • 进行数据采样并且动态调整
      • 总会有一个安全的空挡空间

    4.垃圾回收调优

    4.1.调优简介

    1、查看虚拟机运行参数

    java -XX:PrintFlagsFinal -version | findstr "GC"
    
    • 1

    2、回收器选择问题

    【低延迟】还是【高吞吐量】,选择合适的回收器

    • CMS、G1、ZGC
    • ParallelGC

    3、垃圾回收频繁的问题分析?

    查看Full GC前后的内存占用,考虑下面几个问题

    • 数据太多
    • 数据表示太臃肿
    • 是否存在内存泄露
    4.2.新生代调优

    1、新生代的特点

    • 所有的new操作的内存分配非常廉价
      • TLAB thread-local allocation buffer
      • TLAB 是每个线程都会在伊甸园中分配一块私有的区域,当new一个对象的时候,先去TLAB中查看有没有足够的内存空间,如果有就占用这块内存。
    • 死亡对象的回收代价是零
    • 大部分对象用过即死
    • Minor GC的时间远远低于Full GC

    2、调优参数

    • 晋升阈值配置得当,让长时间存活对象尽快晋升

    • -XX:MaxTenuringThreshold=threshold :最大晋升值

    • -XX:+PrintTenuringDistribution :晋升的日志

    4.3.老年代调优
    • 以CMS为例

    • CMS的老年代内存越大越好

    • 先尝试不做调优,如果没有Full GC那么就无需调优,否则现场时调优新生代

    • 观察发生FullGC时老年代的占比,将老年代内存预设调大1/4 ~ 1/3

      • -XX:CMSInitatingOccupancyFraction=percent
      • 参数为0,只要老年代有垃圾就回收
      • 一般设置在百分之75到80,也就是说,预留20的空间来进行回收
    4.4.GC调优案例

    1、Full GC和Minor GC频繁

    如果GC发生频繁,说明空间紧张,如果是新生代的空间紧张,当业务高峰期来了,大量对象被创建,很快就会把新生代的空间塞满,塞满之后还会造成一个问题,幸存区空间紧张,导致对象的晋升阈值减小,导致一些周期很短的对象也会被晋生到老年代,老年代存了大量的生命周期少的对象,发生Full GC。

    增大新生代内存,新生代内存充裕后,就不会频繁发生GC,这样老年代也不会存储大量周期短的对象。

    2、请求高峰期发生Full GC,单次暂停时间特别长(CMS)

    CMS,查看GC日志,CMS初始标记和并发标记都是比较快的,耗时主要发生在重新标记上,重新标记不但会扫描老年代的和新生代的垃圾,可以在重新标记之前进行新生代的内存。使用参数-XX:+CMSScavengeBeforeRemark设置重新标记之前进行新生代的一次GC回收。

    3、老年代充裕的情况下,发生Full GC(CMS jdk1.7)

    在1.8版本之前永久代的空间不足,也会产生Full GC。

    5.字节码技术

    5.1.类文件结构

    1、JVM规范下类文件结构

    在这里插入图片描述

    2、二进制字节码文件

    //Hello World 示例
    public class Demo16 {
        public static void main(String[] args) {
            System.out.println("hello world");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    od -t -xC Demo16.class

    0000000 ca fe ba be 00 00 00 34 00 22 0a 00 06 00 14 09
    0000020 00 15 00 16 08 00 17 0a 00 18 00 19 07 00 1a 07
    0000040 00 1b 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
    0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
    0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
    0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
    0000140 00 04 74 68 69 73 01 00 14 4c 63 6f 6d 2f 6c 69
    0000160 78 69 61 6e 67 2f 44 65 6d 6f 31 36 3b 01 00 04
    0000200 6d 61 69 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c
    0000220 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 01 00 04
    0000240 61 72 67 73 01 00 13 5b 4c 6a 61 76 61 2f 6c 61
    0000260 6e 67 2f 53 74 72 69 6e 67 3b 01 00 0a 53 6f 75
    0000300 72 63 65 46 69 6c 65 01 00 0b 44 65 6d 6f 31 36
    0000320 2e 6a 61 76 61 0c 00 07 00 08 07 00 1c 0c 00 1d
    0000340 00 1e 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
    0000360 07 00 1f 0c 00 20 00 21 01 00 12 63 6f 6d 2f 6c
    0000400 69 78 69 61 6e 67 2f 44 65 6d 6f 31 36 01 00 10
    0000420 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74
    0000440 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 73
    0000460 74 65 6d 01 00 03 6f 75 74 01 00 15 4c 6a 61 76
    0000500 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
    0000520 3b 01 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e
    0000540 74 53 74 72 65 61 6d 01 00 07 70 72 69 6e 74 6c
    0000560 6e 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f
    0000600 53 74 72 69 6e 67 3b 29 56 00 21 00 05 00 06 00
    0000620 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00
    0000640 00 00 2f 00 01 00 01 00 00 00 05 2a b7 00 01 b1
    0000660 00 00 00 02 00 0a 00 00 00 06 00 01 00 00 00 04
    0000700 00 0b 00 00 00 0c 00 01 00 00 00 05 00 0c 00 0d
    0000720 00 00 00 09 00 0e 00 0f 00 01 00 09 00 00 00 37
    0000740 00 02 00 01 00 00 00 09 b2 00 02 12 03 b6 00 04
    0000760 b1 00 00 00 02 00 0a 00 00 00 0a 00 02 00 00 00
    0001000 06 00 08 00 07 00 0b 00 00 00 0c 00 01 00 00 00
    0001020 09 00 10 00 11 00 00 00 01 00 12 00 00 00 02 00
    0001040 13
    0001041
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    5.2.字节码指令

    1、字节码指令入门

    研究两组字节码指令

    (1)public cn.itcast.jvm.t5.HelloWord();构造方法的字节码指令

    2a b7 00 01 b1
    
    • 1
    • 2a:aload_0加载slot 0的局部变量,即this作为下main的invokespecial构造方法调用的参数
    • b7:invokespecial预备调用构造方法
    • 00 01:引用常量池中#1项,即【Method java/lang/Object.“”😦)V】
    • b1:表示返回

    (2)public static void main(java.lang.String[]);之方法的字节码指令

    b2 00 02 12 03 b6 00 04 b1
    
    • 1
    • b2:getstatic用来加载静态变量
    • 00 02:引用常量池中#2项,即【Field java/lang/System.out:Ljava/io/PrintStream;】
    • 12:ldc加载参数
    • 03:引用常量池中#3项,即【String hello world】
    • b6:invokevirtuaf预备调用成员方法。
    • 00 04:引用常量池中#4项,即【Method java/io/PrintStream.println:(Ljava/lang/String;)V】
    • b1:表示返回
    5.3.图解方法执行流程

    1、原始Java代码

    public class Demo17 {
        /**
         * 演示字节码指令和操作数栈、常量池的关系
         * @param args
         */
        public static void main(String[] args) {
            int a = 10;
            int b = Short.MAX_VALUE+1;
            int c = a + b;
            System.out.println(c);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2、编译后的字节码文件

    javap -v Demo17.class
    
    • 1
    Classfile /D:/ideaworkspace/jvm-demo/target/classes/com/lixiang/Demo17.class
      Last modified 2021-12-10; size 604 bytes
      MD5 checksum e36477501751b13feb4bebbef30bfa7b
      Compiled from "Demo17.java"
    public class com.lixiang.Demo17
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#25         // java/lang/Object."":()V
       #2 = Class              #26            // java/lang/Short
       #3 = Integer            32768
       #4 = Fieldref           #27.#28        // java/lang/System.out:Ljava/io/PrintStream;
       #5 = Methodref          #29.#30        // java/io/PrintStream.println:(I)V
       #6 = Class              #31            // com/lixiang/Demo17
       #7 = Class              #32            // java/lang/Object
       #8 = Utf8               <init>
       #9 = Utf8               ()V
      #10 = Utf8               Code
      #11 = Utf8               LineNumberTable
      #12 = Utf8               LocalVariableTable
      #13 = Utf8               this
      #14 = Utf8               Lcom/lixiang/Demo17;
      #15 = Utf8               main
      #16 = Utf8               ([Ljava/lang/String;)V
      #17 = Utf8               args
      #18 = Utf8               [Ljava/lang/String;
      #19 = Utf8               a
      #20 = Utf8               I
      #21 = Utf8               b
      #22 = Utf8               c
      #23 = Utf8               SourceFile
      #24 = Utf8               Demo17.java
      #25 = NameAndType        #8:#9          // "":()V
      #26 = Utf8               java/lang/Short
      #27 = Class              #33            // java/lang/System
      #28 = NameAndType        #34:#35        // out:Ljava/io/PrintStream;
      #29 = Class              #36            // java/io/PrintStream
      #30 = NameAndType        #37:#38        // println:(I)V
      #31 = Utf8               com/lixiang/Demo17
      #32 = Utf8               java/lang/Object
      #33 = Utf8               java/lang/System
      #34 = Utf8               out
      #35 = Utf8               Ljava/io/PrintStream;
      #36 = Utf8               java/io/PrintStream
      #37 = Utf8               println
      #38 = Utf8               (I)V
    {
      public com.lixiang.Demo17();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1(栈的深度), locals=1(局部变量表的长度), args_size=1(参数的长度)
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/lixiang/Demo17;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=4, args_size=1
             0: bipush        10
             2: istore_1
             3: ldc           #3                  // int 32768
             5: istore_2
             6: iload_1
             7: iload_2
             8: iadd
             9: istore_3
            10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
            13: iload_3
            14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
            17: return
          LineNumberTable:
            line 10: 0
            line 11: 3
            line 12: 6
            line 13: 10
            line 14: 17
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      18     0  args   [Ljava/lang/String;
                3      15     1     a   I
                6      12     2     b   I
               10       8     3     c   I
    }
    SourceFile: "Demo17.java"
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94

    3、图解方法执行流程

    (1)常量池载入运行时常量池

    Java虚拟机的类加载器,会把main方法所在的类进行类加载操作,会把类的字节码相关信息放入内存中,同时把常量池中的信息放入运行时常量池中,后续查找都在运行时常量池中。一些比较小的值,不会直接放到常量池中,而是存放在字节码指令中。常量池也属于方法区,只不过这里单独提出来了。

    在这里插入图片描述

    (2)方法字节码载入方法区,main线程开始运行,分配栈帧内存

    (stack=2,locals=4),两个操作数栈,局部变量表中有四个槽位。

    在这里插入图片描述

    (3)执行引擎开始执行字节码

    • bipush:将一个byte压入操作数栈(其长度会补齐4个字节),类似的指令还有
    • sipush:将一个short压入操作数栈(其余长度会补齐4个字节)
    • ldc:将一个int压入操作数栈
    • ldc2_w:将一个long压入操作数栈(分两次压入,因为long是8个字节)
    • 这里小的数字都是和字节码指令存在一起,超过short范围的数字存入了常量池

    bipush 10:将10这个数值,压入栈顶,执行引擎执行bipush 10

    在这里插入图片描述

    istore 1:将操作数栈中的元素弹出,放入局部变量表slot 1中,对应的代码a = 10

    在这里插入图片描述
    在这里插入图片描述

    ldc #3:读取运行时常量池中#3位置,即32768(超过short最大值范围的数会被放到运行时常量池中),将其加载到操作数栈中

    注意 Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的

    在这里插入图片描述

    istore 2:将操作数栈中的元素弹出,放到局部变量表的2号位置

    在这里插入图片描述

    iload1 iload2:将局部变量表中1号位置和2号位置的元素放入操作数栈中,因为只能在操作数栈中进行运算操作

    在这里插入图片描述

    istore 3:将操作数栈中的元素弹出,放入局部变量表的3号位置

    在这里插入图片描述
    在这里插入图片描述

    getstatic #4:在运行时常量池中找到#4,发现是一个对象,在堆内找到该对象,将其引用放入操作数栈中。

    在这里插入图片描述
    在这里插入图片描述

    iload 3:将局部变量表中3号位置的元素压入操作数栈中

    在这里插入图片描述

    invokevirtual 5:找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法,生成新的栈帧(分配 locals、stack等)

    传递参数,执行新栈帧中的字节码。

    在这里插入图片描述

    执行完毕,弹出栈帧

    清除 main 操作数栈内容

    在这里插入图片描述

    return
    完成 main 方法调用,弹出 main 栈帧,程序结束

    5.4.字节码分析x=0

    代码:

    public class Demo2 {
    	public static void main(String[] args) {
    		int i=0;
    		int x=0;
    		while(i<10) {
    			x = x++;
    			i++;
    		}
    		System.out.println(x); //接过为0
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    字节码解析:

    Code:
         stack=2, locals=3, args_size=1	  //操作数栈分为两个空间,局部变量表分为3个空间,参数的长度为1
            0: iconst_0	 				  //准备一个常数0
            1: istore_1	                  //将常数0放在局部变量表第一个槽位上
            2: iconst_0					  //准备一个常数0
            3: istore_2					  //将常数0放在局部变量表第二个槽位上
            4: iload_1		              //将局部变量表中1号槽位的数据放入操作数栈中
            5: bipush        10	          //将10 放入操作数栈中,此时操作数栈中有10 和 0两个数
            7: if_icmpge     21	          //比较操作数栈中的两个数,如果下面的数大于上面的数,就跳转到21步
           10: iload_2					  //将局部变量表中2号槽位的数据放入操作数栈中,放入的数值为0
           11: iinc          2, 1	      //局部变量表中2号槽位的数加1,自增后,槽位中的数值为1
           14: istore_2					  //将操作数组栈中的数放入到局部变量表中的2号槽位,2号槽位有变成了0
           15: iinc          1, 1 		  //1号槽位进行+1,1号槽位的数值为1
           18: goto          4 			  //跳转到第四条指令
           21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
           24: iload_2
           25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
           28: return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    5.5.构造方法

    1、V

    public class Demo3 {
    	static int i = 10;
    
    	static {
    		i = 20;
    	}
    
    	static {
    		i = 30;
    	}
    
    	public static void main(String[] args) {
    		System.out.println(i); //结果为30
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    编译器会按从上至下的顺序,收集所有static静态代码块和静态成员赋值的代码,合并为一个特殊的方法cinit()V:

    stack=1, locals=0, args_size=0
             0: bipush        10				 //将变量10从字节码中压入操作数栈
             2: putstatic     #3                  // Field i:I
             5: bipush        20 				 //将变量20从字节码中压入操作数栈
             7: putstatic     #3                  // Field i:I
            10: bipush        30 			     //将变量30从字节码中压入操作数栈
            12: putstatic     #3                  // Field i:I
            15: return							 //最后返回,以最后一次的取值为准
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2、()V

    public class Demo4 {
    	private String a = "s1";
    
    	{
    		b = 20;
    	}
    
    	private int b = 10;
    
    	{
    		a = "s2";
    	}
    
    	public Demo4(String a, int b) {
    		this.a = a;
    		this.b = b;
    	}
    
    	public static void main(String[] args) {
    		Demo4 d = new Demo4("s3", 30);
    		System.out.println(d.a);
    		System.out.println(d.b);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    编译器会按从上至下的顺序,收集所有{}(实例块)和成员变量复制的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。

    Code:
         stack=2, locals=3, args_size=3
            0: aload_0
            1: invokespecial #1                  // Method java/lang/Object."":()V
            4: aload_0
            5: ldc           #2                  // String s1
            7: putfield      #3                  // Field a:Ljava/lang/String;
           10: aload_0
           11: bipush        20
           13: putfield      #4                  // Field b:I
           16: aload_0
           17: bipush        10
           19: putfield      #4                  // Field b:I
           22: aload_0
           23: ldc           #5                  // String s2
           25: putfield      #3                  // Field a:Ljava/lang/String;
           //原始构造方法在最后执行
           28: aload_0
           29: aload_1
           30: putfield      #3                  // Field a:Ljava/lang/String;
           33: aload_0
           34: iload_2
           35: putfield      #4                  // Field b:I
           38: return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    5.6.方法调用
    public class Demo5 {
    	public Demo5() {
    
    	}
    
    	private void test1() {
    
    	}
    
    	private final void test2() {
    
    	}
    
    	public void test3() {
    
    	}
    
    	public static void test4() {
    
    	}
    
    	public static void main(String[] args) {
    		Demo5 demo5 = new Demo5();
    		demo5.test1();
    		demo5.test2();
    		demo5.test3();
    		Demo5.test4();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 不同方法在调用时,对应的虚拟机指令有所区别。
    • 私有、构造、被final修饰的方法,在调用时都使用invokespecial指令。
    • 普通成员方法在调用的时候,使用invokevirtual指令。因为编译期间无法确定该方法的内容。只有在运行期间才能确定。
    • 静态方法在调用时使用invokestatic指令。
    Code:
          stack=2, locals=2, args_size=1
             0: new           #2                  // class com/nyima/JVM/day5/Demo5 
             3: dup
             4: invokespecial #3                  // Method "":()V
             7: astore_1
             8: aload_1
             9: invokespecial #4                  // Method test1:()V
            12: aload_1
            13: invokespecial #5                  // Method test2:()V
            16: aload_1
            17: invokevirtual #6                  // Method test3:()V
            20: invokestatic  #7                  // Method test4:()V
            23: return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • new是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈。
    • dup是赋值操作数栈栈顶的内容,本例为【对象引用】,本例即为【对象引用】,为什么需要两份引用呢,一个是配合invokespecial调用该对象的构造方法“init:()V‘(会消耗掉栈顶一个引用),另一个要配合astore_1赋值给局部变量。
    • 终方法(final),私有方法(private),构造方法都是由invokespecial指令来调用,属于静态绑定。
    • 普通成员方法是由invokevirtual调用,属于动态绑定,即支持多态,成员方法与静态方法调用的另一个区别是,执行方法前是否需要对象。
    5.7.多态原理

    因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用invokevirtual指令。

    在执行invokevirtual指令时,经历了以下步骤:

    • 先通过栈帧中对象的引用找到对象
    • 分析对象头,找到对象实际的Class
    • Class结构中有vtable
    • 查询vtable找到方法的具体地址
    • 执行方法的字节码
    5.8.异常处理

    1、try-catch

    public class Demo1 {
    	public static void main(String[] args) {
    		int i = 0;
    		try {
    			i = 10;
    		}catch (Exception e) {
    			i = 20;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 对应字节码文件
    Code:
         stack=1, locals=3, args_size=1
            0: iconst_0     //准备一个常数0
            1: istore_1     //将常数0放在局部变量表的1号槽位上
            2: bipush        10		//从常量池中拿取10
            4: istore_1     //将常数10放在局部变量表的1号槽位上
            5: goto          12  //如果不发生异常跳到12步
            8: astore_2     //出现异常调到异常表
            9: bipush        20   //从常量池中拿取20
           11: istore_1      //将常数20放在局部变量表的1号槽位上
           12: return
         //多出来一个异常表
         Exception table:
            from    to  target type
                2     5     8   Class java/lang/Exception
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 可以看出多出来一个Exception table的结构,[from,to)是前闭后开(也就是检测2~4行)的检测范围,一旦这个范围的字节码执行出现异常,则通过type匹配异常类型,如果一致,进入target所指示行号。
    • 8行的字节码指令astore_2是将异常对象引用用局部变量表的2号位置(为e)

    2、多个single-catch

    public class Demo1 {
    	public static void main(String[] args) {
    		int i = 0;
    		try {
    			i = 10;
    		}catch (ArithmeticException e) {
    			i = 20;
    		}catch (Exception e) {
    			i = 30;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 对应字节码文件
    Code:
         stack=1, locals=3, args_size=1
            0: iconst_0    //准备一个常量0
            1: istore_1    //将常量0放入本地变量表的1号槽位上
            2: bipush        10  //从常量池中获取10这个变量
            4: istore_1      //将常量10放入本地变量表中的1号槽位中
            5: goto          19 //如果没有发生异常就跳转的19行
            8: astore_2    		//发生异常匹配异常表中target为8的异常
            9: bipush        20  //从常量池中拿出20
           11: istore_1  		 //放入本地变量表中的1号槽位
           12: goto          19  //跳转到19号
           15: astore_2          //发生异常匹配异常表中target为15的异常
           16: bipush        30  //从常量池中拿出30
           18: istore_1         //放入本地变量表中的1号槽位
           19: return
         Exception table:
            from    to  target type
                2     5     8   Class java/lang/ArithmeticException
                2     5    15   Class java/lang/Exception
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 因为异常出现时,只能进入Exception table中一个分支,所以局部变量表slot 2位置被共用(astore_2)

    3、finally

    public class Demo2 {
    	public static void main(String[] args) {
    		int i = 0;
    		try {
    			i = 10;
    		} catch (Exception e) {
    			i = 20;
    		} finally {
    			i = 30;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 对应字节码文件
    Code:
         stack=1, locals=4, args_size=1
            0: iconst_0   //准备一个常量0
            1: istore_1   //将常量0放入本地变量表的1号槽位上
            //try块 
            2: bipush        10   //常量池中取出10
            4: istore_1		//将常量10放入本地变量表的1号槽位上
            //try块执行完后,会执行finally    
            5: bipush        30   //从常量池中拿出30
            7: istore_1			//30放到一号槽位上
            8: goto          27  //跳转到27跳指令
           //catch块     
           11: astore_2 //异常信息放入局部变量表的2号槽位
           12: bipush        20
           14: istore_1
           //catch块执行完后,会执行finally        
           15: bipush        30
           17: istore_1
           18: goto          27
           //出现异常,但未被Exception捕获,会抛出其他异常,这时也需要执行finally块中的代码   
           21: astore_3
           22: bipush        30
           24: istore_1
           25: aload_3
           26: athrow  //抛出异常
           27: return
         Exception table:
            from    to  target type
                2     5    11   Class java/lang/Exception
                2     5    21   any
               11    15    21   any
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 可以看出finally中的代码被复制了3份,分别放入try流程,catch流程以及catch剩余的异常类型流程。
    • 注意:虽然从字节码指令看来,每个块中都有finally块,但是finally块中的代码只会被执行一次。

    4、finally中的return

    public class Demo3 {
    	public static void main(String[] args) {
    		int i = Demo3.test();
            //结果为20
    		System.out.println(i);
    	}
    
    	public static int test() {
    		int i;
    		try {
    			i = 10;
    			return i;
    		} finally {
    			i = 20;
    			return i;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 对应的字节码文件
    Code:
         stack=1, locals=3, args_size=0
            0: bipush        10   //常量池中拿取10,放到操作数栈中
            2: istore_0          //将10放入局部变量表中的0号槽位上
            3: iload_0			 //将0号槽位的值放入操作数栈中
            4: istore_1          //暂存返回值 
            5: bipush        20  //常量池中拿取20
            7: istore_0			//将20放入局部变量表中的0号槽位上
            8: iload_0			//将0号槽位的值放入操作数栈中
            9: ireturn	//ireturn会返回操作数栈顶的整型值20
           //如果出现异常,还是会执行finally块中的内容,没有抛出异常
           10: astore_2         //出异常的情况,异常会被吞掉,还是拿取20
           11: bipush        20 	
           13: istore_0
           14: iload_0
           15: ireturn	//这里没有athrow了,也就是如果在finally块中如果有返回操作的话,且try块中出现异常,会吞掉异常!
         Exception table:
            from    to  target type
                0     5    10   any
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 由于finally中的ireturn被插入了所有可能的流程,因此返回结果肯定以finally的为止。
    • 至于字节码中第2行,似乎没啥用,而且留了个伏笔,看下这个例子
    • 跟上例中的finally相比,发现没有athrow了,这告诉我们:如果在finally中出现了return,会吞掉异常。
    • 所以不要在finally中进行返回操作。

    5、finally中return值吞掉异常

    public class Demo3 {
       public static void main(String[] args) {
          int i = Demo3.test();
          //最终结果为20
          System.out.println(i);
       }
    
       public static int test() {
          int i;
          try {
             i = 10;
             //这里应该会抛出异常
             i = i/0;
             return i;
          } finally {
             i = 20;
             return i;
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 会发现打印结果为20,并未抛出异常。

    6、finally不带return

    public class Demo4 {
    	public static void main(String[] args) {
    		int i = Demo4.test();
    		System.out.println(i);
    	}
    
    	public static int test() {
    		int i = 10;
    		try {
    			return i;
    		} finally {
    			i = 20;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 对应字节码文件
    Code:
         stack=1, locals=3, args_size=0
            0: bipush        10
            2: istore_0 //赋值给i 10
            3: iload_0	//加载到操作数栈顶
            4: istore_1 //加载到局部变量表的1号位置
            5: bipush        20
            7: istore_0 //赋值给i 20
            8: iload_1 //加载局部变量表1号位置的数10到操作数栈
            9: ireturn //返回操作数栈顶元素 10
           10: astore_2
           11: bipush        20
           13: istore_0
           14: aload_2 //加载异常
           15: athrow //抛出异常
         Exception table:
            from    to  target type
                3     5    10   any
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    5.9.Synchronized
    public class Demo5 {
    	public static void main(String[] args) {
    		int i = 10;
    		Lock lock = new Lock();
    		synchronized (lock) {
    			System.out.println(i);
    		}
    	}
    }
    
    class Lock{}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 对应字节码文件
    Code:
         stack=2, locals=5, args_size=1
            0: bipush        10      //常量池中拿取10,放在操作数栈中	
            2: istore_1				//将10 放到局部变量表中1号槽位
            3: new           #2     //new对象,对应常量池中#2的位置 // class com/nyima/JVM/day06/Lock
            6: dup //复制一份,放到操/作数栈顶,用于构造函数消耗  
            7: invokespecial #3                  // Method com/nyima/JVM/day06/Lock."":()V
           10: astore_2 //剩下的一份放到局部变量表的2号位置
           11: aload_2 //加载到操作数栈
           12: dup //复制一份,放到操作数栈,用于加锁时消耗
           13: astore_3 //将操作数栈顶元素弹出,暂存到局部变量表的三号槽位。这时操作数栈中有一份对象的引用
           14: monitorenter //加锁
           //锁住后代码块中的操作    
           15: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
           18: iload_1
           19: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
           //加载局部变量表中三号槽位对象的引用,用于解锁    
           22: aload_3    
           23: monitorexit //解锁
           24: goto          34
           //异常操作    
           27: astore        4
           29: aload_3
           30: monitorexit //解锁
           31: aload         4
           33: athrow
           34: return
         //可以看出,无论何时出现异常,都会跳转到27行,将异常放入局部变量中,并进行解锁操作,然后加载异常并抛出异常。      
         Exception table:
            from    to  target type
               15    24    27   any
               27    31    27   any
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    6.编译期处理

    所谓的语法糖,其实就是指java编译器把.java源码编译为.class字节码的过程中,自动生成和转换的一些代码,主要是为了减轻程序员的负担,算是java编译器给我们的一个额外福利。

    6.1.默认构造方法
    public class Demo19 {
    
    }
    
    • 1
    • 2
    • 3
    • 经过编译期优化之后
    public class Demo19 {
       //这个无参构造器是java编译器帮我们加上的
       public Demo19() {
          //即调用父类 Object 的无参构造方法,即调用 java/lang/Object." ":()V
          super();
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    6.2.自动拆装箱
    • 基本类型和其包装类型的相关转换过程,称为拆装箱。
    • 在JDK5以后,他们的转换可以在编译期自动完成。
    public class Demo20{
        public static void main(String[] args){
            Integer x = 1;
            int y = x;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 经过编译期优化之后
    public class Demo20{
        public static void main(String[] args){
            //基本类型赋值给包装类型,称为装箱
            Integer x = Integer.valueOf(1);
            //包装类型赋值给基本类型,称谓拆箱
            int y = x.intValue();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    6.3.泛型集合取值
    • 泛型也是在JDK5开始加入的特性,但java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当作了Object类型来处理。
    public class Demo3 {
       public static void main(String[] args) {
          List<Integer> list = new ArrayList<>();
          list.add(10);
          Integer x = list.get(0);
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 对应字节码
    Code:
        stack=2, locals=3, args_size=1
           0: new           #2                  // class java/util/ArrayList
           3: dup
           4: invokespecial #3                  // Method java/util/ArrayList."":()V
           7: astore_1
           8: aload_1
           9: bipush        10
          11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
          //这里进行了泛型擦除,实际调用的是add(Objcet o)
          14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    
          19: pop
          20: aload_1
          21: iconst_0
          //这里也进行了泛型擦除,实际调用的是get(Object o)   
          22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
    //这里进行了类型转换,将Object转换成了Integer
          27: checkcast     #7                  // class java/lang/Integer
          30: astore_2
          31: return
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    所有调用get函数取值时,有一个类型转换的操作

    Integer x = (Integer) list.get(0);
    
    • 1

    如果将返回结果赋值给一个int类型的变量,则还有自动拆箱的操作

    int x = (Integer) list.get(0).intValue();
    
    • 1
    6.4.可变参数
    public class Demo21{
        public static void foo(String... args){
            //将args赋值给arr,可以看出String...实际就是String[]
            String[] arr = args;
            System.out.println(arr.length);
        }
        
        public static void main(){
            foo("hello","world");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 可变参数String… args其实是一个String[] args,从代码中的赋值语句中就可以看出来。同样java编译器会在编译期间将上述代码转换为:
    public class Demo4 {
       public Demo4 {}
        
       public static void foo(String[] args) {
          String[] arr = args;
          System.out.println(arr.length);
       }
    
       public static void main(String[] args) {
          foo(new String[]{"hello", "world"});
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 注意,如果调用的是foo(),即未传递参数时,等价代码为foo(new String[]{}),创建了一个空数组,而不是直接传递的null。
    6.5.foreach 循环

    1、数组使用foreach

    public class Demo5{
        public static void main(String[] args){
            //数组赋初值的简化写法也是一种语法糖
            int[] arr = {1,2,3,4,5};
            for(int x : arr){
                System.out.println(x);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 编译器会转化为:
    public class Demo5 {
        public Demo5 {}
    
    	public static void main(String[] args) {
    		int[] arr = new int[]{1, 2, 3, 4, 5};
    		for(int i=0; i<arr.length; ++i) {
    			int x = arr[i];
    			System.out.println(x);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2、集合使用foreach

    public class Demo5 {
       public static void main(String[] args) {
          List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
          for (Integer x : list) {
             System.out.println(x);
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 集合要使用foreach,需要该集合类实现了Iterable接口,因为集合的遍历需要用到迭代器Iterator
    public class Demo5 {
        public Demo5 {}
        
       public static void main(String[] args) {
          List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
          //获得该集合的迭代器
          Iterator<Integer> iterator = list.iterator();
          while(iterator.hasNext()) {
             Integer x = iterator.next();
             System.out.println(x);
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    6.6.switch 字符串
    public class Demo6 {
       public static void main(String[] args) {
          String str = "hello";
          switch (str) {
             case "hello" :
                System.out.println("h");
                break;
             case "world" :
                System.out.println("w");
                break;
             default:
                break;
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 在编译器中执行的操作
    public class Demo6 {
       public Demo6() {
          
       }
       public static void main(String[] args) {
          String str = "hello";
          int x = -1;
    	 //通过字符串的hashcode+value来判断是否匹配
          switch (str.hashCode()) {
    		//hello的hashcode值
             case 99162322 :
    			//判断完hashcode在次比较字符串,因为hashcode有可能一样的情况
                if(str.equals("hello")) {
                   //相等给x付0
                   x = 0;
                }
                break;
    		 //world的hashcode值
             case 11331880 :
                if(str.equals("world")) {
                   //在比较字符串,相等给x符1
                   x = 1;
                }
                break;
             default:
                break;
          }
    
    	  //另起一个switch,根据x进行判断
          switch (x) {
             case 0:
                //x是0 ,证明匹配hello
                System.out.println("h");
                break;
             case 1:
                //x是1 ,证明匹配world
                System.out.println("w");
                break;
             default:
                break;
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    过程说明:

    • 在编译期间,单个switch被分为两个
      • 第一个用来匹配字符串,并赋给x
        • 字符串的匹配用到了字符串的hashCode,还用到了equals方法。
        • 使用hashCode是为了提高比较的效率,equals是为了防止有hashCode冲突(如BM和C.)。
      • 第二个用来根据x的值来决定输出语句。
    6.7.switch 枚举
    public class Demo7 {
       public static void main(String[] args) {
          SEX sex = SEX.MALE;
          switch (sex) {
             case MALE:
                System.out.println("man");
                break;
             case FEMALE:
                System.out.println("woman");
                break;
             default:
                break;
          }
       }
    }
    
    enum SEX {
       MALE, FEMALE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 编译器中执行的代码如下
    public class Demo7{
        /**
         * 定义一个合成类(仅jvm使用,对我们不可见)
         * 用来映射枚举的ordinal与数组元素的关系
         * 枚举的ordinal表示枚举对象的序号,从0开始
         * 即MALE的ordinal()=0,FEMALE的ordinal()=1
         */
        static class $MAP{
            //定义一个数组,数组大小为枚举类的于元素个数
            static int[] map = new int[2];
            static {
                //ordinal即枚举元素对应所在位置,MALE为0,FEEMALE为1。
                map[SEX.MALE.ordinal()] = 1;
                map[SEX.FEEMALE.ordinal()] = 2;
            }
        }
         public static void main(String[] args) {
          SEX sex = SEX.MALE;
          //将对应位置枚举元素的值赋给x,用于case操作,用元素下标进行比较
          int x = $MAP.map[sex.ordinal()];
          switch (x) {
             case 1:
                System.out.println("man");
                break;
             case 2:
                System.out.println("woman");
                break;
             default:
                break;
          }
       }
    }
        
    enum SEX {
       MALE, FEMALE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    6.8.枚举类
    enum SEX {
       MALE, FEMALE;
    }
    
    • 1
    • 2
    • 3
    • 转换后的代码
    public final class Sex extends Enum<Sex>{
        //对应枚举类中的元素
        public static final Sex MALE;
        public static final Sex FEMALE;
        private static final Sex[] $VALUES;
        
        static {
            //调用构造函数,传入枚举元素的值及ordinal
            MALE = new Sex("MALE",0);
            MALE = new Sex("FEMALE",1);
            $VALUES = new Sex[]{MALE,FEMALE};
        }
        
        //调用父类中的方法
        private Sex(String name,int ordinal){
            super(name,ordinal);
        }
        public static Sex[] values(){
            return $VALUES.clone();
        }
        public static Sex valueOf(String name){
            return Enum.valueOf(Sex.class,name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    6.9.桥接方法

    我们都知道,方法重写时对返回值分两种情况:

    • 父子类的返回值完全一样
    • 子类返回值可以是父类返回值的子类(比较绕口,看下面例子)
    class A{
        public Number m(){
            return 1;
        }
    }
    
    class B extends A{
        @Override
        public Integer m(){
            return 2;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对于子类,java编译器会做如下处理:

    class B extends A{
    
        public Integer m(){
            return 2;
        }
        //此方法是真正重写了父类 public Number m()方法
        public synthetic bridge Number m(){
            //调用public Integer m();
            return m();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    其中桥接方法比较特殊,仅对JVM可见,并且与原来的public Integer m()没有命名冲突,可以用下面反射代码来验证:

    for(Method m : B.class.getDeclaredMethods()){
        System.out.println(m);
    }
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    6.10.匿名内部类
    public class Demo21 {
    
        public static void main(String[] args){
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    System.out.println("running...");
                }
            };
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 转换后的代码
    public class Demo21{
        public static void main(String[] args){
            //用额外创建的类,来创建匿名内部类
            Runnable runnable = new Demo21$1();
        }
        //创建了一个额外的类来实现Runnable接口
        final class Demo21$1 implements Runnable{
            public Demo21$1(){}
            
            @Override
            public void run(){
                System.out.println("running...");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 如果匿名内部类中引用了局部变量
    public class Demo21 {
       public static void main(String[] args) {
          int x = 1;
          Runnable runnable = new Runnable() {
             @Override
             public void run() {
                System.out.println(x);
             }
          };
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 转换后的代码
    public class Demo21 {
       public static void main(String[] args) {
          int x = 1;
          Runnable runnable = new Demo21$2(final x);
       }
    }
    
    final class Demo21$2 implements Runnable{
        //多创建了一个变量
        int val$x;
        //变为了有参构造器
        public Demo21$2(int x){
            this.val$x = x;
        }
        
        @Override
        public void run(){
            System.out.println(val$x);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    **注意:**局部变量必须是final的,因为在创建Demo21$2对象时,将x的值赋值给了Demo21 2 对象的 v a l 2对象的val 2对象的valx属性,所以x不能再发生改变秒如果变化 ,那么val$x属性没有机会跟着一起改变。

    7.类加载阶段

    7.1.加载
    • 将类的字节码载入方法区(1.8以后为元空间,在本地内存中)中,内部采用C++的instanceKlass描述java类,它的重要field有:
      • _java_mirror即java的类镜像,例如对String来说,它的镜像类就是String.class,作用是把klass暴露给java使用
      • _super即父类
      • _fields即成员变量
      • _methods即方法
      • _constants即常量池
      • _class_loader即类加载器
      • _vtable虚方法表
      • _itable接口方法
    • 如果这个类还有父类没有加载,先加载父类
    • 加载和链接可能时交替运行的

    在这里插入图片描述

    • instanceKlass保存在方法区。JDK 8以后,方法区位于元空间中,而元空间又位于本地内存中
    • _java_mirror则是保存在堆内存
    • InstanceKlass和*.class(JAVA镜像类)互相保存了对方的地址
    • 类的对象在对象头中保存了*.class的地址。让对象可以通过其找到方法区中的instanceKlass,从而获取类的各种信息
    7.2.链接

    1、验证

    验证类是否符合JVM规范,安全性检查。

    2、准备

    为static变量分配空间,设置默认值。

    • static变量在JDK7以前是存储与instacneKlass末尾。但在JDK7以后就存储在_java_mirror末尾了,也就是存在于堆中。
    • static变量在分配空间和赋值是在两个阶段完成的。分配空间在准备阶段完成,赋值在初始化阶段完成。
    • 如果static变量是final的基本类型,以及字符串常量,那么编译阶段就确定了,赋值在准备阶段完成
    • 如果static变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成

    3、解析

    解析的含义:将常量池中的符号引用解析为直接地址。

    验证:

    • 运行测试代码:
    /**
     * 验证解析的含义
     */
    public class Demo22 {
    
        public static void main(String[] args) throws ClassNotFoundException, IOException {
            ClassLoader classLoader = Demo22.class.getClassLoader();
            classLoader.loadClass("com.lixiang.C");
            //用于阻塞主线程
            System.in.read();
        }
    
    }
    
    class C{
        D d = new D();
    }
    
    class D{
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 查看当前运行的java进程:jps

    在这里插入图片描述

    • 连接HSDB工具:java -cp JDK的安装目录\lib\sa-jdi.jar sun.jvm.hotspot.HSDB,输入进程id查找
    java -cp D:\JDK\jdk1.8\lib\sa-jdi.jar sun.jvm.hotspot.HSDB
    
    • 1

    在这里插入图片描述

    在这里插入图片描述

    • 改用new的方式进行创建
    /**
     * 验证解析的含义
     */
    public class Demo22 {
    
        public static void main(String[] args) throws ClassNotFoundException, IOException {
            new C();
            //用于阻塞主线程
            System.in.read();
        }
    
    }
    
    class C{
        D d = new D();
    }
    
    class D{
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述
    在这里插入图片描述

    7.3.初始化

    初始化阶段就是执行类构造器clinit()方法的过程,虚拟机会保证这个类的【构造方法】的线程安全。

    clinit()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。

    注意:

    编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如

    public class Test{
        static {
            i = 0; //给变量赋值可以正常编译通过
            System.out.println(); //这句编译器会提示“非法向前引用”
        }
        static int i = 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    发生时机

    • 测试代码
    class A{
       static int a = 0;
       static {
           System.out.println("A init");
       }
    }
    
    class B extends A{
       final static double b = 5.0;
       static boolean c = false;
       static {
           System.out.println("B init");
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 类的初始化是懒惰的,以下情况不会触发类的初始化

    (1)final修饰的静态变量不会触发初始化

    //访问B中final修饰的变量
    System.out.println(B.b);
    
    • 1
    • 2

    在这里插入图片描述

    (2)类对象.class不会触发初始化

    //访问B的class对象
    System.out.println(B.class);
    
    • 1
    • 2

    在这里插入图片描述

    (3)创建类的数组不会触发初始化

    //创建B类型的数组
    System.out.println(new B[10]);
    
    • 1
    • 2

    在这里插入图片描述

    (4)ClassLoader不会初始化类,但是会加载类

    //创建classLoader不会初始化B类,但是会加载B、A类
    ClassLoader c1 = Thread.currentThread().getContextClassLoader();
    c1.loadClass("com.lixiang.B");
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    (5)Class.forName(),initizlize参数设置成false不会初始化操作

    //Class.forName中initizlize参数设置成false不会初始化类B,但会加载B、A
    ClassLoader c2 = Thread.currentThread().getContextClassLoader();
    Class.forName("com.lixiang.B",false,c2);
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    • 会发生类初始化操作的情况

    (1)首次访问这个类的静态变量或静态方法时

    //访问A中不被final修饰的静态变量
    System.out.println(A.a);
    
    • 1
    • 2

    在这里插入图片描述

    (2)子类初始化,如果父类还没初始化,会先引发父类的初始化

    //访问B中的静态变量,会先加载A的初始化
    System.out.println(B.c);
    
    • 1
    • 2

    在这里插入图片描述

    (3)子类访问父类的静态变量,只会触发父类的初始化

    //子类B访问父类A中的静态变量a,只会初始化A
    System.out.println(B.a);
    
    • 1
    • 2

    在这里插入图片描述

    (4)Class.forName(“”)会初始化类B,但会先初始化类A

    Class.forName("com.lixiang.B");
    
    • 1

    在这里插入图片描述

    8.类加载器

    Java虚拟机设计团队有意把类加载阶段中的 ”通过一个类的全限定名来获取该类的二进制字节流” 这个动作放到Java虚拟机外部去实现,以便让应用程序自己去决定如何获取所需类。实现这个动作的代码被称为**“类加载器”**(ClassLoader)。

    8.1.类与类加载器

    类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远超类加载阶段。

    对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:**比较两个是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,**否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那么这两个必定不相等。

    JDK 8为例

    名称加载的类说明
    Bootstrap ClassLoader(启动类加载器)JAVA_HOME/jre/lib无法直接访问
    Extension ClassLoader(扩展类加载器)JAVA_HOME/jre/lib/ext上级为Bootstrap,显示为null
    Application ClassLoader(应用程序类加载器)classpath上级为Extension
    自定义类加载器自定义上级为Appliaction
    8.2.启动类加载器

    可以通过在控制台输入指令,使得类被启动类加载器加载。

    /**
     * 启用Bootstrap类加载器加载类
     */
    public class Demo25 {
        public static void main(String[] args) throws ClassNotFoundException {
            Class<?> aClass = Class.forName("com.lixiang.F");
            System.out.println(aClass.getClassLoader()); //获取当前的类加载器
        }
    }
    
    class F{
        static {
            System.out.println("bootstrap F init");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    8.3.拓展类加载器

    如果classpath和JAVA_HOME/jre/lib/ext下有同名类,加载时会使用拓展类加载器加载。当应用程序类加载器发现拓展类加载器已经将该同名类加载过了,则不会再次加载。

    8.4.双亲委派模式

    双亲委派模式,即调用类加载器Classloader的loadClass方法时,查找类的规则

    loadClass源码:

    	private final ClassLoader parent;
    
        public ClassLoader(ClassLoader parent) {
            this.parent = parent;
        }
    
        protected  Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException{
            synchronized (getClassLoadingLock(name)){
                //首先要先看下该类是否已经被该类加载器加载过了
                Class<?> c = findLoadClass(name);
                //如果没有被加载过
                if (c == null){
                    long t0 = System.nanoTime();
                    try{
                        //看是否被它的上级加载器加载过了Exception的上级是Bootstrap,但是它显示为null
                        if(parent != null){
                            c = parent.loadClass(name,resolve);
                        }else {
                            //看是否被启动类加载器加载过
                            c = finBootstrapClassOrNull(name);
                        }
                    }catch (ClassNotFoundException e){
                        //ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                        //捕获异常但是不做任何处理
                    }
                    if(c == null){
                        //如果还没有找到,先让拓展类加载器调用findClass方法找到该类,如果还没有会找到,
                        //就抛出异常,然后让应用类加载器去找classpath下找该类
                        long t1 = System.nanoTime();
                        c = findClass(name);
    
                        //记录时间
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if(resolve){
                    resolveClass(c);
                }
                return c;
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    8.5.自定义类加载器

    使用场景

    • 想加载非classpath随意路径中的类文件
    • 通过接口来使用实现,希望解耦时,常用在框架设计
    • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器

    步骤

    • 继承ClassLoader父类
    • 要遵从双亲委派机制,重写findClass方法
      • 不是重写loadClass方法,否则不会走双亲委派机制
    • 读取类文件的字节码
    • 调用父类的defineClass方法来加载类
    • 使用者调用该类加载器的loadClass方法

    自定义类加载器

    public class MyClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            String path = "D:\\MyClassLoader\\"+name+".class";
            try {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                Files.copy(Paths.get(path),os);
    
                //得到字节数组
                byte[] bytes = os.toByteArray();
                //byte[] -> *.class
                return defineClass(name,bytes,0,bytes.length);
            }catch (IOException e){
                e.printStackTrace();
                throw new ClassNotFoundException("类文件未找到",e);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    破坏双亲委派模式

    • 双亲委派模型的第一次 “被破坏” 其实发生在双亲委派模型出现之前。
      • 建议用户重写findClass()方法,在类加载器中的loadClass()方法中也会调用该方法
    • 双亲委派模型的第二次 “被破坏” 是由这个模型自身的缺陷导致的
      • 如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模式
    • 双亲委派模型的第三次 “被破坏” 是由于用户堆程序动态性的最=追求而导致的
      • 这里所说的 “动态性“ 指的是一些非常”热“门的名词: 代码热替换(Hot Swap)、模块热部署(Hot Deployment)等

    9.运行期优化

    9.1.分层编译

    1、案例

    /**
     * -XX:+PrintCompilation  -XX:DoEscapeAnalysis
     */
    public class JIT1 {
        public static void main(String[] args) {
            for (int i = 0; i < 200; i++) {
                long start = System.nanoTime();
                for (int j = 0; j < 1000; j++) {
                    new Object();
                }
                long end = System.nanoTime();
                System.out.printf("%d\t%d\n",i,(end - start));
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    2、JVM将执行状态分成了5个层次

    • 0层:解释执行,用解释器将字节码翻译为机器码

    • 1层:使用C1即时编译器编译执行(不带profiling)

    • 2层:使用C1即时编译器编译执行(带基本的profiling)

    • 3层:使用C1即时编译器编译执行(带完全的profiling)

    • 4层:使用C2即时编译器编译执行

    profiling是指在运行中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的回边次数】等。

    3、即时编译器(JIT)与解释器的区别

    • 解释器

      • 将字节解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释。
      • 是将字节码解释为针对所有平台都通用的机器码。
    • 即时编译器

      • 将一些字节码编译为机器码,并存入Code Cache,下次遇到相同的代码,直接执行,无需在编译。
      • 根据平台类型,生成平台特定的机器码。

    ​ 对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行,另一方面,对于仅占据小部分的热点代码,我们则可以编译成机器码,以达到理想的运行速度。执行效率上简单比较一下Interpreter < C1 < C2 ,总的目标是发现热点代码(hotspot名称的由来),并优化这些热点代码。

    4、逃逸分析

    ​ 逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot虚拟机可以分析新创建对象的使用范围,并决定是否在Java堆上分配内存的一项技术。

    逃逸分析的JVM参数如下:

    • 开启逃逸分析:-XX:+DoEscapeAnalysis

    • 关闭逃逸分析:-XX:-DoEscapeAnalysis

    • 显示分析结果:-XX:+PrintEscapeAnalysis

    ​ 逃逸分析技术在Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。

    5、对象逃逸状态

    全局逃逸(GlobalEscape)

    • 即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
      • 对象是一个静态变量
      • 对象是一个已经发生逃逸的对象
      • 对象作为当前方法的返回值

    参数逃逸(ArgEscape)

    • 即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

    没有逃逸

    • 即方法中的对象没有发生逃逸

    6、逃逸分析优化

    针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化

    锁消除

    我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁。

    例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作。

    锁消除的JVM参数如下:

    • 开启锁消除:-XX:+EliminateLocks
    • 关闭锁消除:-XX:-EliminateLocks

    锁消除在JDK8中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。

    标量替换

    首先要明白标量和聚合量,基础类型和对象引用可以理解为标量,他们不能被进一步的分解。而能被进一步分解的量就是聚合量,比如:对象。

    对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替

    这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。

    标量替换的JVM参数如下:

    • 开启标量替换:-XX:+EliminateAllocations
    • 关闭标量替换:-XX:-EliminateAllocations
    • 显示标量替换详情:-XX:+PrintEliminateAllocations

    栈上分配

    当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了GC压力,提高了应用程序性能。

    9.2.方法内联

    1、内联函数

    内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换。

    2、JVM内联函数

    C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的,如

    public final void doSomething() {  
            // to do something  
    }
    
    • 1
    • 2
    • 3

    总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数。

    第二个原因则更重要:方法内联

    如果JVM检测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如:

    	private int add4(int x1, int x2, int x3, int x4) { 
    		//这里调用了add2方法
            return add2(x1, x2) + add2(x3, x4);  
        }  
    
        private int add2(int x1, int x2) {  
            return x1 + x2;  
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    方法调用被替换后

    	private int add4(int x1, int x2, int x3, int x4) { 
    		//这里调用了add2方法
            return x1 + x2 + x3 + x4;  
        }
    
    • 1
    • 2
    • 3
    • 4
    9.3.反射优化
    //foo.invoke()前面0-15次调用使用的是MethodAccessor的NativeMethodAccessorImpl实现
    public class Reflect {
        public static void foo(){
            System.out.println("foo...");
        }
    
        public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            Method method = Reflect.class.getMethod("foo");
            for (int i = 0; i < 16; i++) {
                method.invoke(null);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 一开始if条件不满足,就会调用本地方法invoke0
    • 随着numInvocation的增大,当它大于ReflectionFactory.inflationThreshold的值16时,就会本地方法访问器替换为一个运行时动态生成的访问器,来提高效率
      • 这时会从反射调用变为正常调用,即直接调用 Reflect1.foo()

    在这里插入图片描述

    10.虚拟机工具

    10.1.jps命令

    1、JPS是什么?

    ​ jps (JVM Process Status Tool)是其中的典型jvm工具。除了名字像 UNIX 的 ps 命令之外,它的功能也和 ps 命令类似:可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class, main()函数所在的类)名称以及这些进程的本地虚拟机唯- ID (Local Virtual Machine Identifier, LVMID),虽然功能比较单一,但它是使用频率最高的 JDK 命令行工具。

    2、实战使用

    • jps -l 输出主类的全名,如果进程执行的是Jar包则输出Jar路径
    jps -l
    
    • 1

    在这里插入图片描述
    在这里插入图片描述

    10.2.jstat命令

    1、jstat是什么

    ​ jstat(JVM Statistics Monitor Tool)是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或远程-虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据,在没有GU图形界面,只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。

    2、jstat命令使用

    jstat -gc 2764 250 20 //2764表示进程id,250表示250毫秒打印一次,20表示一共打印20次
    
    • 1

    在这里插入图片描述

    S0C:第一个幸存区的大小
    S1C:第二个幸存区的大小
    S0U:第一个幸存区的使用大小
    S1U:第二个幸存区的使用大小
    EC:伊甸园区的大小
    EU:伊甸园区的使用大小
    OC:老年代大小
    OU:老年代使用大小
    MC:方法区大小
    MU:方法区使用大小
    CCSC:压缩类空间大小
    CCSU:压缩类空间使用大小
    YGC:年轻代垃圾回收次数
    YGCT:年轻代垃圾回收消耗时间
    FGC:老年代垃圾回收次数
    FGCT:老年代垃圾回收消耗时间
    GCT:垃圾回收消耗总时间
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    10.3.jinfo命令

    1、jinfo是什么

    ​ jinfo(Configuration Info for Java)的作用是实时地查看和调整虚拟机各项参数。使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表,但如果想知道未被显示指定的参数的系统默认值,除了去找资料外,就只能使用info的-flag选项进行查询了。

    2、jinfo命令使用

    jinfo 1444(进程id)
    jinfo -flag CMSInititingOccuancyFraction 1444(进程id)
    
    • 1
    • 2
    10.4.jmap命令

    1、jmap是什么

    ​ Jmap (Memory Map for Java)命令用于生成堆转储快照。如果不使用 jmap 命令,要想获取 Java 堆转储快照,还有一些比较“暴力”的手段:-XX: +HeapDumpOnOutOfMemoryError 参数,可以让虚拟机在 OOM 异常出现之后自动生成 dump 文件,用于系统复盘环节

    ​ 和 info 命令一样,jmap 有不少功能在 Windows 平台下都是受限的,除了生成 dump 文件的- dump 选项和用于查看每个类的实例、空间占用统计的-histo选项在所有操作系统都提供之外,其余选项都只能在Linux/Solaris 下使用。

    2、jmap常用命令

    • -dump

      • 生成 Java 堆转储快照。格式为:-dump: format=b, file=
    windows: jmap -dump:format=b,file=d:\a.bin 1234
    mac:      jmap -dump:format=b,file=/Users/daniel/deskTop
    
    • 1
    • 2

    -histo more分页去查看

    • 显示堆中对象统计信息,包括类、实例数量、合计容量

    B :byte

    C : char

    I :Int

    10.5.jhat命令

    1、jhat是什么

    • Sun JDK 提供 jhat (JVM Heap Analysis Tool)命令常与 jmap 搭配使用,来分析 jmap 生成的堆 转储快照。jhat内置了一个微型的HTTP/HTML服务器,生成dump文件的分析结果后,可以在浏览器中查看

    • 特点:

      • jhat分析工作是一个耗时而且消耗硬件资源的过程
      • jhat 的分析功能相对来说比较简陋

    2、jhat使用

    jhat bin文件
    
    • 1

    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    10.6.jstack命令

    1、jstack是什么

    ​ jstack(Stack Trace for Java)命令用于生成虚拟机当前时刻的线程快照(一般称为threaddump或者javacore文件)

    ​ 线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待都是导致线程长时间停顿的常见原因。线程出现停顿的时候通过jstack来查看哥哥线程的调用堆栈,就可以知道没有响应线程到底在后台做些什么事情,或者等待者什么资源。

    2、Jstack怎么做

    • 常用命令jstack -l 3500(进程id)

    • jstack -F 当正常输出的请求不被响应时,强制输出线程堆栈 Force

    • 线上程序一般不能kill进程pid的方式直接关闭

      • shutdownHook :在关闭之前执行的任务
  • 相关阅读:
    RabbitMQ学习记录
    《软件工程与计算》考试复习笔记
    LeetCode高频题:Android系统中WakeLock防止手机进入睡眠模式,统计出每个应用对WakeLock的不同贡献值
    ARMv7-A 那些事 - 4.处理器模式与特权等级
    散列表:为什么散列表和链表经常会一起使用?
    网络重要知识点
    基于GIS开发的智慧水务管理系统有哪些意义?
    微服务架构中各个组件都需要使用哪些技术?
    大灰狼远控木马分析
    备份和恢复 SecureCRT® 和 SecureFX® 设置(版本 7.3 和更高版本)
  • 原文地址:https://blog.csdn.net/weixin_47533244/article/details/127526119