JAVA虚拟机

一、Java内存区域和内存模型

1、JDK体系和Android体系

1. JDK体系
JDK体系

JDK:Java Development Kit(java开发工具包),包含JRE和开发工具包,例如javac、javah(生成实现本地方法所需的 C 头文件和源文件)。
JRE:Java Runtime Environment(java运行环境),包含JVM和类库。
JVM:Java Virtual Machine(Java虚拟机),负责执行符合规范的Class文件。


JDK跨平台特性

JDK跨平台特性

JDK跨平台特性是因为可以将class文件转换成不同系统的机器码执行。
Java包含两个编译器(编译器和即时编译器)和一个解释器;

2. Android体系
Android体系
  • Android虚拟机就位于Android Runtime;
  • Android虚拟机是面向Linux,嵌入式操作系统的虚拟机,主要负责生命周期管理、堆栈管理、线程管理、安全和线程管理,垃圾回收等。
  • Android虚拟机分为Dalvik虚拟机和ART虚拟机。

Android虚拟机进化史:

  • Android 1.1 Dalvik虚拟机:解释器
    最早的虚拟机,采用的是边编译,边执行。

  • Android 2.2 Dalvik虚拟机:解释器+JIT(just-in-time即时编译)
    执行过程中,每遇到一个新的类别,都会被编译优化成相当精简的原生型指令码,下次执行到相同逻辑的时候,速度就会更快。
    JIT编译产生的机器指令保存在内存中,不会进行持久化存储,所以应用每次启动都会重新进行编译。
    Dalvik虚拟机执行程序dex文件前,系统会对dex文件做优化,生成可执行文件odex,保存到data/dalvik-cache目录。
    正常的执行流程,安装APK,将dex文件通过Dx工具转化为odex文件,启动APP的时候,采用JIT技术将odex字节码转化为机器指令,CPU执行。

  • Android 4.4 ART虚拟机:AOT(Ahead-of-time预编译)
    Art采用的是AOT模式,AOT(Ahead-of-time)即在应用安装的时候,dex文件就会被预先编译可执行文件,这个过程就叫做预编译。
    具体过程,安装APK的时候调用dex2oat,把.dex文件编译oat文件并保存到磁盘中,该文件采用的是成ELF文件格式,该文件格式为native code机器可以直接运行的格式,每次应用启动不用重新编译。
    安装过程中耗时增加,启动速度有很大提升,增加了存储空间的使用,是一种空间换时间的策略。
    默认的还是Dalvik虚拟机,Android 5.0之后系统虚拟机彻底切换为ART虚拟机。

  • Android 7.0 AOT+解释执行+JIT
    Android7.0开始采用混合模式,即AOT+JIT+解释执行3种模式共存的方式解决安装时间过长。
    工作过程如下:首先,在应用安装时dex文件不会被预先编译成机器码。然后,在App运行时,dex文件先通过解释器直接执行,热点函数会被识别并被JIT编译后存储在JIT code cache中并生成profile文件记录热点函数信息。最后当手机进入idle状态或者充电状态,系统扫描app目录下的profile文件进行AOT编译。

  • Android 8.0 ART改进
    新的并发压缩式垃圾回收器(GC)。该回收器会在每次执行 GC 时以及应用正在运行时对堆进行压缩,且仅在处理线程根时短暂停顿一次。

3. JVM和DVM区别

1、解释

  • JVM是Java Virtual Machine的缩写,叫做 java虚拟机。
  • DVM是Dalvik Virtual Machine的缩写,叫做 Dalvik虚拟机。

2、关系

  • DVM是针对JVM而言的,JVM是Oricle公司的产品,担心版权问题,既然java是开源的,索性就研究了JVM,写出了DVM。
  • DVM是针对移动设备而生的虚拟机。

3、基于的架构不一样

  • Java是基于栈的架构,栈是内存上面一段连续的存储空间。
  • Android是基于寄存器的架构,寄存器是cpu上的一块存储空间。

所以cpu直接访问自己上面的一块空间的数据的效率肯定要大于访问内存上面的数据。

4、执行的字节码文件不一样

  • JVM: .java-->.class-->.jar
  • DVM: .java-->.class-->.dex-->(Dalvik:odex文件-->机器指令)或(ART:oat文件机器可读)

.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的头信息(如编译版本)、常量池、类信息、域、方法、属性等等,当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,这样会很慢,而移动设备的内存本来就很小,不可能像JVM这样加载,所以它使用的不是.jar文件,而是.apk文件,该文件里面只包含了一个.dex文件,这个.dex文件里面将所有的.class里面所包含的信息全部整合在一起了,这样再加载就很快了。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作。

