java虚拟机

一、前言

1、jvm的定义:

java virtual machine ,是一台执行Java字节码的虚拟计算机(不同于C,C++等语言,java将源文件编译为class字节码文件,这种文件无法在操作系统上直接运行,这就需要jvm来执行这些文件)。
需要注意的是,jvm是一套规范,由不同的厂商来实现这套规范。

2、作用:

  • 实现了一次编译到处运行(java将程序编译为class字节码而不是机器码,通过在不同的操作系统上安装不同的虚拟机实现了一次编译到处运行)。
  • 自动内存管理,垃圾回收(C,C++需要自己是否内存)
  • 数组下标越界检查(避免修改了不能修改存储空间的内容)
  • 多态:通过虚方法表实现

3、 jvm,jre与jdk

在这里插入图片描述

4、class文件的类容

将一个源文件编译为class文件会产生如下三部分内容(可以通过javap -v *.class反编译查看编译文件的内容):

  • 类的基本信息:包括类名,最新修改时间和从哪个文件编译来的等信息
  • 常量池:不仅仅包含在类和方法中定义的常量,方法名,变量名,变量类型等都以常量的形式存储在常量池中。常量池本质上是一张表,虚拟机根据这张表找到函数执行需要的类名,方法名等信息。
  • 类的方法定义(包含了jvm指令)

二、jvm的内存结构

在这里插入图片描述

1、 程序计数器(Program Counter Register):

记下一条(有的说是正在执行的)jvm字节码指令的地址(如果正在执行的是本地方法则为空)

  • 程序计数器作用:
    jvm通过程序计数器找到需要执行的jvm指令,将jvm指令解释为机器码,再交由CPU执行,与此同时将下一条jvm指令放入程序计数器。
  • 硬件怎么实现的:
    通过寄存器实现
  • 为什么需要程序计数器:
    每个线程都有属于自己的一个程序计数器,在进行线程切换时需要记录当前执行到哪条指令了,程序计数器就存储了这个地址。
  • 程序计数器的特点:
    <1>线程私有
    <2>程序计数器没有内容溢出(jvm规范规定的,其实也好理解,程序计算器只会记录一个地址,当然不会溢出)

2、虚拟机栈:

虚拟机栈中存储的单位是栈帧,每个栈帧对应着一个要执行的方法(java类方法),栈也维护方法执行的顺序。

  • 栈帧:
    每个方法执行时需要的内存,存储了局部变量表、操作数栈、常量池引用等信息。当一个方法执行完毕后才会将当前栈帧出栈。
  • 虚拟机栈的特性:
    <1>虚拟机栈是每个线程私有的。
    <2>栈不会涉及到垃圾回收,当一个方法执行完后,对应的栈帧就会被出栈。
    <3>栈越大可运行的线程越少:物理内存是有限的,那栈越大可以运行的线程就越少。可以通过-Xss指定栈帧大小。
    <4>方法的局部变量是否是安全的:如果方法的局部变量只在这个方法内使用那它就是线程安全的。
    <5>栈溢出:栈中栈帧数量过多(常见的是递归调用忘记设置终止条件)或者栈帧过大会导致栈溢出。

3、本地方法栈

类似于虚拟机栈,它服务于本地方法(本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序)。java中用native修饰的方法(没有方法体)调用的就是本地方法。

  • 本地方法栈也是线程私有的
  • 本地方法栈也不涉及到垃圾回收

4、 堆:

使用new关键字创建的对象都存放在堆里

  • 特点:
    <1>堆中的对象时线程共享的,需要考虑线程安全问题
    <2>有垃圾回收机制
  • 设置堆大小,Xmx

5、方法区:

用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 特点:
    <1>与方法区类似,也是线程共享的。
    <2>在虚拟机创建时就被创建
    <3>逻辑上是堆的一部分(实现时有可能不是)
  • jdk1.6和1.8方法区的结构:
    jdk1.6中方法区放在jvm的内存中(可以使用-XX:MaxPermSize设置大小),在jdk1.7中StringTable从方法区中移到了堆中,在jdk.18中方法区移到了本地内存中但常量数组放在了堆中(可以使用-XX:MaxMetaspaceSize设置大小)。在spring,mybatis中可能会动态的产生大量字节码文件,这些文件加载带方法区中可能会导致方法区内存溢出。将方法区移到本地内存中,方法区中的垃圾会及时的被操作系统处理,并且本地内存较大发生溢出的概率降低。
    在这里插入图片描述
  • 运行时常量池:
    当一个类被jvm加载时,类中的常量池信息会被放入运行时常量池,常量池中的符号地址(常量表中的键)地址会被转换为真实地址。
  • StringTable:
    本质上是一张hash表,长度是固定的,用来存放String对象。
    <1>工作过程:将一个class文件中的常量池加载到运行时常量池时,字符串常量(此时只是一个符号并不是String对象,字符串常量在常量池中是唯一的)并不会马上转换为String对象并存入StringTable中,只有当运行到字符串常量所在位置的代码时才会把常量池中的字符串常量转为字符串对象并存入StringTable。如果在运行过程中一个字符串常量已被转为String对象并存入StringTable则直接取出这个String对象。
    <2>字符串拼接:在编译时,在+两边的如果都是字符串常量,会直接将两个字符串进行拼接并且放入常量池。只要+两边有一个不是常量,在运行时会调用StringBuilder的append方法将第二个字符串拼接在第一个字符串后面,然后调用toString方法返回一个字符串对象。为什么?其实很好理解,如果是字符串常量,那两边的值一定是不变的,编译时就可以确定结果,如果不是常量,编译时无法确定两边的值自然也就无法拼接。
    <3>哪些字符串对象会放入StringTable:
    如果常量池中有对应的字符串常量,当使用到该字符串常量时,会将字符串常量转为String对象放入StringTable中;
    使用String对象的intern()方法将创建的String对象放入常量池中。假设使用new关键字创建了String对象s1,在jdk1.8中,如果字符串对应的String对象没有被存入StringTable中,将s1放入StringTable中;如果字符串对应的String对象已经被存入StringTable中,则s1不会被放入常量池中,并且intern()返回StringTable()中的对象。在jdk1.6中,如果字符串对应的String对象没有被存入StringTable中,将s1拷贝一份放入StringTable中,返回StringTable中的对象(s1没有被放入StringTable);如果字符串对应的String对象已经被存入StringTable中,则与jdk1.8相同。
    <3>面试题:
    jdk1.8
