深入学习jvm,这一篇就够了(图解+面试题)

4 篇文章 0 订阅
2 篇文章 0 订阅

jvm完全深入理解,一文即可

文章目录

类加载子系统

类加载过程

image-20210719203102281

加载
  • 通过一个类的全限定名获取定义此类的二进制文件流、

  • 将这个类字节流所代表的静态存储结构转换为方法去的运行时数据结构

  • 在内存中的堆区生成一个代表这个类的Class对象,作为方法区中这个类的数据结构的访问入口

区别两个Class是不是同一个对象

首先两个类是不是在同一个包下,是否同名

两个类的加载器是否相同

加载的方式

  • 在本地系统中直接加载
  • 通过网络获取
  • 从zip压缩包中读取
  • 运行时计算生成,通过动态代理实现
  • 由其他文件生成
  • 从专有数据库中提取.class文件
  • 从加密文件中获取
链接

验证

确保Class文件的字节流中包含的信息符合虚拟机要求,保证加载类的正确性,不会威胁虚拟机的自身安全。

主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

为类变量分配内存并且设置该类型量的默认初始值,即零值或者是null值(final修饰的static变量不会分配,因为final变量在编译的时候就分配了,准备阶段会显式初始化;也不会为实例变量分配初始化,类变量会分配到方法区中,而实例变量会随着对象一起分配到java堆中

解析

将常量池内的符号引用转换为直接引用的过程

伴随着jvm执行完初始化之后再执行

初始化

对类的静态变量,静态代码块执行初始化操作

初始化过程就是通过执行javac编译器自动收集类中的所有类变量和赋值动作和静态代码块中的语句生成一个执行类构造器方法的过程。

虚拟机必须保证执行类构造器方法只会执行一次,在多线程环境下,被同步加锁。

类什么时候才被初始化:

1)创建类的实例,也就是new一个对象

2)访问某个类或接口的静态变量,或者对该静态变量赋值

3)调用类的静态方法

4)反射(Class.forName(“com.lyj.load”))

5)初始化一个类的子类(会首先初始化子类的父类)

6)JVM启动时标明的启动类,即文件名和类名相同的那个类 只有这6中情况才会导致类的类的初始化。

类加载器的分类

引导类加载器(bootstrap ClassLoader):c ,c++ 实现

获取不到。不由java编写,嵌套在jvm内部;但是可以找到能供加载哪些api路径:sun,misc.Launcher.getBootstrapClassPath().getURLS();

加载扩展类和应用程序类加载器,并制定为他们的父类加载器

只加载包名为java、javax、sun开头的目录下的类

自定义类加载器: java实现

凡是直接或者间接继承了ClassLoader的类都叫做自定义类加载器

自定义的类默认使用的是系统类加载器进行加载;系统的核心类库都是由引导类加载器进行加载的。

扩展类加载器

派生于classloader类,父类加载器为启动类加载器

从jdk的安装目录的jre/lib/ext子目录或者从java.ext.dirs系统属性所制定的目录中加载类库,如果用户创建的jar包放在此目录下,也会自动由扩展类加载器进行加载

系统类加载器

派生于classloader,父类为扩展类加载器

负责加载环境变量classpath或者系统属性 java.class.path制定路径下的类库

该类加载器是程序中默认的类加载器,一般来说,java应用的类都是由他来加载完成的

用户自定义类加载器

需要自定义类加载器的情况有哪些?
  • 隔离加载器
  • 修改类加载方式
  • 扩展加载源
  • 防止源码泄露
自定义加载器的步骤?
  • 继承抽象类classloader

  • 重写findClass()方法,将自定义的逻辑写在这个方法中

获取类加载器的途径

1.通过反射获取

Class.forName("java.lang.String").getClassLoader();

2.通过线程获取

Thread.currentThread().getContextClassLoader();

3.获取系统加载器

ClassLoader.getSystemClassLoader().getParent();

双亲委派机制

双亲委派机制有什么作用?

如果一个类加载器收到加载请求,他不会自己去加载,而是将它向上交给他的父类加载器去加载,如果父类还存在父类就再向上传递,直到交给启动类加载器。如果父类加载器可以完成加载,就成功返回,否则就子类才会尝试去加载。