4、执行的字节码文件不一样

  • JVM: 只能运行一个实例,即所有的应用都运行在同一个JVM中。
  • DVM: 每个应用启动都运行一个单独的DVM,每个DVM单独占用一个Linux进程。

2、Java内存区域

JVM运行时内存区域
  • 方法区(公有):即元数据区

用户存储已被虛拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。异常状态 OutOfMemoryError。
其中包含常量池:用户存放编译器生成的各种字面量和符号引用。

  • 堆(公有):

JM所管理的内存中最大的一块。唯一目的就是存放实例对象,几乎所有的对象实例都在这里分配。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为“GC堆”。异常状态 OutOfMemoryError。

  • 虛拟机栈 (线程私有):

描述的是java方法执行的内存模型:每个方法在执行时都会创建一个栈帧,用户存储局部变量表,操作数栈,动态连接,方法出口等信息。一个方法从调用直至完成的过程,就对应着一个栈帧在虛拟机栈中入栈到出栈的过程。对这个区域定义了两种异常状态 OutOfMemoryError和StackOverflowError。

  • 本地方法栈(线程私有):

与虛拟机栈所发挥的作用相似。它们之间的区别不过是虛拟机栈为虛拟机执行java方法,而本地方法栈为虛拟机使用到的Native方法服务。

  • 程序计教器(线程私有):

一块较小的内存,当前线程所执行的字节码的行号指示器。字节码解释器工作时,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

3、Java内存模型

Java内存模型
1. Java内存模型JMM(Java memory model)

定义:Java 内存模型中规定了所有的变量都存储在主内存中,每个线程还有自己的工作内存(类比缓存理解),线程的工作内存中保存了该线程使用到主内存中的变量拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递(通信)均需要在主内存来完成,
作用:屏蔽掉各种硬件/操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的内存访问效果。
主要目标:是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

2. 八种操作

关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。

Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:

  • 不允许read和load、store和write操作之一单独出现,以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
  • 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
  • 一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
  • 如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。

参考: JVM的艺术—JAVA内存模型

二、Java类加载机制和类加载器

1、Java类加载机制

1. 定义

把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始
化,最终形成可以被虛拟机直接使用的Java类型。

在Java语言里,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策咯虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点来实现的。

2. 类的生命周期
类的生命周期
  • 七个阶段:加载,验证,准备,解析,初始化,使用和卸载。

1、其中验证,淮备,解析3个部分统称为连接。
2、加载,验证,准备,初始化,卸载这5个阶段的顺序是确定的,而解析阶段则不一定:它在某些情況下可以在初始化完成后在开始,这是为了支持Java语言的运行时绑定。
3、其中加载,验证,准备,解析及初始化是属于类加载机制中的步骤。注意此处的加载不等同于类加载。

  1. 加载
    类加载过程的一个阶段,ClassLoader通过一个类的完全限定名查找此类字节码文件,并利用字节码文件转换为方法区内的运行时数据结构,在内存中创建一个class对象作为方法区这个类的数据访问入口。

  2. 验证
    目的在于确保class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证。

  3. 准备
    为类变量(static修饰的字段变量)分配内存并且设置该类变量的初始值,(如static int i = 5 这里只是将 i 赋值为0,在初始化的阶段再把 i 赋值为5),这里不包含final修饰的static ,因为final在编译的时候就已经分配了。这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到Java堆中。

  4. 解析
    这里主要的任务是把常量池中的符号引用替换成直接引用。

  5. 初始化
    初始化就是对类变量进行赋值及执行静态代码块。
    这里是类加载的最后阶段,执行类构造器()方法的过程。如果该类具有父类就对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量(前面已经对static 初始化了默认值,这里我们对它进行赋值)。成员变量也将被初始化。

方法的区别:
Class.forName()得到的class是已经初始化完成的。
Classloader.loaderClass得到的class是还没有链接(验证,准备,解析三个过程被称为链接)的。

注意执行顺序:
静态代码块>构造代码块>构造函数>普通代码块
如果有父类顺序:
父类静态代码块>子类静态代码块>父类构造代码块>父类构造方法>子类构造方法>子类构造代码块