//题
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b";
        String s4 = s1 + s2;
        String s5 = "ab";
        String s6 = s4.intern();
        final String s7 = "a";
        final String s8 = "b";
        String s9 = s7 + s8;
        // 问
        System.out.println(s3 == s4);   //false
        System.out.println(s3 == s5);   //true
        System.out.println(s3 == s6);   //true
        System.out.println(s9 == s5);   //true;
        //题
        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();
        //问
        System.out.println(x1 == x2);//false
        //题
        String y2 = new String("e") + new String("f");
        y2.intern();
        String y1 = "ef";
        //问
        System.out.println(y1 == y2);//true
        //题
        String z1 = "gh";
        String z2 = new String("g") + new String("h");
        z2.intern();
        //问
        System.out.println(z1 == z2);//false

jdk1.6

        //题
        String x2 = new String("c") + new String("d");
        String x1 = "cd";
        x2.intern();
        //问
        System.out.println(x1 == x2);//false
        //题
        String y2 = new String("e") + new String("f");
        y2.intern();
        String y1 = "ef";
        //问
        System.out.println(y1 == y2);//false
        //题
        String z1 = "gh";
        String z2 = new String("g") + new String("h");
        z2.intern();
        //问
        System.out.println(z1 == z2);//false

<4>StringTable的位置,在jdk1.6中StringTable存放在永久代的常量池中,在jdk1.7,StringTable移到了堆中。为什么这么做?
永久代中的垃圾在触发了full GC时才会被回收,回收效率低,放入堆中,在触发minor GC后就会进行垃圾回收。
<5>StringTable的调优,打印StringTable的信息-Xmx10m -XX:+PrintStringTableStatistics 输出垃圾回收的信息-XX:+PrintGCDetails -verbose:gc 设置StringTable的大小-XX:StringTableSize=1009
StringTable越大,查找的效率越高。
直接内存:常见于 NIO(java non-blocking IO) 操作时,用于数据缓冲区。分配回收成本较高,但读写性能高。不受 JVM 内存回收管理。

  • 什么是直接内存:
    <1>为什么要直接内存
    在文件读写中,我们需要申请一块缓冲区(文件太大,直接读入内存放不下),然后将文件内容读入缓冲区中,在对这个缓冲区进行操作。例如,在使用byte(例如:byte[] buf = new byte[10241024])数组作为缓冲区(位于jvm管理的内存中)时,读文件时操作系统会从用户态(执行java程序)切换到内核态(执行本地方法也就是操作系统提供的文件读写方法),然后操作系统开辟一块缓冲区(缓冲区位于操作系统管理的内存中)将文件读一部分放入缓冲区,然后操作系统切换到用户态(执行java程序),java程序将操作系统管理的缓冲区中的内容读入到byte数组中。在从系统内存缓冲区向java缓冲区中拷贝的过程需要花费时间,这个过程其实是没有必要的。
    在这里插入图片描述
    <2>直接内存
    使用诸如ByteBuffer(例如:ByteBuffer buf = ByteBuffer.allocateDirect(1024
    1024);)等类作为缓冲区时,申请的内存不再放在java内存中,而是放在操作系统的内存中,在操作系统读文件时也将读入的内存放入对应的区域中,这样避免了不必要的缓冲区拷贝。直接内存需要使用Unsafe对象的allocateMemory(size)进行申请,直接内存不能被jvm的垃圾回收机制回收而是使用unsafe对象的freeMemory(address)方法进行释放。
    ByteBuffer的allocateDirect方法创建了DirectByteBuffer对象,而DirectByteBuffer的构造方法你使用了Unsafe对象去申请直接内存。
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
{
    public static ByteBuffer allocateDirect(int capacity) {
        return new DirectByteBuffer(capacity);
    }
}

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{
    // Cached unsafe-access object
    protected static final Unsafe unsafe = Bits.unsafe();
    DirectByteBuffer(int cap) {                   // package-private

        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
}

在这里插入图片描述
<3>直接内存导致内存溢出:如果将直接内存的内容放入java的内存区域,当放入的东西过多也会导致java内存溢出(不是直接内存区域溢出)。
<4>直接内存的回收
直接内存无法被jvm的垃圾回收机制回收,DirectByteBuffer 使用了一个若引用对象Cleaner来关联它,当DirectByteBuffer 被垃圾回收时,Cleaner对象的clean方法会被调用,而clean会调用Deallocator的run方法,这个run方法里调用了unsafe对象的freeMemory(address)

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
{ 
	public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }
 }
public class Cleaner extends PhantomReference<Object> {
	public void clean() {
        if (remove(this)) {
            try {
           		//thunk就是Deallocator对象
                this.thunk.run();
            } catch (final Throwable var2) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        if (System.err != null) {
                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
                        }

                        System.exit(1);
                        return null;
                    }
                });
            }

        }
    }
}

三、垃圾回收

垃圾回收主要针对方法区和堆,java虚拟机栈,本地方法栈和程序计数器是线程私有的不会被垃圾回收。

1、判断对象是否可被回收

  • 引用计数法:
    为对象添加一个引用计数器,被一个新的对象引用则引用计数器加1,如果一个引用失效则引用计数器减1,计数器为0则该对象可以被回收。
    循环计数器存在着循环引用问题,A引用了B,B引用了A,但A,B都不在被使用,这时A,B本应该被回收但却不会吧回收。因此,java没有采用这种方法。
public class Test {
    public Object instance = null;
    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}
  • 可达性分析:
    以GC Root对象为起点开始搜索,如果能沿着引用链找到该对象那该对象不可被回收,否则可以被回收。
    <1>哪些对象可以作为GC Root
    虚拟机栈中局部变量表中引用的对象
    本地方法栈中 JNI 中引用的对象
    方法区中类静态属性引用的对象
    方法区中的常量引用的对象
  • 引用类型:
    传统的引用是指非基本类型(类类型)的变量,String a,a就是一个引用,但是java的已经对引用的定义进行了扩展,不能再以传统的思想来理解java的引用。
    <1>软/弱/虚 引用类,软/弱/虚 引用对象
    java中有个引用类(Reference),这个类有三个子类:软引用类(SoftReference),弱引用类(WeakReference)和虚引用类(PhantomReference)。如果一个类继承了这个三个子类中的一个,那这个类也就是对应引用类型的类。
    有了3种引用类型的类,可以得到3种引用对象,分布是弱引用对象,软引用对象,虚引用对象。
    <2>四种引用
    首先我们需要明确,如果一个对象没有被引用,那这个对象在垃圾回收时一定会被回收。
    强引用:强引用指的就是经典定义中的引用(比如String a = “hello world”,a就是一个引用本质上是一个类类型的变量),被强引用引用的对象不会被回收。
    软引用:软引用(本质上是一个对象,用来修饰另一个对象,注意只能修饰一个)来描述一些有用但非必要的对象,被软引用关联着的对象,当内存不足并且进行垃圾回收时,会将这个对象进行回收。如果这个软引用关联一个引用队列,那这个软引用(实际上是软引用的引用)在它关联的对象被回收时会被放入引用队列中。需要注意的是,Entry ref = new Entry(new byte[1024*1024]); 中的byte数组对象不会被回收,因为它还被一个强引用所关联着(value)。可以通过软引用对象的get方法返回这个对象。
 class Entry extends SoftReference{
 		private byte[] value;
        public Entry(byte[] ref){
            super(ref);
            value = ref;
        }
    }