可以防止核心类被篡改,也可以避免重复加载

执行流程:

1.当系统类加载器 收到一个类加载请求时,他首先不会自己去尝试加载这个类,而是将这个请求委派给父类加载器扩展类加载器去完成。

2.当扩展类加载器收到一个类加载请求时,他首先也不会自己去尝试加载这个类,而是将请求委派给父类加载器启动类加载器去完成。

3.如果启动类加载器加载失败(在<JAVA_HOME>\lib中未找到所需类),就会让扩展类加载器尝试加载。

4.如果扩展类加载器也加载失败,就会使用系统类加载器加载。

5.如果系统类加载器也加载失败,就会使用自定义加载器去尝试加载。

image-20210719200632819

image-20210719201627099

运行时数据区

描述了程序运行的实时状态;虚拟机栈,本地方法栈,和程序计数器是每一个线程私有。堆和方法区是线程共享。

image-20210724190744432

程序计数器

是当前线程所执行的字节码的行号指示器,是用来存储执行下一条指令的地址,也就是将要执行的指令代码。由执行引擎读取下一条指令。

image-20210720154530205

为什么需要程序计数器?

CPU进行线程切换或者被其他工作打断之后再次回到线程执行的时候,需要知道上一次执行到哪个位置,而程序计数器就记录了程序执行的位置,所以可以快速的定位到上一次执行中断的位置,继续执行。

为什么每个线程都需要一份私有的程序计数器?

一个进程中会有多个线程,并且各个线程之间可以进行频繁的切换,如果我们只用一个程序计数器可能会导致不同线程之间执行的字节码地址相互覆盖相互干扰,所以最好的解决办法就是么个线程都拥有一份程序计数器,各自急速正在执行的字节码地址。

虚拟机栈

为什么会出现虚拟机栈?

java架构拥有跨平台性,指令集小,编译器易于实现的优点,不同平台CPU的架构是不相同的,所以为了实现跨平台设计,java的指令流是根据栈的结构来设计的,将每一个方法放入栈中,然后从栈顶 开始执行,执行完就出栈。但是这样的架构设计也存在一些缺陷,那就是性能相对于基于寄存器的设计来说比较慢,实现相同的功能所需要的指令更多等。

什么是java虚拟机栈?优点?

栈是运行时的单位,它是线程私有的,生命周期和线程一致;保存了方法的局部变量(包括8中基本数据类型,对象的引用地址),部分结果,参与方法的调用返回。

作为一种分配存储方式,访问速度仅此于程序计数器,只有进栈出栈两个过程,不存在垃圾回收问题,但是存在溢出异常和内存不足异常的问题。

既然会内存空间不足异常,那么如何设置栈大小来改变某些特定环境下的要求?

2021版本之前,run ==》edit Configurations

image-20210720163402834

2021版本idea,help ==> custom VM options

image-20210720163635377

栈的存储单位

image-20210720164606846

栈的基本存储单位是栈帧。

不同线程中所包含的栈帧是不允许存在相互引用的,栈是线程私有的,相互隔离的。

方法结束的两种方式:

  • 正常返回结束
  • 出现未捕获异常,以抛出异常的方式结束异常
栈帧内部结构