public class CodeBlock {
    static{
        //静态代码块在类被加载的时候就运行了,而且只运行一次
        System.out.println("静态代码块");
    }
    {
        //构造代码块在创建对象时被调用,每次创建对象都会调用一次,但是优先于构造函数执行。
        System.out.println("构造代码块");
    }
    public CodeBlock(){
        //构造函数的功能主要用于在类的对象创建时定义初始化的状态。
        System.out.println("无参构造函数");
    }
     
    public void sayHello(){
        {
            //普通代码块的执行顺序和书写顺序一致
            System.out.println("普通代码块");
        }
    }
     
    public static void main(String[] args) {
        System.out.println("执行了main方法");
        new CodeBlock().sayHello();;
    }
}
3. 触发类加载的条件
  1. 遇到new、getstatic、pubstatic、invokestatic这四条字节码指令时。
    new :new关键字实例化对象;
    getstatic、pubstatic:读取或设置一个类的静态字段的时候;
    invokestatic:调用类的静态方法的时候;
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候;
  3. 初始化一个类发现父类没有没初始化,则先初始化父类;
  4. 虚拟机启动,先初始化用户指定的执行主类(即包含main方法)
  5. JDK1.7动态语言支持,一个java.lang.invoke.MethodHandler实例解析引用了静态类并且没有初始化,则先初始化这个类。

2、类加载器

作用:将类的Class文件读入内存,并为之创建一个java.lang.Class对象。

1. 类加载器分类
  1. 启动加载器(Bootstrap ClassLoader):
    由C++语言实现(针对HotSpot),负
    责将存放在Vib目录或-Xbootclasspath参数指定的路径中的类库加载到内存中,即负责加载Java的核心类。
  2. 其他加载器:
    由Java语言实现,继承自抽象类ClassLoader。
    1. 扩展类加载器 (Extension Class Loader):
      负责加载Viblext目录或java.ext.dirs系统变量指定的路径中的所有类库,即负责加载Java扩展的核心类之外的类。
  • 2.应用程序类加载器 (Application ClassLoader):
    负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器,通过ClassLoader getSystemClassLoaderQ方法道接获取。一般情況,如果我们没有自定义类加载器默认就是用这个加载器。
2. 双亲委派模型
双亲委派模型的工作流程是:

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范国中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。


双亲委派模型
优点:
  1. Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。
  2. 考虑到安全因素比如所有Java对象的超级父类java.lang.Object,位于rt.jar,无论哪个类加载器加载该类,最终都是由启动类加载器进行加载,保证安全。即使用户自己编写一个java.lang.Object类并放入程序中,虽能正常编译,但不会被加载运行,保证不会出现混乱。

注意:
在JVM中标识两个Class对象,是否是同一个对象存在的两个必要条件:
1、类的完整类名必须一致,包括包名。
2、加载这个ClassLoader(指ClassLoader实例对象)必须相同。