弱引用:弱引用(本质上是一个对象,用来修饰另一个对象,注意只能修饰一个)来描述一些有非必须对象,被弱引用关联着的对象,当进行垃圾回收时不管内存是否充足,会将这个对象进行回收。如果这个弱引用关联一个引用队列,那这个弱引用(实际上是弱引用的引用)在它关联的对象被回收时会被放入引用队列中。可以通过弱引用对象的get方法返回这个对象。
虚引用:虚引用(本质上是一个对象,用来修饰另一个对象,注意只能修饰一个)被虚引用关联的对象随时都可能被回收,并且通过虚引用对象不可获取这个对象,因为虚引用对象的get方法返回的是null。如果这个虚引用关联一个引用队列,那这个虚引用(实际上是弱引用的引用)在它关联的对象被回收时会被放入引用队列中。(虚引用对象必须关联一个引用队列,这个引用队列由一个 ReferenceHandler线程处理)。一个对象被虚引用引用,等于没有被引用,只是为了告诉java程序进行一些后续处理。
需要注意的是只有软/弱/虚的父类的构造方法才能赋予被关联的对象这些性质,什么意思呢?Entry ref = new Entry(new byte[1024*1024]);并不能使得byte数组对象在内存不够的时候被回收掉。

 class Entry extends SoftReference{
 		private byte[] value;
        public Entry(byte[] ref){
            value = ref;
        }
    }

还需要注意的是,一个对象可能被多种类型的引用同时关联,那这些引用是按照强/软/弱/虚起作用的。
<2>软,弱虚引用对象为什么需要引用队列呢?
软/弱/虚引用关联的对象的回收时不受我们控制的,自己回收可能会照成程序出错,将这个几种类型的引用放入引用队列中可以让用户(虚引用对象由 ReferenceHandler线程处理)去进行后续处理。比如,WeakHashMap中的得Entry继承了WeakReference类(这其实不影响Entry的回收),其中的key是被虚引用(这个虚引用就是Entry对象自己,还需要注意的是它Entry内部没有提供key属性,否则key就被强引用了)所关联。所以WeakHashMap提供了一个引用队列,当key被回收时Entry就被放入队列中,当执行resize(),put(),get()方法时,WeakHashMap会从引用队列中取出对应的引用并删除对应的节点。

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {
        V value;
        final int hash;
        Entry<K,V> next;
        Entry(Object key, V value,
              ReferenceQueue<Object> queue,
              int hash, Entry<K,V> next) {
            super(key, queue);
            this.value = value;
            this.hash  = hash;
            this.next  = next;
        }
}

<3>如何关联引用队列:在创建软/弱/虚对象时传入一个引用队列就行了。
<4>虚引用有什么用
一个对象是否被虚引用关联(其实也就是引用)并不会影响整个对象的生命周期,但一旦这个对象被回收,关联它的虚引用就会被放入队列,然后进行后续处理。
下图说明了DirectByteBuffer的工作原理:一旦buf不再引用DirectByteBuffer,DirectByteBuffer在某个时间段被回收,但它申请的直接内存却不会被回收,cleaner被放入引用队列,某个时间段一个优先级比较低的线程调用Cleanear对象的clean方法,clean方法启用Deallocator的run方法,该方法实现了对垃圾的回收,然后Cleaner对象被回收,接着Deallocator对象被回收。
在这里插入图片描述

<5>终结器引用(不太明白确切的定义)
所有的java对象都会集成Object父类,儿object父类里都有finallize()方法,当这个对象重写了finallize方法,并且没有强引用后,进行垃圾回收时,由虚拟机创建一个终结器引用,当这个对象被垃圾回收时,会把终结器引用加入到引用队列,值得注意的是,这个时候这个对象并没有立刻被垃圾回收,而是先把他的终结器引用放入引用队列,再由一个优先级很低的线程 (finallize Hanlder),在某些时期 查看是否有终结器引用,如果有,则通过终结器引用找到对象,通过调用finallize方法回收掉。
<6>软、弱、虚引用存在的意义:在C++语言中,可以人工进行垃圾回收,而在java中没有对垃圾进行回收的机制,通过使用软、弱、虚引用可以一定程度上控制垃圾回收。

2、垃圾回收算法

  • Mark Sweep(标记清除算法):
    在标记阶段,程序会检查每个对象是否为活动对象,如果是活动对象,则程序会在对象头部打上标记。
    在清除阶段,会进行对象回收并取消标志位,另外,还会判断回收后的分块与前一个空闲分块是否连续,若连续,会合并这两个分块。回收对象就是把对象作为分块,连接到被称为 “空闲链表” 的单向链表,之后进行分配时只需要遍历这个空闲链表,就可以找到分块。
    缺点:标记和清除标记的效率低,会产生大内存碎片。
    在这里插入图片描述
  • Mark Compact(标记整理):
    让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    缺点:需要移动对象,效率低。
    在这里插入图片描述
  • 复制算法:
    将内存分为两个大小相等的块(From和To),每次只用一个区,当内存用完时,将From中的还存活的对象复制到To中,然后交换From和To(标记的交换)。
    缺点:只使用了一半的内存。
    在这里插入图片描述
  • 分代垃圾回收:
    商业的虚拟机一般都采用分代回收算法,并不是jvm的独创。
    分代垃圾回收将堆的空间分为新生代和老年代两部分。新生带分为伊甸园,幸存区From,幸存区To。伊甸园,幸存区From,幸存To三者的空间大小为8:1:1,并且新生带采用的是复制算法。因为新生代的对象大部分都会死去只有一部分的对象会存活,使用复制算法的效率高,8:1是统计出来的幸存的比例。老年代中对象的生存周期一般比较长,并且没有额外空间对它担保,因此使用标记+整理算法。
    (1)工作的基本过程
    <1>新生代的GC
    新创建的对象会放入伊甸园,如果伊甸园满了则进行一次minor GC(同时对伊甸园和幸存区进行),存活的对象会被放入To中并且寿命加1,然后交换From与To的标记。如果幸存区放不下幸存的对象,多余的对象会放入老年代。minor GC会stop the world,暂停用户线程,执行垃圾回收进程。
    <2>哪些对象会晋升老年代
    进行minor GC后,如果幸存区放不下幸存的对象,多余的对象会放入老年代。
    大对象直接晋升老年代(大对象的复制比较麻烦)
    寿命超过一个阈值的对象晋升老年代
    如果新生代中的对象有一般年龄相同,那大于这个年龄的对象会晋升。
    <3>major GC和full GC
    (2)子线程的内存溢出不会导致整个程序挺直运行。
    在这里插入图片描述