image-20210724190857211

  • 局部变量表

    • 就是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。
    • 所需容量在编译期确定,所以运行期间不会改变其大小。
    • 因为是线程私有,所以不会存在线程安全问题。
    • 局部变量表的基本存储单元是slot,每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
    • 32位以内包括32位的类型占一个slot(引用类型),64位的占2个slot(long,double),当访问两个slot的局部变量的时候,访问前一个slot的索引即可
    • byte,short,char在存储前都被转换为int,Boolean也会转换成int。
    • 如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this会存放在index为0的slot处,其余参数依次排列。
    • slot可以重复利用,当一个局部变量出了他的作用域范围就会被销毁,另外的局部变量就会可以占据他的位置,从而实现重复利用。(一个对象的作用域在代码块中,那么出了这个代码块,接下来的变量就会占据他的位置,如果后面没有占据,那么该位置就不会被占用,如果该位置创建了对象,那么出了代码块也不会被回收,只有接下来有变量占据了该位置才会对该位置原来创建的对象进行回收)
  • 操作数栈(表达式栈)

    • 保存计算过程的中间结果,同时作为计算过程变量的临时存储空间。
    • 在方法执行过程中,根据字节码指令,可能会往栈中写入数据或者是提取数据。
    • 32位数据类型占1个栈空间,64位占2个栈空间。
    • 如果被调用的方法带有返回值,其返回值也会被压入当前栈帧的操作数栈中。
  • 动态链接(执行运行时常量池的方法引用)

    • 每个栈帧内部都有一个指向运行时常量池中该栈帧所属方法的引用。
    • 将这些符号引用转换为调用方法的直接引用。
    • 常量池的作用就是提供一些符号和常量,便于指令识别。
    • 早期绑定
      • 被调用的方法在编译器可知,且运行期不变时,即可将这个方法所属类型进行绑定,明确了被调用的目标方法是哪一个,因此可以使用静态链接的方式将符号引用转换为直接引用。
    • 晚期绑定
      • 编译器无法确定,只能在运行期间根据实际类型绑定相关的方法。
        • 举例:多态问题。当我们调用类型为向上转型后的类型中的方法时,因为在编译器无法预知具体调用的方法,只有在运行期根据实际情况来确定。
    class Animal {
    
        public void eat(){
            System.out.println("动物在吃");;
        }
    }
    
    class Cat extends Animal {
        public void eat(){
            System.out.println("猫吃鱼");
            super.eat();
        }
    
    }
    class Dog extends Animal {
        public void eat(){
            System.out.println("狗吃骨头");
        }
    
    }
    public class testAnimal(Animal animal) {
        animal.eat() //晚期绑定
    }
    
    • 非虚方法
      • 私有方法,静态方法,实例构造方法,父类中的方法(必须显式的用super来调用才算),final修饰的方法 (invokestatic调用静态方法,invokespecial调用,私有,父类方法),编译器就确定。
    • 虚方法
      • 在编译器不确定的方法。(invokevirtual:调用所有虚方法;invokeinterface:调用所有接口方法)

    java7中新增invokedynamic动态调用指令,需要动态解析出需要调用的方法,java8中的stream流就是最好的体现。

  • 方法返回地址

    • 存放调用该方法的pc寄存器的值。
    • 将pc寄存器的值返回交给执行引擎。
    • 返回指令包括ireturn(boolean,byte,char,short,int类型时使用)、lreturn、lreturn、freturn 、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法。
  • 附加信息

虚拟机相关面试题:
举例栈溢出的情况?(StackOverflowError)

当一个栈空间加载栈帧,由于栈帧过多,就会出现栈溢出的情况,可以通过-Xss设栈的大小,就是扩容;当整体空间不足就会出现内存溢出问题。

调整栈大小,就能保证不出现溢出吗?

不能。因为内存空间是有限的,而程序执行可以无限循环,当达到一定的限度,就一定会沾满栈空间。只能让溢出出现的时候推迟,而不能避免。

垃圾回收是否会设计到虚拟机栈?

不会。前面就已经讲过,虚拟机栈存在error但是不存在GC(垃圾回收);

方法中定义的局部变量是否线程安全

前面提到过,说因为局部变量是线程私有的,所以不会存在线程安全问题。但是,这不是绝对的,大多数情况是这样的,我们需要知道哪些情况会发生线程不安全问题,这也是面试官想考的点。当局部变量在一个方法中被创建,然后在方法中被销毁,那么这个局部变量就是线程安全的 ;如果一个变量在方法中被创建,但是作为返回值返回出该方法,那么就有可能有其他的线程会对这个返回值进行操作,就可能存在线程安全问题。(提醒一下:方法内部需要保持原子性才能实现同步安全,可以通过锁实现)

本地方法接口,本地方法库

什么是本地方法?

被native关键字修饰的方法,本地方法只被定义,具体实现不是由java语言实现,使得其他语言的代码也能为java所用。

作用

就是通过java调用其他语言的代码实现一些java实现起来相对较为困难的需求。实际运用得少,了解即可。

本地方法栈

什么是本地方法栈

