Java

Java虚拟机(二)

2021-03-12  本文已影响0人  涛涛123759

Android知识总结

一、类加载机制

(一)、一个类生命周期

类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备和解析这是三个部分统称为连接(linking)。

(二)、一个类载入过程

通过上面的内容我们知道,一个类的加载过程被分为5个阶段:加载连接初始化。其中连接分为三个步骤:验证准备解析。如下图

1)通过类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的方位入口

第一步主要是获取一个类的二进制字节流,意思就是把类以流的形式加载进内存,类的来源没有说,可以是jar包,也可以是class文件或者是apk文件。这个特性是能够实现插件化技术的理论基础。

第二步就是在获取到这个字节流以后,虚拟机就会把类中的静态存储结果保存到方法区中,保存的过程会转化对应方法区中的数据结构,所以说静态的结构都保存在内存中的方法区中。

第三步是当类加载进内存以后,每个类都会生成一个对应的Class对象,当我们使用这个类的时候,都是通过此Class对象为入口来使用的,比如我们写程序的时候通过 new 关键字创建一个类的对象的时候,也是通过这个类的Class对象来创建的。

<clinit>与<init>的区别

  • 这两个方法一个是虚拟机在装载一个类初始化的时候调用——<clinit>。另一个是在类实例化的时候调用的——<init>。

JVM的大致物理结构图

二、垃圾收集算法

(一)、复制算法

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这种算法适用于对象存活率低的场景,比如新生代。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

(二)、Appel式的复制回收算法

一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1和 Survivor2),按照8:1:1的关系分配。

专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。

HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有 10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

(三)、标记清除算法

标记-清楚算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象进行标记,玩标记完毕后,再扫描整个空间未被标记的对象进行回收

(四)、标记整理算法

标记整理算法的标记过程类似标记清楚算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代),其原来如下:

(四)、分代收集算法

对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对昂也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况是不一样的),故而不同声明周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高JVM的执行效率。当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就采用标记清楚算法或者标记整理算法。Java堆内存一般可以分为新生代老年代永久带三个模块。栈中分配新生代占1/3,老年代占2/3.

三、判断对象的存活

在堆里面存放着几乎所有的对象实例,垃圾回收器在对对进行回收前,要做的事情就是确定这些对象中哪些还是“存活”着,哪些已经“死去”(死去代表着不可能再被任何途径使用得对象了)。

(1)、引用计数算法:

判断对象的引用数量是否为0:
  Python在用,但主流虚拟机(Hotspot)没有使用,因为存在对象相互引用的情况,这个时候需要引入额外的机制来处理(补偿算法),这样做影响效率。
  每个对象实例都有一个引用计数器,被引用则+1,完成引用则-1;任何引用计数为0的对象实例可以被当作垃圾收集。

(2)、可达性分析算法:

该方法的基本思想是通过一系列的GC Roots对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

作为GC Roots的对象包括下面几种:(通常来说我们只要知道虚拟机栈和静态引用就够了)

Class要被回收,条件比较苛刻,必须同时满足以下的条件

  • 1、该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
  • 2、加载该类的classloader已经被回收
  • 3、该类的java.lang.Class对象没有在任何地方被引用,也就是说无法通过反射再带访问该类的信息

四、Finalize方法

即使通过可达性分析判断不可达的对象,也不是非死不可,它还会处于缓刑阶段,真正要宣告一个对象死亡,需要经过两次标记过程,一次是没有找到与GCRoots的引用链,它将被第一次标记。随后进行一次筛选(如果对象覆盖了finalize),我们可以在finalize中去拯救。

public class FinalizeGC {
    public static FinalizeGC instance = null;
    public void isAlive(){
        System.out.println("I am still alive!");
    }
    @Override
    protected void finalize() throws Throwable{
        super.finalize();
        System.out.println("finalize method executed");
        FinalizeGC.instance = this;
    }
    public static void main(String[] args) throws Throwable {
        instance = new FinalizeGC();
        //对象进行第1次GC
        instance =null;
        System.gc();
        Thread.sleep(1000);//Finalizer方法优先级很低,需要等待
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
        //对象进行第2次GC
        instance =null;
        System.gc();
        Thread.sleep(1000);
        if(instance !=null){
            instance.isAlive();
        }else{
            System.out.println("I am dead!");
        }
    }
}

运行结果:



可以看到,对象可以被拯救一次(finalize执行第一次,但是不会执行第二次)。

注意:finalize方法执行缓慢,当还没有挽救,还没有挽救垃圾回收就回收掉了。所以建议大家尽量不要使用finalize,因为这个方法太不可靠。在生产中你很难控制方法的执行或者对象的调用顺序,因为在finalize方法能做的工作,java中有更好的,比如try-finally或者其他方式可以做得更好