3、垃圾回收器

相连表示两个垃圾回收器可以配合工作,CMS和Serial Old的连线表示CMS在特定的情况下会转换为Serial Old。
在这里插入图片描述

  • Serial垃圾回收器:
    <1>特点:工作在新生代。单线程垃圾回收器,只会使用一个线程(一个处理器)去回收垃圾,并且回收垃圾时会暂停其他所有工作线程(Stop The World)。
    在这里插入图片描述
    <2>回收算法:复制算法。
  • Serial Old垃圾回收器(单线程)
    <1>特点:工作在老年代。单线程垃圾回收器,只使用一个线程进行垃圾回收,进行垃圾回收时会暂停所有工作线程(Stop The World)。
    <2>回收算法:标记整理算法。
  • ParNew垃圾回收器(并行)
    <1>特点:工作在新生代。Serial的多线程版本,ParNew收集器默认开启的收集线程数与CPU的数量相同,在进行垃圾回收时也会Stop The World。
    在这里插入图片描述
    <2>回收算法:复制算法。
    <3>调优指令: -XX:ParallelGCThreads用来限制垃圾回收线程的数目。
  • Parallel Scavenge垃圾回收器(并行)
    <1>特点:新生代垃圾回收器基本过程与ParNew垃圾回收器类似,但它的目标是达到一个可控的吞吐量,这与其它垃圾回收器的关注点不同(响应时间优先)。因此,Parallel Scavenge垃圾回收器也被称为吞吐量优先垃圾回收器。
    在这里插入图片描述
    在这里插入图片描述
    <2>垃圾回收算法:复制算法
    <3>调优指令:-XX:MaxGCPauseMillis设置垃圾收集停顿时间, -XX:GCTimeRatio设置吞吐量大小
  • Parallel Old垃圾回收器(并行)
    <1>特点:工作在老年代,是Parallel Scavenge的老年代版本,追求的目标也是达到一个可控的吞吐量。
    <2>垃圾回收算法:标记整理算法。
  • CMS垃圾回收器
    <1>特点:工作在老年代,最求的是响应时间(一次垃圾回收的时间尽可能短)。
    <2>垃圾回收算法:使用标记清除算法,工作的机制比较复杂主要分为以下几个阶段:
    1.初始标记:标记GC Roots对象,需要Stop The World。初始标记时间较短。
    2.并发标记:从GC Roots对象开始,标记所有对象。不需要Stop The World,与用户线程一起允许。由于用户线程会修改对象的引用关系,会导致标记结果不准确(漏标、误标)。并发标记时间很长
    3.重新标记:使用三色标记法,解决并发标记阶段的漏标、误标等问题,需要Stop The World。工作时间比初始标记略长,但比远小于并发标记的时间。
    4.并发清理:并发清理,清理掉死亡对象所在的分块,由于不需要移动对象,因此不需要Stop The World,与用户线程共同允许。
    <3>调优指令:
    -XX:CMSInitiatingOccupancyFraction=percent:当老年代内存占用比达到percent时开始垃圾回收(因为用户线程是并发的,如果老年代用完才进行清理,在还未清理完成是用户又创建了新的对象就会导致内存溢出)
    -XX:+CMSScavengeBeforeRemark:新生代的对象可能会引用老年代中的对象,而新生代中的对象大部分都会被回收,如果从新生代出发对老年代进行可达性分析就会做无用功。这个参数会在重写标记前对新生代进行回收,避免做无用功。
    <3>CMS垃圾回收器的退化问题:由于CMS使用的是标记清楚算法,会产生内存碎片,当碎片过多,垃圾回收失败时就CMS就会退化为Serial Old,使用标记整理算法。
    在这里插入图片描述
  • 上述垃圾回收算法分类:
    <1>单线程:Serial + Serial Old,适用于单核CPU
    <2>吞吐量优先:Parallel Scavenge + Parallel Old,适用于对响应时间要求不高,运行于后台的程序
    <3>响应时间优先:ParNew + CMS,适用响应时间要求较高的程序,比如网络应用。
  • 安全点与安全区域:
    <1>安全点:程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为“安全点(Safepoint) ”。个人理解就是,Stop The World并不是随时都能执行的,需要每个工作线程运行到指定的位置才能Stop
    The World。
    <2>安全区域:安全点机制要求每个线程都运行到指定的位置,但有的线程并没有执行(比如一个线程正在sleep),它就没办法运行到安全点。安全区域指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的。
    <3>安全区域发生GC的过程:
    1、在工作线程进入安全区域时,会标注线程进入安全区域,如果这时发生GC,JVM会忽略这个线程。
    2、工作线程离开安全区域时,如果这时JVM已经完成GC,工作线程可以离开安全区域,否则等待直到收到可以离开安全区域的信号。
    https://blog.csdn.net/qq_35077107/article/details/107822097?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-107822097-blog-123476687.topnsimilarv1&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-107822097-blog-123476687.topnsimilarv1&utm_relevant_index=1
    https://blog.csdn.net/weixin_45970271/article/details/123508686
  • G1垃圾回收器主要分为以下三个阶段:
    <1>Yong Collection
    程序启动时会默认给新生代分配5%的空间
    在这里插入图片描述
    当region满时会将新生代中的幸存对象拷贝到幸存区中
    在这里插入图片描述
    除了超大对象不会直接晋升老年代,对象晋升原则与分代回收类似。对于超大对象(大小超过region大小一半),会存放在一个humongous区,当进行新生代或老年代垃圾回收时会对大对象进行回收。在进行新生代垃圾回收时还好进行初始标记,因此也需要stop the world。
    <2>Yong Collection + Concurrent Mark
    当老年代内存使用超过一定比例时,进行Yong Collection后会进行Concurrent Mark(这与CMS类似)。
    <3>Mixed Collection(完成Yong Collection + Concurrent Mark后)
    会同时对新生代,幸存区和老年代进行垃圾回收。先进行stop the world,然后进行最终标记阶段。将新生代幸存对象拷贝到幸存区,将幸存区需要晋升的对象拷贝到一个新的老年代不需要晋升的对象拷贝到一个新的幸存区,老年代中的幸存对象也会拷贝到一个新的老年代(并不是所有的老年代都会进行垃圾回收,老年代垃圾较少,拷贝所有的老年代对象会花费大量的时间。为了达到低延迟的目标,一次只会回收一部分的老年代)。当进行没有空闲的region用于复制时,就会使用Serial进行标记整理。
  • G1垃圾回收器的一点点细节
    <1>卡表与remember set
    在新生代对老年代进行跨代引用(老年代引用了新生代)时,由于老年代存活对象比较多,通过新生代找老年代(判断新生代是否被老年代引用)需要花费大量的时间。因此将老年代在细分为一个个card,如果card中的一个对象引用了新生代就将其标记为脏卡,并且每个region都有一个remember set,记录它有哪些脏卡。当通过新生代找老年代时,只需要搜索对于的脏卡就行了。
    <2> JDK 8u20 字符串去重
    Java的String对象会存储一个char数组,当两个String对象的字符串内容相同时,就会让两个String对象的char数组指向同一个。
    所有新分配的字符串都会被放入一个队列中,当进行垃圾回收时G1检查是否有重复的字符串,如果有则让两个String对象指向同一个字符串。
    <3>JDK 8u40 并发标记类卸载
    所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类
    <4> JDK 9 并发标记起始时间的调整
    JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent设置一个阈值,超过整个阈值后就进行并发标记。
    JDK9以后动态调整并非标记的时间。
  • Full GC
    SerialGC
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足发生的垃圾收集 - full gc
    ParallelGC
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足发生的垃圾收集 - full gc
    CMS
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足,触发Concurrent Mode Failure时触发Full GC
    G1
    新生代内存不足发生的垃圾收集 - minor gc
    老年代内存不足,无多余Region可供拷贝,触发FullGC
  • 三色标记法
    不论CMS或者是G1垃圾回收器,进行标记时都分为了三部分:初始标记,并发标记和最终标记(重新标记三个阶段)。在并发标记阶段,用户线程也在对对象进行修改,并发标记对的结果可能不准确(垃圾变为非垃圾,非垃圾变为垃圾)。
    <1>在进行可达性分析的过程中,对象会被标记为三种颜色:
    白色:对象没有被访问过(如果一次完整的标记过后,它仍然为白色说明这个对象不可被访问,需要被回收)。
    灰色:这个对象已经被访问过,但它引用的对象有的还没被访问过。
    黑色:这个对象已经被访问过,它引用的对象都被访问过。
    <2>算法工作的过程
    首先从GC Roots对象开始处理,它被标记为黑色,它直接引用的对象标记为灰色。
    处理灰色对象,将其标为黑色,它直接引用的对象变为灰色。
    循环往复,直到所有对象处理完毕。
    并发标记时带来的问题:多标,垃圾变为非垃圾也就是产生浮动垃圾(在被标记为黑色后,线程断开了对它的引用,不需要处理顶多增加了一点垃圾),漏标,非垃圾变为垃圾(当前节点断开了对某个节点的引用,但之前的处理过的节点又引用这个节点)。垃圾变成非垃圾危害性小,而非垃圾变为垃圾危害性性大,因此主要解决非垃圾变为垃圾的问题。
    解决方法:
    <1>增量更新
    增量跟新就是在进行赋值操作前添加一个写屏障(写屏障就是一个操作,它保存了新添加的引用关系)。比如进行A.f = F操作时,将A标记为灰色,这样在进行重新标记阶段就会对F进行重新处理,解决了非垃圾变为垃圾的问题。
    <2>原始快照
    在进行置空操作时进添加一个写屏障(记录被置空的对象的引用)。比如一开始A.f = F,然后进行了A.f = null,这时添加一个写屏障(将F标记为黑色),如果它是垃圾,那等到下次GC时再对F进行处理即可。
    <3>二者对比:
    增量更新产生浮动垃圾更少,原始快照效率更高。CMS使用的是增量更新,G1使用的是原始快照。