对比java虚拟机栈,java虚拟机栈是用来管理java方法的调用的,而本地方法栈自然就是用于管理本地方法的调用的。他同样是线程私有的。其他信息也可虚拟机栈大同小异。

image-20210724190744432

Java堆是java虚拟机所管理内存中最大的一块内存空间,处于物理上不连续的内存空间,只要逻辑连续即可,主要用于存放各种类的实例对象。该区域被所有线程共享,在虚拟机启动时创建,用来存放对象的实例,几乎所有的对象以及数组都在这里分配内存(栈上分配、标量替换优化技术的例外)。

jvm内存的逻辑细分

image-20210723122604859

  • JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation)、老年代(Old Generation),非堆内存就一个永久代(Permanent Generation)【注:jdk1.8中称为元空间】。

  • 年轻代又分为Eden和Survivor区。Survivor区由FromSpace和ToSpace组成。Eden区占大容量,Survivor两个区占小容量,默认比例是8:1:1(实际情况中,我们计算得到的比例并不是8:1:1,要想变成8:1:1,使用显式指定:-XX:SurvivorRtio=8)。

  • 堆内存用途:存放的是对象,垃圾收集器就是收集这些对象,然后根据GC算法回收。

  • 非堆内存用途:永久代,也称为方法区,存储程序运行时长期存活的对象,比如类的元数据、方法、常量、属性等。

分代的标准

将对象根据存活概率进行分类,对存活时间长的对象,放到固定区,从而减少扫描垃圾时间及GC频率。针对分类进行不同的垃圾回收算法,对算法扬长避短。

堆空间大小设置

堆空间大小在jvm启动时就已经设定好了。

-Xms表示堆区起始内存,等价于-XX:InitialHeapSize. 默认默认电脑内存大小/64。

-Xmx表示堆区的最大内存,等价于-XX:MaxHeapSize. 默认物理电脑内存大小/4。

-NewRatio:设置新生代老年代的比例。默认是1/2。

-xmn: 设置新生代的空间大小。

image-20210723124503922

绝大部分的java对象都是在堆区中的Eden区被创建出来的,而且大部分的对象也都是在这个区被销毁。

内存分配策略

新生成的对象首先放到Eden区,当Eden空间满了,就会触发YGC,存活下来的对象移动到from(Survivor0)区,Eden区满后再次触发执行Minor GC,from(Survivor0)区存活对象也会移动到 to (Suvivor1)区,这样保证了一段时间内总有一个survivor区为空。经过多次Minor GC达到晋升的阈值的时候,仍然存活的对象就会被放入到老年代。
老年代存储长期存活的对象,占满时会触发Major GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Major GC,避免响应超时。

补充:因为Eden:s0:s1=8:1:1,当s0或者s1满的时候,并不会触发ygc,只有Eden满了才会触发ygc;当Eden满了之后会触发YGC,这时会连同s区一起进行回收。如果出现s区满了或者相同年龄的对象大小之和大于空间的一半就会特殊处理,直接放入老年代;当出现一个大对象在新生代放不下,也会直接放入老年代,老年代空间不够则进行major GC

区别Minor GC、 Major GC 、Full GC、Mixed GC

Minor GC:仅仅是指新生代收集(Eden,s0,s1)。

Major GC:指老年代收集。

Mixed GC:混合收集,收集整个新生代和部分老年代。

Full GC:收集整个java堆和方法区的垃圾收集。

面试题:堆空间一定是所有线程共享的嘛?

不是,在新生区的伊甸园区 存在一个TLAB,这个空间是每个线程自己私有的,约为伊甸园区的1%,这各区域主要是用来存放每个线程私有的一些对象,当这个区域放不小才会放入Eden共享的区域。

代码优化
栈上分配
面试题:堆是唯一分配对象存储的选择吗?

如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。什么是逃逸分析呢?当一个对象在方法内部定义,对象只在方法内部使用,那么就叫没有发生逃逸,就可以优 化为栈上分配;如果对象被外部方法引用(作为调用参数传递到其他地方),就认为发生逃逸。

同步省略

通过他逃逸分析判断同步块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程,那么就可以取消对这段代码的同步,提高并发性。也就是说,当一个对象只能被一个线程访问,那么就没必要加锁。