五、对象的分配策略

对象的分配原则

虚拟机参数:
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:+PrintGCDetails 打印垃圾回收日志,程序退出时输出当前内存的分配情况

注意:新生代初始时就有大小

参数设置:
-Xms20m
-Xmx20m
-Xmn10m
-XX:+PrintGCDetails
-XX:PretenureSizeThreshold=4m
-XX:+UseSerialGC
PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效。

栈中分配对象

堆中的优化技术

六、常量池和String

1)、常量池

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

常量池有很多概念,包括运行时常量池class常量池字符串常量池。虚拟机规范只规定以上区域属于方法区,并没有规定虚拟机厂商的实现。

严格来说是静态常量池运行时常量池

  • 静态常量池是存放字符串字面量、符号引用以及类和方法的信息。(即:class 里面的一些信息。)

  • 运行时常量池存放的是运行时一些直接引用。(即:就是类加载是放到运行时数据区的方法区,把符号引用变成直接引用。)

运行时常量池是在类加载完成之后,将静态常量池中的符号引用值转存到运行时常量池中,类在解析之后,将符号引用替换成直接引用。

这两个常量池在JDK1.7版本之后,就移到堆内存中了,这里指的是物理空间,而逻辑上还是属于方法区(方法区是逻辑分区)。

在 JDK1.8 中,使用元空间代替永久代来实现方法区,但是方法区并没有改变,所谓"Your father will always be your father"。变动的只是方法区中内容的物理存放位置,但是运行时常量池和字符串常量池被移动到了堆中。但是不论它们物理上如何存放,逻辑上还是属于方法区的。

字面量:
给基本类型变量赋值的方式就叫做字面量或者字面值
比如:String a=“b” ,这里“b”就是字符串字面量,同样类推还有整数字面值、浮点类型字面量、字符字面量。

符号引用:
符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,JAVA 在编译的时候一个每个 java 类都会被编译成一个 class文件,但在编译的时候虚拟机并不知道所引用类的地址(实际地址),就用符号引用来代替,而在类的解析阶段(后续 JVM 类加载会具体讲到)就是为了把这个符号引用转化成为真正的地址的阶段。
包括类和方法的全限定名(例如 String 这个类,它的全限定名就是 Java/lang/String)、字段的名称和描述符以及方法的名称和描述符。
一个 java 类(假设为 People 类)被编译成一个 class 文件时,如果 People 类引用了 Tool 类,但是在编译时 People 类并不知道引用类的实际内存地址,因此只能使用符号引用(org.simple.Tool)来代替。而在类装载器装载 People 类时,此时可以通过虚拟机获取 Tool 类的实际内存地址,因此便可以既将符号 org.simple.Tool 替换为 Tool 类的实际内存地址。

直接引用:
具体对象的索引值。

2)、String 对象是如何实现的

String 对象是对 char 数组进行了封装实现的对象,主要有 2 个成员变量:char 数组,hash 值。


了解了 String 对象的实现后,你有没有发现在实现代码中 String 类被 final 关键字修饰了,而且变量 char 数组也被 final 修饰了。我们知道类被 final 修饰代表该类不可继承,而 char[]被 final+private 修饰,代表了 String 对象不可被更改。Java 实现的这个特性叫作 String 对象的不可变性,即 String 对象一旦创建成功,就不能再对它进行改变。

在 Java 中,通常有两种创建字符串对象的方式

这种方式首先会检查该对象是否在字符串常量池中,如果在,就返回该对象引用,否则新的字符串将在常量池中被创建。这种方式可以减少同一个值的字符串对象的重复创建,节约内存。


这种方式,首先在编译类文件时,"abc"常量字符串将会放入到常量结构中,在类加载时,“abc"将会在常量池中创建;其次,在调用 new 时,JVM 命令将会调用 String 的构造函数,同时引用常量池中的"abc” 字符串,在堆内存中创建一个 String 对象;最后,str 将引用 String 对象。

使用 new,对象会创建在堆中,同时赋值的话,会在常量池中创建一个字符串对象,同时这个堆中对象的成员变量会引用了常量池中的字符串对象。

public class Location {
private String city;
private String region;
}
String str= "abcdef";

String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用。


  • 1、new Sting() 会在堆内存中创建一个 a 的 String 对象,king"将会在常量池中创建
  • 2、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
  • 3、调用 new Sting() 会在堆内存中创建一个 b 的 String 对象。
  • 4、在调用 intern 方法之后,会去常量池中查找是否有等于该字符串对象的引用,有就返回引用。
    所以 a 和 b 引用的是同一个对象
上一篇 下一篇

猜你喜欢

热点阅读