四、类加载与字节码技术

1、类文件的结构

public class Demo3_1 {
  public static void main(String[] args) {
    int a = 10;
    int b = Short.MAX_VALUE + 1;
    int c = a + b;
    System.out.println(c);
 }
}

使用javap -v Demo3_1.class命令进行反编译后得到

//类文件信息
Classfile /root/Demo3_1.class
Last modified Jul 7, 2019; size 665 bytes
MD5 checksum a2c29a22421e218d4924d31e6990cfc5
Compiled from "Demo3_1.java"
public class cn.itcast.jvm.t3.bytecode.Demo3_1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
//常量池,如果一个整数在short的范围内,这个整数将存放在方法区中
Constant pool:
 #1 = Methodref     #7.#26     // java/lang/Object."<init>":()V
 #2 = Class       #27      // java/lang/Short
 #3 = Integer      32768
 #4 = Fieldref      #28.#29    //
java/lang/System.out:Ljava/io/PrintStream;
 #5 = Methodref     #30.#31    // java/io/PrintStream.println:(I)V
 #6 = Class       #32      // cn/itcast/jvm/t3/bytecode/Demo3_1
 #7 = Class       #33      // java/lang/Object
 #8 = Utf8        <init>
 #9 = Utf8        ()V
#10 = Utf8        Code
#11 = Utf8        LineNumberTable
#12 = Utf8        LocalVariableTable
#13 = Utf8        this
#14 = Utf8        Lcn/itcast/jvm/t3/bytecode/Demo3_1;
#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        MethodParameters
#24 = Utf8        SourceFile
#25 = Utf8        Demo3_1.java
#26 = NameAndType    #8:#9     // "<init>":()V
#27 = Utf8        java/lang/Short
#28 = Class       #34      // java/lang/System
#29 = NameAndType    #35:#36    // out:Ljava/io/PrintStream;
#30 = Class       #37      // java/io/PrintStream
#31 = NameAndType    #38:#39    // println:(I)V
#32 = Utf8        cn/itcast/jvm/t3/bytecode/Demo3_1
#33 = Utf8        java/lang/Object
#34 = Utf8        java/lang/System
#35 = Utf8        out
#36 = Utf8        Ljava/io/PrintStream;
#37 = Utf8        java/io/PrintStream
#38 = Utf8        println
#39 = Utf8        (I)V
//方法
{
public cn.itcast.jvm.t3.bytecode.Demo3_1();
//()V表示方法的类型,是一个无参的方法
 descriptor: ()V
//方法的修饰符
 flags: ACC_PUBLIC
 Code:
 //这个方法的操作数栈的信息,stack栈的深度,locals局部变量表的长度,args_size参数的个数
  stack=1, locals=1, args_size=1
  //指令
    0: aload_0
    1: invokespecial #1         // Method java/lang/Object."
<init>":()V
    4: return
//源代码的行号和字节码行号的对应关系
  LineNumberTable:
   line 6: 0
 //局部变量表
  LocalVariableTable:
  //start和Length 确定了局部变量的作用范围,Slot表示这个局部变量所在槽位
   Start Length Slot Name  Signature
     0    5   	  0   this  Lcn/itcast/jvm/t3/bytecode/Demo3_1;
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 8: 0
   line 9: 3
   line 10: 6
   line 11: 10
   line 12: 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
 MethodParameters:
  Name              Flags
  args
}