public void test() {
   Test t = new Test();
   synchronized (t) {
       System.out.println(t);
   }
}

例如,上面的代码,即使是高并发的情况下,t也不会被两个线程使用,因为每个线程进入这个方法都会新建一个t,所以就没必要对这个对象进行加锁。

分离对象或标量替换

有的对象可能不需要作为一个连续内存结构存在也可以访问到,那么对象的部分或者全部可以不用存储在内存,而是存储在CPU的寄存器(栈)中。

通过逃逸分析,发现如果对象没有逃逸,那么就可以将对象进行标量替换,分解成若干个标量,就想当于转换成了多个局部变量存放在局部变量表中。

方法区(元空间)

image-20210724190242814

在某些场景下,为永久代设置空间大小是很难确定的,如果动态加载类过多,容易产生Perm区的oom。空间过小,会造成full GC,使得用户进程受阻,拖慢程序性能。过大又会造成大量浪费。所以将类的元数据信息移到了一个与堆不相连的本地内存区域,这个区域就叫做元空间。由于类的元数据信息被分配到了本地内存中,元空间能分配的最大内存就是我们的本地内存。至此,永久代永久退出历史舞台,元空间由此诞生。

方法区中存储了被虚拟机加载的类型信息,常量,即时编译器编译后的代码缓存等。

在jdk1.6之前,静态变量的引用,字符串常量池都是存放在永久代上的。

设置元空间大小(jdk8)

-XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m

运行时常量池

Java中的常量池,分为两种:静态常量池运行时常量池

静态常量池,就是class文件中的常量池,class文件中的常量池既包含字符串(数字)字面量,也包含类、方法的信息,占用class文件绝大部分空间。这种常量池主要用于存放两大类常量:字面量符号引用量,字面量相当于Java语言层面常量的概念,如文本字符串,声明为final的常量值等,符号引用则属于编译原理方面的概念,包括了如下三种类型的常量:

  • 类和接口的全限定名
  • 字段名称和描述符
  • 方法名称和描述符

运行时常量池则是jvm虚拟机在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中,我们常说的常量池,就是指方法区中的运行时常量池。

运行时常量池相对于CLass文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入CLass文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用比较多的就是**String类的intern()**方法。

String的intern()方法会查找在常量池中是否存在一份equal相等的字符串,如果有则返回该字符串的引用,如果没有则添加自己的字符串进入常量池。

常量池的好处

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
例如字符串常量池,在编译阶段就把所有的字符串文字放到一个常量池中。
(1)节省内存空间:常量池中所有相同的字符串常量被合并,只占用一个空间。
(2)节省运行时间:比较字符串时,比equals()快。对于两个引用变量,只用判断引用是否相等,也就可以判断实际值是否相等。

类中各种属性的加载赋值时间小总结
  • 单独用final修饰的变量也有可能在字节码找到对应的ConstantValue属性,但是会被JVM忽略掉。
  • final修饰的实例属性,在实例创建的时候才会赋值。
  • static修饰的类属性,在类加载的准备阶段赋初值,初始化阶段赋值。
  • static+final修饰的String类型或者基本类型常量,JVM规范建议在初始化阶段赋值,但是HotSpot VM直接在准备阶段就赋值了。
  • static+final修饰的其他引用类型常量,赋值步骤和第二点的流程是一样的。

image-20210724191104643

对象实例化

创建对象的方式
  • new 关键词
    • 单例模式中通过静态方法创建。
    • xxxBuilder/xxxFactory的静态方法。
  • 反射Class的newInstance():【只能调用空参构造器,而且权限必须是public】
  • 反射Constructor的newInstance(xxx):【也是反射,可以调用有参构造器,权限无要求】
  • 使用clone():不需要任何构造器,当前类需要实现Cloneable()接口,实现clone()方法。
  • 使用反序列化:将网络中或者是本地中的对象二进制流还原为一个对象。
  • 第三方库。