3. 双亲委派代码实现
protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized(this.getClassLoadingLock(name)) {
            //检查该类是否已被加载过了
            Class c = this.findLoadedClass(name);
            if (c == null) {
                //该类没有被加载过进入
                long t0 = System.nanoTime();
                try {
                    if (this.parent != null) {
                        //父类加载器不为空则父类加载
                        c = this.parent.loadClass(name, false);
                    } else {
                        //父类加载器为空则调用启动加载器加载
                        c = this.findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException var10) {
                      //非空父类加载器无法找到相应的类抛出异常
                }

                if (c == null) {
                    //父类加载器无法加载则调用findClass加载;自定义加载器也是覆写这个方法
                    long t1 = System.nanoTime();
                    c = this.findClass(name);
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }

            if (resolve) {
                 //对类进行link操作(即包括校验,准备,解析)
                this.resolveClass(c);
            }

            return c;
        }
    }
4. Android的ClassLoader

PathClassLoader 和 DexClassLoader类

//类路径:/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
 public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

//类路径: /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

可以看到,两者都是继承自BaseDexClassLoader ,构造方法的具体逻辑在父类中实现,唯一不同的是一个参数:optimizedDirectory;
最终在Native 层的源码追踪中DexFile_openDexFileNative

static jint DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {    
    //...略
    const DexFile* dex_file;    
    if (outputName.c_str() == NULL) {// 如果outputName为空,则dex_file由sourceName确定
        dex_file = linker->FindDexFileInOatFileFromDexLocation(dex_location, dex_location_checksum); 
    } else {// 如果outputName不为空,则在outputName目录中去寻找dex_file
        std::string oat_location(outputName.c_str());    
        dex_file = linker->FindOrCreateOatFileForDexLocation(dex_location, dex_location_checksum, oat_location);  
    }    
    //...略
    return static_cast(reinterpret_cast(dex_file));    
}

PathClassLoader 和 DexClassLoader分别调用不同的方法;
DexClassLoader通过 outputName拿到oat_location地址,加载对应地址 oat_location的dex文件(即SDK存储位置文件);
PathClassLoader从 dex_location 中拿到 dex 文件;

总结
1、Android 中,apk 安装时,系统会使用 PathClassLoader 来加载apk文件中的dex,PathClassLoader的构造方法中,调用父类的构造方法,实例化出一个 DexPathList ,DexPathList 通过 makePathElements 在所有传入的dexPath 路径中,找到DexFile,存入 Element 数组,在应用启动后,所有的类都在 Element 数组中寻找,不会再次加载。
2、在热更新时,实现 DexClassLoader 子类,传入要更新的dex/apk/jar补丁文件路径(如sd卡路径中存放的patch.jar),通过反射拿到 DexPathList,得到补丁 Element 数组,再从Apk原本安装时使用的 PathClassLoader 中拿到旧版本的 Element 数组,合并新旧数组,将补丁放在数组最前面,这样一个类一旦在补丁 Element 中找到,就不会再次加载,这样就能替换旧 Element 中的旧类,实现热更新。

三、对象的创建、内存布局、访问定位

1、对象的创建过程

  1. 虛拟机遇到一个new指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用;
  2. 检查这个符号引用代表的类是否已经被加载,解析和初始化过。如果没有,那必须先执行响应的类加载过程;
  3. 在类加载检查通过后,为新生对象分配内存。对象所需的内存大小在类加载完成后便可完全确定;
  4. 内存分配完成以后,虚拟机需要将分配的内存空间都初始化为零值,保证了对象的实例字段在Java代码中可以不赋予初值能直接使用,程序能访问到这些字段的数据类型对应的零值。

2、内存布局

对象的内存布局一般分为三个部分:对象头,实例数据,对齐填充

1. 对象头中包括两部分:

第一部分:存放着对象自身的运行时数据,如哈希码,GC分带年龄,锁状态标志,偏向线程ID,线程持有的锁。
第二部分:类型指针,即对象指向它的类元数据的指针。若是数组还有记录数组长度的数据。因为虚拟机可以通过普通java对象的元数据信息确定java对象的大小。

2. 实例数据:

对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

3. 对齐填充:

对齐填充不是必然存在的。HotSpot VV的自动内存管理系统要求对象起始地址必须是8字节的整数倍,也就是说对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对其补充来补全了。

3、访问定位

Java程序需要通过栈上的reference数据来操作堆上的具体对象。
目前主流的方式有两种:句柄访问和直接指针

1. 句柄访问:

Java堆中会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对实例数据与类型数据各自具体的地址信息,


句柄访问
2. 直接指针:

reference中存储的直接就是对象地址。


直接指针
3. 两种方式比较:

使用句柄的好处就是引用中存储的是稳定的句柄地址,当被移动时只会修改句柄中的实例数据指针,而引用地址不会被改变。
使用直接指针访问方式的最大好处就是速度更快,它节省了一次访问指针定位的时间开销,引用直接指向存放实例数据的堆内存,在该内存中存放着指向方法区的类型数据地址。

4、Java对象的生命周期

java对象的生命周期可以分为7个阶段:创建阶段、使用阶段、不可视阶段、不可达阶段、可收集阶段、终结阶段、释放阶段。

  1. 创建阶段
    java创建一个对象的方式:
  • 使用new关键字。
  • 使用反射机制。
  • 对象clone。Object类中存在clone(),但访问权限为protected,因此被clone的类需要实现Cloneable接口,将方法权限提升为public。Cloneable只是一个标识接口。
  • 使用序列化。
  1. 使用阶段
    四类对象引用:强引用、软引用、弱引用、虚引用。
  • 强引用:强引用对象无论如何都不会被回收。当出现内存不足的情况,宁愿抛出内存溢出(OutOfMemoryError)错误。
  • 软引用:具有与强引用相同的引用功能,只有当JVM出现内存不足时,对象会被回收。
    -弱引用:无论内存是否充足,对象都会被GC回收。
  • 虚引用:主要用于辅助finalize函数的使用,标识那些处于不可达,但是却未被GC回收的对象。
  1. 不可视阶段
    对象使用已经结束,并且在其可视区域不再使用。此时应主动将对象置为null,有助于JVM及时发现该垃圾对象。

  2. 不可达阶段
    JVM通过可达性分析,从root集合中找不到该对象直接或间接的强引用。此时该对象被标注为GC回收的预备对象,但没有被直接回收。
    在可达性分析时可作为root的对象:

  • 被启动类(bootstrap 加载器)加载的类和创建的对象;
  • 栈(本地方法栈和java栈)引用的对象。
  • 方法区中静态引用指向的对象。
  • 方法区中常量引用指向的对象。
  1. 可收集阶段
    GC已经发现该对象不可达。

  2. 终结阶段
    finalize方法已经被执行

  3. 释放阶段
    对象空间已经被重用

四、JVM垃圾回收机制

1、垃圾收集算法

1. 标记-清除法(Mark-Sweep)

分标记清楚两部分:首先标记需要被清除数据;标记完成后统一回收。
缺点:

  • 效率不足:标记和清除效率都不高;
  • 空间问题:标记清除后产生大量不连续的内存碎片;
2. 复制算法

解决标记-清除法效率问题;将内存按容量大小划分为大小相等的两块;每次只使用一块,当一块内存使用完了,将存活对象复制到另一块。
缺点:

  • 将内存缩小为原来的一半;

HotSpot虚拟机将内存分为Eden和两块Survivor,比例为8:1:1;

3. 标记-整理法

复制算法对象存活率高,多次复制,效率变低;根据老年代特点,首先标记需要被清理的数据,然后让存活的对象都向一端移动,清理掉边界外的内存;

4. 分代收集算法

把JAVA堆分为新生代和老年代,根据各个年代特点选用适当算法:
新生代:存活率低,选用复制算法;
老年代:存活率高,选用标记整理法;

2、垃圾回收机制知识

架构图
1. 垃圾回收

收集并删除未引用的对象。可以通过调用System.gc()来触发[垃圾回收],但并不保证会确实进行垃圾回收。JVM的垃圾回收只收集哪些由new关键字创建的对象。所以,如果不是用new创建的对象,你可以使用finalize函数来执行清理。

2. JVM中的年代

把对象根据存活概率进行分类,采用分代回收机制,从而减少扫描垃圾时间及GC频率。

JVM内存划分为堆内存和非堆内存:

  • 非堆内存就一个永久代(Permanent Generation)
    非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。在JDK1.8版本废弃了永久代,替代的是元空间(MetaSpace),元空间并不在JVM中,而是使用本地内存。

  • 堆内存分为
    堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。
    1、年轻代(Young Generation)
    年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1。From和To是主要为了解决内存碎片化。
    好处:使用对象最多和效率最高的就是在Young Generation中,通过From to就避免过于频繁的产生FullGC(Old Generation满了一般都会产生FullGC)

2、老年代(Old Generation)
如果OldGeneration满了就会产生FullGC,老年代满的原因总结:
(1)from survive中对象的生命周期到一定阈值;
(2)分配的对象直接是大对象;
(3)由于To 空间不够,进行GC直接把对象拷贝到老年代(老年代GC时候采用不同的算法)

新生代GC过程:
虚拟机在进行MinorGC(新生代的GC)的时候,会判断要进入OldGeneration区域对象的大小,是否大于Old Generation剩余空间大小,如果大于就会发生Full GC。

刚分配对象在Eden中,如果空间不足尝试进行GC,回收空间,如果进行了MinorGC空间依旧不够就放入Old Generation,如果OldGeneration空间还不够就OOM了。
比较大的对象,数组等,大于某值(可配置)就直接分配到老年代,(避免频繁内存拷贝)

3. Minor GC和Full GC区别
  • Minor GC:发生在新生代垃圾收集动作,该动作比较频繁。
  • Full GC/Major GC:发生在老年代垃圾收集动作,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。
4. 空间分配担保

在发生Minor GC前:老年代最大可用连续空间是否大于新生代所有对象总空间?大于则Minor GC安全:小于则虚拟机查看HandlePromotionFailure设置允许担保失败?不允许则Full GC:允许则检查老年代最大可用连续空间是否大于历次晋升到老年代对象平均大小?大于则尝试进行一次Minor GC:小于则Full GC;

3、垃圾收集器

垃圾收集器

JAVA中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。

1、JVM中新生代垃圾收集器:Serial收集器、ParNew收集器、Parallel收集器解析
新生代收集器 线程 算法 优点 缺点
Parallel Scavenge 多线程(并行) 复制算法 吞吐量优先适用在后台运行不需要太多交互的任务有GC自适应的调节策略开关 无法与CMS收集器配合使用
ParNew 多线程(并行) 复制算法 响应优先、适用在多CPU环境Server模式、一般采用ParNew和CMS组合、多CPU和多Core的环境中高效 Stop The World
Serial收集器 单线程(串行) 复制算法 响应优先、适用在单CPU环境Client模式下的默认的新生代收集器、无线程交互的开销,简单而高效(与其他收集器的单线程相比) Stop The World
老年代收集器 线程 算法 优点 缺点
Serial Old收集器 单线程(串行) “标记-整理”(Mark-Compact)算法 响应优先、单CPU环境下Client模式,CMS的后备预案。无线程交互的开销,简单而高效(与其他收集器的单线程相比) Stop The World
Parallel Old收集器 多线程(并行) 标记-整理 响应优先、吞吐量优先、适用在后台运行不需要太多交互的任务、有GC自适应的调节策略开关
CMS收集器 多线程(并发) 标记-清除 响应优先、集中在互联网站或B/S系统服务、端上的应用。并发收集、低停顿。 1、对CPU资源非常敏感:收集会占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低;2、无法处理浮动垃圾;3、清理阶段新垃圾只能下次回收;4、标记-清除算法导致的空间碎片
新/老年代收集器 线程 算法 优点
G1 多线程(并发) 标记-整理+复制 1、面向服务端应用的垃圾收集器;2、分代回收;3、可预测的停顿 这是G1相对CMS的一大优势;4、内存布局变化:将整个Java堆划分为多个大小相等的独立区域(Region);5、避免全堆扫描

垃圾回收分成四个阶段:
1、CMS-initial-mark初始标记阶段会stop the world,短暂的暂停程序根据跟对象标记的对象所连接的对象是否可达来标记出哪些可到达
2、CMS-concurrent-mark并发标记,根据上一次标记的结果确定哪些不可到达,线程并发或交替之行,基本不会出现程序暂停。
3、CMS-remark再次标记,会出现程序暂停,所有内存那一时刻静止,确保被全部标记,有可能第二阶段之前有可能被标记为垃圾的对象有可能被引用,在此标记确认。
4、CMS-concurrent-sweep并发清理垃圾,把标记的垃圾清理掉了,没有压缩,有可能产生内存碎片,不连续的内存块,这时候就不能更好的使用内存,可以通过一个参数配置,根据内存的情况执行压缩。

参考:JVM架构和GC垃圾回收机制(JVM面试不用愁)

五、JVM怎么判断对象已死

1、引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能被再使用的。主流的JVM里面没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象间的互循环引用的问题。

2、可达性分析法

通过一些列的称为“GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时(就是从GC Roots 到这个对象是不可达》,则证明此对象是不可用的。所以它们会被判定为可回收对象。
在Java语言中,可以作为GC Roots 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象:

总结就是:方法运行时,方法中引用的对象;类的静态变量引用的对象;类中常量引用的对象;Native方法中引用的对象。

可达性分析法中,一个对象真正宣布死亡经历两个过程:
1、对象可达性分析法后发现无GC Roots相连接引用链;则标记并第一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize(方法已经被虛拟机调用过,虚拟机将这两种情況都视为“没有必要执行”
2、如果这个对象被判定为有必要执行finalize方法,那么这个对象将会放置在一个叫做F-Queue队列之中,并在稍后由一个由虛拟机自动建立的、低优先级的
Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍候GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中成功拯救自己只要重新气引用链上的任何一个对象建立关联即可,譬如把自己this关键字赋值给某个类变量或者对象的成员变量,那在第二次标记时它将会被移除出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的被回收了。

3、对象存活与引用的关系

Java对引用的概念进行了扩充,将引用分为强引用 (Strong Reference)、软引用(Soft Reference)、弱引用 ( Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐減弱。

  • 强引用:就是指在程序代码之中普遍存在的,类似"Object obi =new Objecto”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
  • 软引用:用来描述一些还有用但并非必须的对象。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。
  • 弱引用:用户描述非必须对象的。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对系。
  • 虛引用:一个对象是否有虚引用存在,完全不会对其生存时间构成影响,也无法通过虛引用来取得一个对象实例。为一个对象设置虛引用的唯一目的就是能在这个对象被收集器回收时刻得到一个系统通知。

你可能感兴趣的:(JAVA虚拟机)