2、指令的执行过程

  • 类加载器对class文件进行加载,class文件中的常量 池将会被加载到运行时常量池,class文件中的方法将会被加载到方法区。在运行时,如果一个方法被调用,将会为这个方法创建一个栈帧,栈帧由两部分组成:局部变量表和操作数栈。局部变量表用来存储局部变量的值,操作数栈保存值主要用来进行加减乘除等。操作数栈的宽度为4个字节,如果放入的数据小于4个字节则补0,如果放入的数据大于4个字节则拆成几个较小的部分。
  • jvm首先从main方法开始执行,main方法的栈帧会被放入到虚拟机栈中。jvm解析main方法的指令,如果调用了某个对象的方法,先将这个对象的引用放入main方法的操作数栈中,然后弹出这个对象的引用,调用这个对象的方法(为这个方法创建一个栈帧,将栈帧放入方法栈中,然后执行这个方法,执行完毕后再弹出这个栈帧)。
    面试题:
public class Demo3_2 {
  public static void main(String[] args) {
    int a = 10;
    int b = a++ + ++a + a--;
    System.out.println(a);
    System.out.println(b);
 }
}

加减乘除等算法都是在操作数栈中执行的,而自增自减操作是直接在局部变量表中的对应槽位上进行的。赋值操作时,将常量池(short类型范围内的数在方法区中)的对于值放在对用的槽位上,当执行加减乘除等操作时再将槽位的数放加入操作数栈中。执行a++操作时,会先将a放入操作数栈中,然后再在对于的槽位上执行加1操作。
上面的列子中,先将10放入a对于的槽位上,然后再执行a++前,先将a放入操作数栈,然后槽位上a的值加1(也就是11),再执行++a,槽位上a再加1(也就是12),然后执行a++ + ++a,操作数栈中的元素出栈,然后相加得到22,将22存入操作数栈。执行a–,先将a放入操作数栈(也就是12入栈),然后a减一(也就是11),执行 a++ + ++a + a–,操作数栈的两个数(分别时22和12)出栈,然后相加,得到34,入栈,然后出栈赋值给b。所以最后得到a=11,b=34。
再看下面这个例子:

public static void main(String[] args) {
        int a = 10;
        a = a++;
        System.out.println(a);
        int b = 10;
        b++;
        b = b;
        System.out.println(b);
}

结构分别时10,和11,第一个,执行a = a++时,先将a放入操作数栈中,然后在a对应的槽位上将执行加1操作,槽位上a的值为11,在执行赋值操作,将栈中的元素弹出赋值给a也就是10。但下面这个,在进行赋值操作时就是将b的槽位上的值赋值给它本身因此是10。而a = a++赋值时是运算的结果,结果存放在操作数栈中。

3、cinit方法与init方法

在编译阶段,编译器会从上到下收集所有的静态赋值语句和静态代码块,然后将它们合并为一个()V方法,这个方法在类初始化时会被调用。
对于{}代码快与成员变量赋值语句,编译时会从上到下收集并且与原来的构造函数合并为一个新的构造函数(构造函数的语句放在最后面)。

4、多态的实现(虚方法表)

public class Demo3_9 {
  public Demo3_9() { }
  private void test1() { }
  private final void test2() { }
  public void test3() { }
  public static void test4() { }
  public static void main(String[] args) {
    Demo3_9 d = new Demo3_9();
    d.test1();
    d.test2();
    d.test3();
    d.test4();
    Demo3_9.test4();
 }
}

编译后的文件进行反编译得到

0: new      #2         // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3         // Method "<init>":()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: aload_1
21: pop
22: invokestatic #7         // Method test4:()V
25: invokestatic #7         // Method test4:()V
28: return

由于test1和test2方法不可被重写,因此不可能实现多态,编译后用invokespecial调用。test4是静态方法,也不可能被重写,因此用invokestatic调用(注意使用对象调用,而test3是public的,可能被重写,因此使用虚方法表进行调用)。
虚方法:
为每个类类型生成一个虚方法表,这个表其实就是一个数组,记录了这个类自己的方法(重写的父类的方法也算是自己的方法)和从父类中继承来的方法(这个方法并没有被重写)。方法表需要满足以下条件:
子类方法表,包含父类方法表中的所有方法
子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同
在执行过程中,如果是静态绑定就直接执行绑定的方法,如果是动态绑定需要先解析实际指向的对象,然后查找对应对象的方法表,执行对应的方法即可。
参考链接:https://baijiahao.baidu.com/s?id=1720665109262635460&wfr=spider&for=pc

  • try,cach,finally的实现原理
    对于
public class Demo3_11_4 {
  public static void main(String[] args) {
    int i = 0;
    try {
      i = 10;
   } catch (Exception e) {
      i = 20;
   } finally {
      i = 30;
   }
 }
}

编译后会得到