创建对象的步骤
  • 判断对象对应的类是否加载、连接、初始化。

    • 到元空间的运行时常量池中定位到一个类的符号引用,并检查这个类是否被加载,连接,初始化(检查类的元信息是否存在)。如果没有则在双亲委派模式下,使用加载器进行查找对应的class文件,没有找到就报classnotfoundexception,找到就进行加载。
  • 为对象分配内存

    • 内存规整—指针碰撞
    • 内存不规整—虚拟机维护一个列表;空间列表进行分配
  • 处理并发安全问题

    • 采用CAS失败重试、区域加锁保证更新的原子性。
    • 每个线程预先分配一块TLAB–通过 --XX:+/-UseTlab参数设定。
  • 初始化分配到的空间。

  • 设置对象的对象头。

    • 运行时元数据:哈希值,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID,偏向时间戳。
    • 类型指针:指向类元数据instanceklass,确定该对象所属类型。
  • 执行init方法进行初始化。

对象访问定位
对象访问的两种方式
  • 句柄访问
    • 堆区中存在一块句柄池区域,里面存放的是到对象实例数据的指针,指向堆中的对象实例,还有一个到对象类型数据的指针,指向元空间的对象类型数据。而在栈帧的局部变量表中就存放了该实例在句柄池中的位置,从而找到对象。
  • 直接指针
    • 局部变量表中直接保存了堆中对象实例数据,堆中的实例数据中包含了元空间中对象类型数据所在的位置。

执行引擎

image-20210724191947755

解释器

当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。

  • Interpreter模块:实现了解释器的核心功能
  • Code模块:用于管理HotSpot VM在运行时生成的本地机器指令
JIT编译器
  • 由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的,比如Python、 Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C+ +程序员所调侃。
  • 为了解决这个问题,JVM平台支持一种叫做即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
  • 不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。
热点探测

那么,怎样的一个调用频率才能称得上是热点代码呢?

jvm在server模式下,当方法调用达到10000次称之为热点代码,而在client模式下当方法调用达到1500次就是热点代码。64位PC默认是server模式。

image-20210726180107699

热度衰减

方法调用计数器统计的是一个方法调用的相对执行频率,如果在一定的时间内,方法的调用次数始终都达不到热点次数就会进行热度衰减操作,也就是方法计数器减少一半。

也可以使用-XX:UseCounterDecay来关闭热度衰减。-XX:CounterHalfLifeTime来设置热度衰减时间。

总结:总的来看,执行引擎中既有解释器又有及时编译器,那么怎么切换呢?

默认是混合模式,解释器和及时编译器模式混合。

编译器模式:java -Xcomp -versio

解释器模式:java -Xint -version

混合模式 :java -Xmixd -version

垃圾回收算法

什么是垃圾?为什么要回收?

在java中,当一个对象没有被任何指针指向的时候,他就成为了一个垃圾,也就应该对其进行回收,如果不对其进行回收就会造成内存泄露,甚至是内存溢出。

垃圾回收分为两个阶段垃圾标记和垃圾回收。

垃圾标记
引用计算算法(java并未使用,不存在循环引用造成内存泄露)

对每一个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。当对象被任何一个对象引用就会对计数器加以,引用失效就减一,直到计数器为0,就把该对象视为垃圾,对其进行回收。

该算法具有效率高,没有回收延迟的特性,但是他却增大了开销,每次进行加减法也增大了时间复杂度,同时,他不能处理循环引用的问题。所以实际上java并未采用这样的算法。

可达性分析算法

以根对象集合为起始点,按照从上到下的方式搜索根对象集合所连接的目标对象是否可达。内存中存活的对象都直接或者间接同根对象连接在一起。否则,就认为该对象应该被回收。

那么根元素集合包括哪些?

  • 虚拟机栈中引用的对象。
    • 例如,各个线程调用方法中使用到的参数,局部变量等。
  • 本地方法栈内引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中常量引用的对象。
    • 字符串常量池中的引用。
  • 所有被同步锁持有的对象。
  • java虚拟机内部的引用。
对象的finalization机制

一个对象在被回收之前会执行finalization()方法,来进行资源释放等操作。

虽然对象的finalization机制听起来很高大上,但是我们平时好像从来没有用过,没有用过就对了,因为这是给垃圾回收调用的,并不需要程序员调用,而且也不允许程序员调用。因为finalization可能会导致对象复活,其次该机制发生的时间是由GC线程决定的,如果不发生GC就永远不会发生这个机制,最后,如果这个机制使用不当,就会严重影响GC性能。

对象的三种状态
  • 可触及状态
    • 正常使用的对象
  • 可复活状态
    • 对象不可触及,但是在finalization中被复活
  • 不可触及状态
    • 对象不能被复活,真正要被回收之前的状态。

虚拟机在判定一个对象是否被回收的时候,需要进行两次标记,第一次,判断该对象从根对象集合开始有没有引用链,,然后判断对象是否需要执行finalization()方法,如果该对象没有重写finalization()方法,活着之前已经调用过该方法(这个方法只能被调用一次),那么就认为不需要执行,那么该对象也就达到了第三种状态。

如果该对象重写了finalization()方法,且未执行过,那么该对象就会被插入一个队列中,由虚拟机自动创建一个低优先级的finalizer线程触发其finalization()方法,进行第二次标记,如果方法中,对象突然复活,就会将其移除即将回收的集合,达到第二个复活状态。

垃圾清除
标记清除算法

将所有具有引用执行的对象进行标记,对未标记的的对象进行清除。

由于清除阶段需要对所有对象进行遍历,所以效率不高,而且清除的空间是碎片化的,需要维护一个空闲列表。

复制清除算法

对所有存活的对象不进行标记,而是直接将其复制到另外一块一模一样大小的空间中去,然后对原来空间进行整体回收。这样做的好处是没有标记和清除过程,使得清除十分高效,复制过去的内存空间是连续的,不需要维护空闲表。但是需要两倍的内存空间,而且对象的复制会到值栈中引用地址会发生变化。

标记压缩算法

复制算法建立在存活对象少,垃圾比较多的情况下。该算法就是在标记清除算法的基础上进行了碎片整理,这样就不需要维护空闲列表。但是效率比标记清除算法更低。

分代收集算法

按照不同的分区选择不同的算法。在新生区,由于对象具有朝生夕死,回收频率高的特性,所以可以采用回收效率较高的复制算法。而对于老年区,由于对象的生命周期较长,而且空间比较大,回收频率不高,我们就可以采用标记清除,标记压缩算法或者混合使用。

增量收集算法

仍是传统的标记-清除算法和复制算法,只是通过通过分阶段的方式处理线程冲突问题,允许垃圾回收线程分阶段完成清理和复制。但是频繁的进行线程切换会造成垃圾回收的总成本升高,系统的吞吐量下降。

分区算法

将堆空间划分为很多个小空间,根据目标的停顿时间,每次合理的回收若干个小区间,从而减少一次GC所产生的停顿。

内存泄露

当一个对象不被使用的时候,jvm会自动对其进行回收,但是如果出现一些意外情况导致垃圾回收器没有对其回收,这样的现象,我们就叫他内存泄露,简单来说,就是对象不能被使用了,但是还不能回收掉,长期占用一定的内存空间。那么在哪些情况下会产生内存泄露?

当存在一些复杂的指针指向,如果存在对象不使用了,于是很多指针都放开,但是某一个指针没有放开,导致这些对象中。仍然有部分指针没有断开,而造成对象不能被回收。

另外,在单例模式中,单例的生命周期和应用程序的周期是一样长的,所以单例程序中,如果持有对不对象的引用的情况,那么这个外部对象就不能被回收,就会导致内存泄露。

还有一些就是资源未关闭连接的行为,数据库连接,网络连接,io操作等,就会导致对象不会被回收(主要是一些和外部资源有连接的对象)。

引用
强引用

类似new出来的对象,只要引用还在就永远不会进行回收。

Object o = new Object();

软引用

如果出现内存不足就会对软引用对象进行回收。

SoftReferencre<User> usersoftRef = new SoftReference<User>(new User("zero",18));

弱引用

生命周期仅存在于下一次垃圾回收之前,也就是只要发生GC就一定会回收弱引用。

WeakReference<User> userweakreference = new WeakReference<User>(new User("zero",18));

虚引用(对象回收跟踪)

虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

垃圾回收器

serial回收器(串行回收)