public static void main(java.lang.String[]);
  descriptor: ([Ljava/lang/String;)V
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
   stack=1, locals=4, args_size=1
    0: iconst_0
    1: istore_1   // 0 -> i
    2: bipush     10   // try --------------------------------------
    4: istore_1       // 10 -> i                 |
    5: bipush     30   // finally                 |
    7: istore_1       // 30 -> i                 |
    8: goto      27   // return -----------------------------------
    11: astore_2       // catch Exceptin -> e ----------------------
    12: bipush     20   //                     |
    14: istore_1       // 20 -> i                 |
    15: bipush     30   // finally                 |
    17: istore_1       // 30 -> i                 |
    18: goto      27   // return -----------------------------------
    21: astore_3       // catch any -> slot 3 ----------------------
    22: bipush     30   // finally                 |
    24: istore_1       // 30 -> i                 |
    25: aload_3       // <- slot 3                |
    26: athrow        // throw ------------------------------------//抛出异常
    27: return
    //用来监测某几行是否会抛出异常
   Exception table:
    from   to  target type
      2   5   11  		Class java/lang/Exception //如果2到5行发生了java/lang/Exception异常就直接跳转到11行
      2   5   21  		any   // 剩余的异常类型,比如 Error
      11   15   21  	any   // 剩余的异常类型,比如 Error
   LineNumberTable: ...
   LocalVariableTable:
    Start  Length  Slot  Name  Signature
     12    3   2   e  Ljava/lang/Exception;
      0    28   0  args  [Ljava/lang/String;
      2    26   1   i  I
   StackMapTable: ...
  MethodParameters: ...

本质上就是一个分支语句,分别是3个块try块,catch块和finally块。元素代码中的finally块中的类容会附加到try块和catch块中,由此保证finally块的内容一定会被执行。
面试题:

public static int test() {
    try {
      return 10;
   } finally {
      return 20;
   }
 }

会返回20,因为finally语句也会被执行,注意如果在finally语句中添加了return语句则异常会被吞掉,即使发生异常也不会被抛出。

public static int test() {
    int i = 10;
    try {
      return i;
   } finally {
      i = 20;
   }
 }

返回时,谁染finally中的语句也执行了,但是try块里的结果被保存了起来,最后返回的还是try块里的结果(根据经验判决就行)。

5、类的加载阶段

  • 加载阶段
    类的加载就是将类的class文件放入方法区中,而jvm是用C++的数据结构instanceKlass去描述java的类,instanceKlass的属性如下:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表

需要注意的是java无法直接访问instanceKlass,jvm在堆中提供了与instanceKlass相互映射的一个class对象
在这里插入图片描述
class对象与instanceKlass相互持有彼此的地址,当调用某对象的方法时先去找它的类对应的class对象,再由class对象找instanceKlass。
如果一个类的父类没有加载先去加载父类。

  • 链接阶段(链接阶段与加载阶段可能是交替进行的)
    <1>验证:验证这个类是否符合规范
    <2>准备: 为 static 变量分配空间,设置默认值
    static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
    static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
    如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶
    段完成
    如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
    <3>解析:将常量池中的符号引用解析为直接引用
public class Load2 {
  public static void main(String[] args) throws ClassNotFoundException,
IOException {
    ClassLoader classloader = Load2.class.getClassLoader();
    // loadClass 方法不会导致类的解析和初始化
    Class<?> c = classloader.loadClass("cn.itcast.jvm.t3.load.C");
   
    // new C();
    System.in.read();
 }
}
class C {
  D d = new D();
}
class D {
}

在加载过后,D只是一个符号,并不知道D是一个对象。

  • 初始化:执行类的()V
    发生的时机
    概括得说,类初始化是【懒惰的】
    main 方法所在的类,总会被首先初始化
    首次访问这个类的静态变量或静态方法时
    子类初始化,如果父类还没初始化,会引发
    子类访问父类的静态变量,只会触发父类的初始化
    Class.forName
    new 会导致初始化
    不会导致类初始化的情况
    访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
    类对象.class 不会触发初始化
    创建该类的数组不会触发初始化
    类加载器的 loadClass 方法
    Class.forName 的参数 2 为 false 时

6、类加载器

  • 类加载器的功能
    <1>找到.class文件,将.class转为字节流
    <2>将字节加载到jvm中(类加载)
  • 类加载器的种类
    类加载器大体上分为两类,引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
    <1>引导类加载器
    使用C++编写,它只加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sum.boot.class.path路径下的内容)。
    <2>自定义类加载器:说有直接或者间接继承ClassLoader类的加载器。
名称加载哪些类上级实现类继承自哪个类
Extension ClassLoader(扩展类加载器)JAVA_HOME/jre/lib/ext上级为 Bootstrap,显示为 nullLauncher.ExtClassLoader直接继承自URLClassLoader,间接继承自ClassLoader
Application ClassLoader (应用程序类加载器)classpath上级为Extension ClassLoaderLauncher.AppClassLoader直接继承自URLClassLoader间接继承自ClassLoader
用户自定义类加载器用户自定义类的加载路径上级为Application ClassLoader继承URLCLassLoader或者继承ClassLoader
  • 类加载器的类加载器
    除了Bootstrap ClassLoader,其他类加载器都是java的类,加载这些类也需要类加载器。
名称类加载器
Extension ClassLoader(扩展类加载器)Bootstrap ClassLoader
Application ClassLoader (应用程序类加载器)Bootstrap ClassLoader
用户自定义类加载器用户自定义类的加载路径
  • 双亲委派机制
    <1>双亲委派机制:一个类加载器加载类时会先看当前类加载器有没有加载过这个类,如果没有加载过则让父类加载器加载,如果父类加载器加载失败则自己加载。
    在这里插入图片描述
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //查看当前类加载器是否已经加载过这个类
            Class<?> c = findLoadedClass(name);
            //没有加载过
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	//如果有父类加载器让父类加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }
                //父类加载器加载失败(父类加载器对应的路径下没有这个类)则自己加载。
                if (c == null) {
                    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;
        }
    }

<2>注意点:如果用户自定义类加载器重写了loadClass()方法就不会走双亲委派 。

  • 类加载器分类的原因和使用双亲委派的原因。
    <1>不同类加载器的加载的路径不同,使用不同的类加载器可以加载不同路径的类。
    <2>可以保证安全,对于一些核心的类库,只有Bootstrap ClassLoader可以加载,如果我们在classpath下新建了一个全限定名与核心类库中相同的类,由双亲委派原则,最终会由Bootstrap ClassLoader去加载,而Bootstrap ClassLoader并不会去加载claspath中的类,这就可以防止核心类库中的类被修改。
    举个例子,classpath中的类的加载都依赖于Application ClassLoader,如果我们自己在classpath目录下创建一个全限定名与Luncher(Application ClassLoader是Luncher的匿名内部类)完全相同的类,然后重写Application ClassLoader中的loadClass方法,在里面写一个死循环。如果没有双亲委派机制,类加载器就会使用classpath下的Application ClassLoader,加载类时就会执行它的loadClass方法,这就会导致程序崩溃。
    这种安全机制也称为沙箱安全机制。
  • 自定义类加载器的实现
    <1>使用场景
    想加载自定义类路径下的class文件。
    <2>使用步骤
    1.继承ClassLoder类
    2.重写loadClass()或者findClass()方法,loadClass()会调用findClass()方法,如果重写loadClass()方法就不会走双亲委派。
public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String path = "C:\\Users\\53202\\Desktop" + name + ".class";
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            Files.copy(Paths.get(path), os);
            byte[] bytes = os.toByteArray();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
            throw new ClassNotFoundException("类文件未找到");
        }
    }
}
  • 类的唯一标志
    jvm中用类加载器+类的全限定名唯一标志一个类,因此在jvm中可以出现全限定名(包名 + 类名)完全一致的类,但jvm将这两个类视为不同类。如下,使用两个不同的自定义类加载器加载Example类(Example类不在classpath下,否则会走双亲委派),返回的Class对象不相同。