应用场景是在单核CPU的情况下使用一条收集线程去完成清理工作,在垃圾回收期间,其他所有的工作线程都要停下来,直到垃圾回收线程完毕。在其特殊的工作场景下,他的回收效率是最高的,因为他不需要反复进行多线程的切换。通常和Serial Old GC搭配工作。

ParNew回收器(并行回收)

采用并行回收的方式,其他跟serial回收器十分类似。主要运用在多CPU的环境下。可以和CMS GC以及 Serial Old GC搭配工作。

Parallel Scavenge回收器(吞吐量优先)

一个采用了复制算法,并行回收和stw机制的垃圾回收器。由此可知,该回收器的目标是注重吞吐量,

他和采用了标记-压缩算法、并行回收、stw机制的Parallel Old 收集器搭配, 效果非常好。(是jdk8的默认垃圾回收器)

cms回收器(并发收集器)

第一款能让垃圾回收线程和用户线程同时进行工作的回收器。他的关注点是尽可能缩短垃圾回收时用户线程的停顿时间,提高响应速度。

采用标记-清除算法,所以在垃圾回收之后会产生一些碎片空间,只能引用空闲列表的方式来执行后续的内存分配。

对CPU的资源也很敏感,在并发阶段因为会占用一部分线程,从而使程序变慢,总吞吐量也会降低。

无法处理浮动垃圾。在第二阶段的并发标记过程完成后,因为用户线程的某些操作使得某些之前不是垃圾的对象变成了垃圾,但是第三阶段的重写标记只是进行对之前怀疑是垃圾的对象确认一下是否是真的垃圾,不会标记新产生的这部分浮动垃圾。也就不会回收这部分垃圾。

流程:

  • 初始标记

    • 所有的工作线程都会出现短暂的stw,这个阶段的工作目的是标记所有GC Root能直接关联的对象,一旦标记完成就会恢复所有暂停线程,由于关联的对象比较小,所以速度非常快
  • 并发标记

    • 从GC root直接关联的对象开始遍历整个关联对象图的过程,这个过程耗时较长,但是不需要暂停用户线程,可以与垃圾收集并发运行。
  • 重新标记

    • 修正标记期间,因为用户进程继续运行导致标记变动的那部分对象的标记记录,这个阶段的停顿时间会比初始阶段长,但远比并发标记阶段时间短。
  • 并发清除

    • 清理删除标记阶段判断的已经不使用的对象,释放内存空间。这个阶段不需要移动存活的对象,所以也是可以和用户线程并发执行的。
G1垃圾回收器(区域分代化)

一个并行回收器,将堆内存划分成多个不想关的区域(物理上不连续),不同的区域表示不同的堆分区(Eden,s0,s1,old),这样做的目的是避免在堆中做全区域的回收,通过跟踪每个区域的回收价值(回收获得的空间大小以及所需时间比例)维护一个优先级列表,根据允许回收的时间,选择回收价值大的区域进行回收。

特点:

  • 并行性
    • 在回收期间可有多个GC线程同时工作,利用多核的特性,减少用户线程的stw。
  • 并发性
    • G1拥有于用户线程交替执行的能力,所以整个回收阶段发生不会完全阻塞程序的执行。
  • 分代收集
    • 将整个堆空间分为若干个区域,每个区域都代表堆空间的原始分区的一部分,而且他的回收不像其他回收器一样,他是兼顾新生区和老年区的,
  • 空间整合
    • 区域之间采用的是复制算法,而整体是标记压缩算法。在进行垃圾回收之后,不会有碎片化的空间,避免了后续空间分配带来的许多问题。
  • 可预测的停顿时间模型
    • G1除了追求低停顿之外,还建立可预测的停顿时间模型,让使用者在M的时间片段内,让垃圾回收的时间不超过n。

回收环节:

  • 年轻代GC
    • 当Eden内存空间不足的时候,就会进行YGC,这个时候他是一个并行的独占式的收集器,所有的用户线程都会停下来,将Eden中的存活对象放到s区,大对象直接放到老年区。
  • 老年代并发标记
    • 和cms 的清理过程类似
  • 混合回收
    • 会同时回收新生区和老年区的垃圾。
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值