class Solution {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        ClassLoader loader1 = new MyClassLoader();
        Class clazz1 = loader1.loadClass("Example");
        ClassLoader loader2 = new MyClassLoader();
        Class clazz2 = loader2.loadClass("Example");
        System.out.println(clazz1 == clazz2);
    }
}
false

7、运行时优化

  • 分层编译
    jvm将执行状态分为了5个层次
    0 层,解释执行(Interpreter)
    1 层,使用 C1 即时编译器编译执行(不带 profiling)
    2 层,使用 C1 即时编译器编译执行(带基本的 profiling)
    3 层,使用 C1 即时编译器编译执行(带完全的 profiling)
    4 层,使用 C2 即时编译器编译执行
    解释执行:在执行时将字节码解释为机器码,在下次执行这条代码时仍然会解释执行。
    编译执行:将一些热点代码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译。
  • 逃逸分析
    对于那些产生无用对象的代码(比如 new String()),不会再执行。
  • 如果发现 某个方法是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置
  • 常量折叠
    对于9 * 9会直接折叠为81
  • 字段优化
    如果某个对象的方法使用了这个对象内部的某些属性,在每次执行时都会去读取这个属性。在进行方法内联时,会直接将者属性拷贝到缓存中,这样就不需要每次都去拷贝。在执行一定次数对这个属性的访问后,也会将这个属性放入缓存中。也可以在编码时,在方法内部拷贝这个属性。
  • 反射优化
    在执行反射时会默认使用本地方法,如果重复执行超过16次则会 使用 ASM 动态生成的新实现代替本地实现。

8、JMM(java内存模型)

java内存区域与java内存模型是两个不同的概念。
java的内存区域分为方法区、堆、虚拟机栈、本地方法栈、程序计数器。
JMM指的是定义了共享变量访问方式的一套规范
JMM将内存区域划分为了两部分:主内存(对应着方法区和堆,线程共享的存储区),工作内存(对应着堆、虚拟机栈、本地方法栈、程序计数器)。一个线程对共享变量进行修改时,必须先将主内存的变量复制一份到工作内存中,在工作内存中完成对共享变量的修改,修改完成后再放回主内存。JMM定义了一组规则可以保证原子性,可见性和有序性。

  • 原子性:一个操作不可被中断
    可以通过使用synchronized关键字或者CAS + volatile关键字来保证某个操作的原子性。
    <1>synchronized:在编译器编译时会添加加锁和解锁操作,会不断尝试解锁,如果解锁失败则重新解锁。
    对象头中的Mark Word 平时存储这个对象的 哈希码 、 分代年龄 ,当加锁时,这些信息就根据情况被替换为 标记位 、 线程锁记录指针 、 重量级锁指针 、 线程ID 等内容。
    轻量级锁:当一个线程(假设是A)对一个对象加轻量级锁时,想将Mark复制到这个线程中存放起来,CAS修改Mark为线程的锁记录地址并且将标志位设置为轻量级锁(00表示轻量级锁,01表示无锁),线程执行完毕后解锁。如果一个线程重复对某个对象加锁,加锁失败发现是加的锁就进行锁重入。
    重量级锁:如果另一个线程(假设是B)加锁失败并且发现不是自己加的锁就进行锁膨胀,CAS 修改 Mark 为重量锁(将01改为10),并且进入阻塞状体。A释放轻量级锁,释放失败后释放重量级锁并唤醒B。
    自旋:B直接进入阻塞,唤醒将会进行上下文切换,耗费时间。B会循环尝试一段时间(自旋),失败后再进入阻塞。
    偏向锁:轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID是自己的就表示没有竞争,不用重新 CAS。
    参考链接

    <2>CAS + volatile
    CAS是基于一种乐观锁的思想。

while(true) {
 int 旧值 = 共享变量 ; // 比如拿到了当前值 0
 int 结果 = 旧值 + 1; // 在旧值 0 的基础上增加 1 ,正确结果是 1
 /*
  这时候如果别的线程把共享变量改成了 5,本线程的正确结果 1 就作废了,这时候
  compareAndSwap 返回 false,重新尝试,直到:
  compareAndSwap 返回 true,表示我本线程做修改的同时,别的线程没有干扰
 */
 if( compareAndSwap ( 旧值, 结果 )) {
  // 成功,退出循环
}
}

先将共享变量的值读入,然后修改,再判断共享变量的值有没有变,如果没有变则将修改的值写回主内存,变了则写入失败,重新尝试。由于需要保证每次读入的共享变量的值都是最新的,所以需要在共享变量前添加volatile关键字。结合 CAS 和 volatile 可以实现无
锁并发,适用于竞争不激烈、多核 CPU 的场景下。
CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令,juc(java.util.concurrent)中提供了原子操作类,可以提供线程安全的操作,例如:AtomicInteger、AtomicBoolean等,它们底层就是采用 CAS 技术 + volatile 来实现的。

  • 可见性:
    当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
    不可见性主要是由jvm在运行时的优化导致的,当某个线程对某个共享变量访问次数达到一个阈值后,将会把这个属性放入缓存中,下次访问时将直接访问缓存中的值。在这个共享变量前加volatile关键字,在对这个变量访问时会强制去主内存中取。
  • 有序性:
    代码的执行是按顺序依次执行的
    jvm会使用指令重排来进行优化,在共享变量上加volatile将会禁用指令重排。
  • happens-before:
    哪些写操作对读操作可见(就是对前面的可见性和有序性的总结)

五、Java中的语法糖

1、自动装箱与拆箱

  • 手动装箱与拆箱
//装箱
Integer num = Integer.valueOf(1);
//拆箱
int n = num.intValue();
  • 自动装箱与拆箱,编译时会自动生产手动装箱与拆箱的代码
//装箱
Integer num = 1;
//拆箱
int n = num;

2、泛型擦除

List<String> list = new ArrayList<>();
//list中添加的其实是Object类型的对象,而不是String类型的对象,这就叫做泛型擦除。但在执行list.add()时,会先检查里面传入的数据是不是泛型的类型。
list.add("hello");
//list.get(0)得到的是一个Object对象,编译器在执行String s = list.get(0)时等价于执行String s = (String)list.get(0);
String s = list.get(0);

需要注意的是,泛型信息存放在LocalVariableTable中,以供类型检查和类型转换

3、枚举

public enum Sex {
    MALE, FEMALE
}

等价于下面代码,也就是创建一个类继承了Enum类,并且提供了几个静态变量。并且枚举提供了readResolve方法,可以防止反序列化破坏单例。

public final class Sex extends Enum<Sex> {
	public static final Sex MALE;
	public static final Sex FEMALE;
	private static final Sex[] $VALUES;
	static {
		MALE = new Sex("MALE", 0);
		FEMALE = 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);
	}
}

参考链接

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值