当前位置:   article > 正文

Jvm面试题

jvm面试题

类加载

有三种类加载器

  1. 引导类加载器:BootstrapClassLoader,启动类加载器负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的类。
  2. 扩展类加载器:ExtensionClassLoader,扩展类加载器负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。
  3. 应用类加载器:ApplicationClassLoader,应用类加载器负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器。

类加载器的双亲委派机制

在这里插入图片描述

双亲委派机制是当一个类收到类加载请求后,它首先不会自己尝试加载这个类,而是把这个请求交给父类,每一层加载类都是这样,会一直从自定义类加载器往上找,只有当父类加载器反馈自己无法完成的时候,子类加载器才会去加载。

例如:因为java自带String类,如果自己写了个父类的话,类加载器会找到java自带的String类,为了保证User编写的代码不污染java自带的代码,因此就有了双亲委派机制,保证了安全,确保使用不同的类加载器最终得到的都是同一个object对象。

为什么需要双亲委派

防止多份同样字节码的加载

Jvm类加载机制的流程

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

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用、卸载七个阶段。类加载机制的保持则包括前面五个阶段。

  1. 加载

    加载是指将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

  2. 验证

    验证的作用是确保被加载的类的正确性,包括文件格式验证,元数据验证,字节码验证以及符号引用验证。

  3. 准备

    准备阶段为类的静态变量分配内存,并将其初始化为默认值。假设一个类变量的定义为public static int val = 3;那么变量val在准备阶段过后的初始值不是3而是0。

  4. 解析

    解析阶段将类中符号引用转换为直接引用。符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可。

  5. 初始化

    初始化阶段为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。

JVM内存如何分配

JVM中的内存主要划分为5个区域,即方法区,堆内存,程序计数器,虚拟机栈以及本地方法栈

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ruJt43hx-1605091706617)(/Users/jiangcheng/Java-Notes/图片/JVM图/Jvm结构图.png)]

  1. 方法区:方法区是一个线程之间共享的区域。常量,静态变量以及JIT编译后的代码都在方法区。主要用于存储已被虚拟机加载的类信息,也可以称为“永久代”,垃圾回收效果一般,通过-XX:MaxPermSize控制上限。
  2. 堆内存:堆内存是垃圾回收的主要场所,也是线程之间共享的区域,主要用来存储创建的对象实例,通过-Xmx 和-Xms 可以控制大小。
  3. 虚拟机栈(栈内存):栈内存中主要保存局部变量、基本数据类型变量以及堆内存中某个对象的引用变量**。每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表,操作数栈,动态链接,方法出口等信息。栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈的操作。
  4. 程序计数器:程序计数器是当前线程执行的字节码的位置指示器。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是内存区域中唯一一个在虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
  5. 本地方法栈:主要是为JVM提供使用native 方法的服务。

堆区如何分类

VM的内存可以分为堆内存和非堆内存,堆内存分为新生代和老年代。年轻代又可以进一步划分为一个Eden(伊甸)区和两个Survivor(幸存)区组成。

在这里插入图片描述

JVM堆内存的分配

JVM初始分配的堆内存由**-Xms指定,默认是物理内存的1/64。JVM最大分配的堆内存由-Xmx**指定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此我们一般设置-Xms和-Xmx相等以避免在每次GC 后调整堆的大小。

通过参数**-Xmn2G** 可以设置年轻代大小为2G。通过**-XX:SurvivorRatio**可以设置年轻代中Eden区与Survivor区的比值,设置为8,则表示年轻代中Eden区与一块Survivor的比例为8:1。注意年轻代中有两块Survivor区域。

JVM非堆内存的分配

JVM使用**-XX:PermSize** 设置非堆内存初始值,默认是物理内存的1/64。由**-XX:MaxPermSize**设置最大非堆内存的大小,默认是物理内存的1/4。

堆内存上对象的分配与回收

创建的对象会优先在Eden分配,如果是大对象(很长的字符串数组)则可以直接进入老年代。虚拟机提供一个**-XX:PretenureSizeThreshold参数**,令大于这个参数值的对象直接在老年代中分配,避免在Eden区和两个Survivor区发生大量的内存拷贝。

另外,长期存活的对象将进入老年代,每一次MinorGC(年轻代GC),对象年龄就大一岁,默认15岁晋升到老年代,通过**-XX:MaxTenuringThreshold设置晋升年龄。**

堆与栈区别

管理方式:栈自动释放,堆要GC

空间大小:栈比堆小

碎片相关:栈产生的碎片远小于堆

分配方式:栈支持静态和动态分配,而堆是支持动态分配

效率:栈的效率比堆高

为什么要把字符串常量池放到堆区

字符串常量池在jdk1.6的时候放在方法区,由于在方法区,内存回收效率不高,容易导致内存溢出问题。因此在jkd1.8的时候把字符串常量池从方法区移出,移动了堆。字符串常量池物理上存放在堆,但是逻辑上还是在方法区。

如果字符串常量池放在方法区中,那么每次修改字符串都会产生一个新的对象,而方法区的垃圾回收效率很低,产生过多字符串对象很容易发生OOM错误,因此把字符串常量池放到堆中,方便垃圾回收字符串对象。

java8内存管理上的变化

java8永久代移除了,被元空间取代了,但是元空间本质和永久代差不多。

元空间和永久代最大区别是:永久代使用jvm堆内存,但是java8以后元空间不在虚拟机,而是使用本机的物理内存,元空间仅受本地内存大小限制

为什么要使用元空间代替永久代

字符串在永久代中,容易出现性能问题和内存溢出的问题。类和方法的信息等比较难确定大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出。使用元空间则使用了本地内存。

判断对象已经无效

判断一个对象是否应该被回收,主要是看其是否还有引用。判断对象是否存在引用关系的方法包括引用计数法以及root根搜索方法。

引用计数法

是一种比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题,如A引用B,B引用A,双方计数都是1,导致无法回收。

root根搜索方法

root搜索方法的基本思路就是通过一系列可以做为root的对象作为起始点,从这些节点开始向下搜索。当一个对象到root节点没有任何引用链接时,则证明此对象是可以被回收的。以下对象会被认为是root对象:

  1. 栈内存中引用的对象
  2. 方法区中静态引用和常量引用指向的对象
  3. 被启动类(bootstrap加载器)加载的类和创建的对象
  4. Native方法中JNI引用的对象。

对象的引用

如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称为这块内存代表一个引用。JDK1.2以后将引用分为强引用,软引用,弱引用和虚引用四种。

  1. **强引用:**普通存在, P p = new P(),只要强引用存在,垃圾收集器永远不会回收掉被引用的对象。
  2. **软引用:**通过SoftReference类来实现软引用,在内存不足的时候会将这些软引用回收掉。
  3. **弱引用:**通过WeakReference类来实现弱引用,每次垃圾回收的时候肯定会回收掉弱引用。
  4. **虚引用:**也称为幽灵引用或者幻影引用,通过PhantomReference类实现。设置虚引用只是为了对象被回收时候收到一个系统通知。

哪些对象可以作为GC Root

可以通过引用计数法和可达性分析算法判断对象是否已经死亡

引用计数法实现原理

原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只需要收集计数为0的对象。此算法最致命的是无法处理循环引用的问题,如A引用B,B引用A,双方计数都是1,导致无法回收。

可达性分析算法

根对象(即GC Root)作为起始点集,从这些节点开始根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Root没有引用链相连、则证明此对象是不可能再被使用的。

GC Root对象

可以为作为GC Root对象的是

  1. 虚拟机栈中引用的对象,,如各个线程被调用的方法堆栈中使用的参数,局部变量,临时变量
  2. 在方法区中的类静态属性引用的对象
  3. 方法区中的常量引用对象
  4. 本地方法栈中JNI(即native方法)引用的对象
  5. java虚拟机内部的引用,如基本数据类型对应的Class,一些常驻的异常对象(NullPointException)等,还有类加载器
  6. 所有被同步锁(synchronized)持有的对象
  7. 反应Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等

finalize()方法回收对象的两次标记过程

要判断一个对象是否真正死亡,要经历两次标记过程

  1. 如果对象在进行可达性分析后发现没有与GCRoot相连接的引用链,第一次标记
  2. 随后根据对象是否有必要执行finalize()方法进行一次筛选,假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过则不会执行finalize()方法,如果这个对象被判定有必要执行finalize()方法,那么该对象会被放入F-Queue队列中,然后低调度优先级的finalize线程去执行它们的finalize()方法,随后收集器会对F-Queue队列中对象进行第二次标记,被第二次标记的对象将会被回收。

如何避免第二次标记被回收

对象和引用链的任何对象建立联系即可,如改对象被其他类或者对象成员变量引用。

Java四种引用及场景

强引用

当内存不足,JVM开始垃圾回收,对于强引用的对象,就算出现OOM也不会对该对象进行回收

public class jinfo {
    public static void main(String[] args) throws InterruptedException {
        Object o1=new Object();
        Object o2=o1;
        System.gc();
        System.out.println(o1);
        System.out.println(o2);

    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

软引用

当系统内存充足 不会被回收, 当系统内存不充足 会被回收,一般用于高速缓存

package JVM;

import java.lang.ref.SoftReference;

public class jinfo {
    public static void softRef_Memory_Enough() {
        Object o1=new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());
        //内存够用,不会被回收
        o1=null;
        System.gc();

        System.out.println(o1);
        System.out.println(softReference.get());

    }

    public static void softRef_Memory_NotEnough() {

        Object o1=new Object();
        SoftReference<Object> softReference = new SoftReference<>(o1);
        System.out.println(o1);
        System.out.println(softReference.get());
        //内存不够用,会被回收
        o1=null;

        try {
            byte[] bytes = new byte[30 * 1024 * 1024];
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            System.out.println(o1);
            System.out.println(softReference.get());

        }

    }
    public static void main(String[] args) throws InterruptedException {
//        softRef_Memory_Enough();
        softRef_Memory_NotEnough();
    }
}

  • 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

弱引用

只要垃圾回收机制一运行,不管内存多少 一定会回收

package JVM;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

public class jinfo {



    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        WeakReference<Object> weakReference = new WeakReference<>(o1);
        System.out.println(o1);
        System.out.println(weakReference.get());

        o1=null;
        System.gc();


        System.out.println(o1);
        System.out.println(weakReference.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
谈一下WeakHashMap

原来的HashMap key为空也不会被gc回收

但是WeakHashMap,当key为空时,gc后,会回收这个map

package JVM;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;
public class jinfo {
    public static void main(String[] args) throws InterruptedException {
        WeakHashMap<Integer, String> map = new WeakHashMap<>();
        Integer key = new Integer(2);
        String value = "WeakHashMap";

        map.put(key, value);
        System.out.println(map);

        key = null;
        System.out.println(map);

        System.gc();
        System.out.println(map);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

虚引用

虚引用形同虚设,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收

虚引用要配合引用队列来操作

package JVM;

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.WeakHashMap;

public class jinfo {

    public static void main(String[] args) throws InterruptedException {
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        //虚引用可以用 弱引用另一外一种调用方法代替
//        WeakReference<Object> weakReference = new WeakReference<>(o1, referenceQueue);
        //虚引用
        PhantomReference<Object> phantomReference = new PhantomReference<>(o1, referenceQueue);
        System.out.println(o1);
        System.out.println(phantomReference.get());
        System.out.println(referenceQueue.poll());

        o1=null;
        System.gc();
        Thread.sleep(500);
        System.out.println();

        System.out.println(o1);
        System.out.println(phantomReference.get());
        //调用gc后,虚引用进入 引用队列
        System.out.println(referenceQueue.poll());

    }
}

  • 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

垃圾回收算法和流程

垃圾回收算法可以分为引用计数式垃圾收集和追踪式垃圾收集。

垃圾回收类型

Minor GC 目标只是新生代的垃圾回收

Major GC 目标只是老年代的垃圾回收,目前只有CMS收集器会有单独回收老年代

Mixed GC 目标是整个新生代和部分老年代的垃圾回收,目前只有G1收集器有这种行为

Full GC 收集整个堆和方法区

垃圾回收算法类型

标记清除算法

原理:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象。或者标记出所有存活的对象,在标记完成后,统一回收所有没有被标记的对象。

缺点

  1. 执行效率不稳定,因为如果java堆中包含大量对象,其中大部分都要被回收,则这时就要大量标记和清除动作,导致标记和清除两个过程执行效率都随对象数量增长而降低。
  2. 内存空间碎片化问题,标记和清除对象后会产生大量不连续的内存碎片,碎片太多可能导致以后要分配较大对象的时候,无法找到足够的连续内存空间来保存对象,而不得不触发另外一次垃圾收集动作。
执行过程图

在这里插入图片描述

标记复制算法

原理:将可用内存分为大小相等的两块,每次只使用其中一块,当这一块内存使用完后,则把内存还存活的对象复制到另外一块内存空间,把已使用的那一块清理掉。如果内存中多数对象都可以回收,那么算法只需要复制少量对象。每次分配都是针对整个半区进行内存回收,分配时,不需要考虑内存空间碎片化问题。分配内存的时候只需要移动栈顶指针,按需分配。

缺点

  1. 如果内存中大多数对象都是存活的,则这个算法会产生大量的内存复制开销。
  2. 内存缩小为原来的一半,造成空间的浪费。
执行过程图

在这里插入图片描述

标记整理算法

原理:标记过程和标记清除算法一样,标记完成把存活对象都行内存空间一端移动,然后清理掉边界以外的内存。与标记清除算法本质区别是,标记清除是一种非移动式的回收算法,标记整理是一种移动式的回收算法。

缺点:不管是否移动对象都存在弊端,移动则内存回收更复杂,不移动则内存分配的时候会更复杂,虽然不移动对象,则停顿时间更短,但是吞吐量会降低。移动对象吞吐量会提升,但是要耗费大量时间。总体来说内存分配比内存访问频率要高很多,因此移动对象总体效率更高。

执行过程图

在这里插入图片描述

如何减少full gc的次数

  • 对象不用的时候设置为null
    • 设置对象为null,有利于GC收集器判定垃圾,提高GC效率。
  • 少用System.gc()
    • 此函数建议JVM进行主GC,会增加主GC的频率,增加了间接性停顿的次数。
  • 少用静态变量
    • 静态变量属于全局变量,不会被GC回收,他们会一直占用内存
  • 尽量使用StringBuffer, 而不用String来累加字符串
    • 每次改变String内容的时候,String都会创建新的对象,导致生成大量垃圾对象。使用StringBuffer的时候,因 StringBuffer是可变长的,它在原有基础上进行扩增,不会产生中间对象。
  • 分散对象创建或删除的时间
    • 集中在短时间内大量创建新对象,特别是大对象,会导致突然需要大量内存,JVM在这种情况下只能进行主GC以回收内存,从而增加主GC的频率。
    • 集中在短时间内删除大量对象,它使得突然出现了大量的垃圾对象,空闲空间必然减少,从而大大增加了下一次创建新对象时强制主 GC 的机会。
  • 尽量少用finaliza函数
    • 它会加大 GC 的工作量
  • 能用基本类型如int,long,就不用Integer,Long 对象
  • 增大-Xmx的值

方法区是否需要gc

主要有两种垃圾回收方式

  1. 常量池回收(废弃常量,比如一个常量不在任何地方被引用,则被标记为废弃常量,这个常量可以回收)

  2. 对无用类的回收(类卸载)

    1. 若堆中不存在该类的任何实例对象。
    2. 加载该类的类加载器已经被回收。
    3. 该类对于的java.lang.Class对象不在任何地方被引用,且无法在任何地方通过反射访问该类的方法。
    • 满足以上三点,代表该类可以回收,但是并非一定会回收,还要看jvm设置的参数控制。

什么时候会会进行Minor GC

大多数情况下,对象在新生代Eden区分配,当在Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

查看GC状态的命令

jps

可以查看正在运行的虚拟机进程

jstat

用于监视虚拟机各种运行状态,可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即使编译等运行数据。命令为 jstat [option vmid [interval[s|ms] [count]]]

例子 jstat -gc 200 100 20 代表每100毫秒查询一次进程id为200的垃圾收集状况,一共查询20次。如果省略

100和20,代表只查询一次。

jinfo

实时查看和调整虚拟机各项参数。

jinfo [option]

jmap

用于生成堆转储快照 jmap [option]

option取值

选项作用
-dump生产java转储快照,格式为-dump:[live,]format=b,file=,其中live子参数说明是否只dump出存活对象。
-heap显示java堆详细信息,如使用哪种回收器,参数配置,分代状况。只在Linux/Solaris有效
-histo显示堆中对象统计信息,包括类,实例数量,合集容量

jhat

用来分析jmap生成的堆转储快照。现在一般不用jhat,而用VisualVM

jstack

用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机每一条线程正在执行的方法堆栈的集合,生成线程快照的目的是定位线程出现长时间停顿的原因,如线程间思索,死循环等

命令格式 jstack [option] vmid

选项作用
-F当正常输出的请求不被响应时,强制输出线程堆栈
-l除堆栈外,显示关于锁的附加信息
-m如果调用到本地方法的话,可以显示c/c++堆栈

CMS收集器的流程

运行过程

  1. 初始标记

    这个过程要"Stop the World",标记速度很快,只是标记GC Roots能直接关联到的对象

  2. 并发标记

    就是从GC Roots的直接关联对象开始遍历整个图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行

  3. 重新标记

    这个过程要"Stop the World",这个阶段是为了修正并发标记期间,因用户程序继续运行而导致的标记产生变动的那一部分对象记录,这个阶段停顿时间比初始标记时间长一些,但是也比并发标记阶段时间短。

  4. 并发清除

    清理删除掉标记阶段判断已经死亡的对象,由于不需要移动存活对象,所以可以和用户线程同时并发执行。

收集器运行示意图

在这里插入图片描述

缺点

  1. 在并发阶段,虽然不会导致用户线程停顿,但是会占用一部分线程,导致程序变慢,降低吞吐量。
  2. 随着程序运行会产生大量浮动垃圾,这部分垃圾出现在标记过程结束后,所以CMS无法再统一处理它们,要等待下一次垃圾收集时再清理。
  3. 如果CMS的触发百分比(CMSInitiatingOccu-pancyFraction)过高,就会没有足够的内存来满足分配内存的需求。如果CMS的触发百分比太低,因为老年代增长不是很快,所以导致频繁回收内存,降低了内存回收效率。
  4. CMS是基于标记清除算法实现的收集器,可以产生大量碎片,导致要分配大对象的时候触发FullGC。

G1收集器的流程

使用的是Mixed GC模式,基于Region堆内存布局,不以固定大小以及固定数量的分代区域划分,而是把连续的java堆划分为多个大小相等的独立区域每一个Region都可以根据需求,扮演新生代的Eden空间,Survivor空间或者老年代空间,收集器可以扮演不同的角色,也可以根据不同的扮演的不同角色采用不同的策略去处理。

如何处理大对象

Region有一类特殊的Humongous区域,专门用来存在大对象,如果一个对象大小超过了Region容量的一半的对象就可以大对象,每个Region大小可以通过-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。如果一个大对象超过Region容量,则会被存放在N个连续的Humongous Region中,G1大多数行为会把Humongous Region当作老年代一部分看待。每次垃圾回收,G1都会跟踪各个Region里面的垃圾堆价值大小,优先处理回收价值最大的Region。

G1结构图

在这里插入图片描述

运行过程

  1. 初始标记

    仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的只,让下一阶段用户线程并发运行时能正确地在可用Region中分配对象,这个阶段要暂停线程,但是借用在MinorGC的时候同步完成,所以耗时很短。

  2. 并发标记

    从GC Roots开始与堆中对象进行可行性分析,递归扫描整个堆里面的对象图,找出要回收的对象,耗时较长,可与用户程序并发执行。当扫描完对象图后还要重新处理STAM记录下的在并发时有引用变动的对象。

  3. 最终标记

    暂停用户线程,然后处理并发阶段后仍然遗留下来的最后那少量SATB。

  4. 筛选回收

    负责更新Region的统计数据,根据Region的回收价值和成本进行排序,然后根据用户所期望的停顿时间来制定回收计划。可以选择任意多个Region构成回收集,然后把回收那一部分的Region复制到空Region中,再清理整个旧Region全部空间。这里要移动对象,所以要暂停用户线程

  • 除了并发标记阶段,其余阶段都要完全暂停用户线程,确保在延迟可控的情况下获得尽可能高的吞吐量。

收集器运行示意图

在这里插入图片描述

CMS收集器和G1收集器的区别

  1. CMS基于标记复制算法,G1整体基于标记整理算法实现的收集器,但是G1从局部(两个Region之间)看又是基于标记复制算法。
  2. G1运行期间不会产生内存空间碎片,而CMS会产生
  3. G1垃圾收集后有规整的内存,有助于程序长时间运行,在程序为大对象分配内存时不容易因无法找到连续内存空间而提前触发下一次收集。CMS回收后内存不规整,遇到大对象会触发Full GC。
  4. G1位了垃圾收集产生的内存占用和程序运行时的额外执行负载都比CMS高。
  5. G1卡表比CMS卡表复杂,G1占用更多的内存,G1无论是新生代还是老年代都有一份卡表,而CMS只有一份卡表。
  6. G1利用的是原始快照搜索算法,CMS利用的是增量更新算法。
  7. CMS适用于小内存,G1适用于大内存。

原始快照搜索算法和增量更新算法区别

原始快照搜索算法

若删除没被访问过的对象的引用,则把删除的引用记录下来,在并发扫描结束后,将这些被删除引用的对象设置根,进行搜索。

增量更新算法

已经被标记为的对象一旦新插入了指向没被标记的对象引用之后,把这个新的引用记录下来,等并发扫描结束后,再将这些记录过的引用对象设为根,重新扫描。

内存管理(对象创建、布局、访问定位)

对象的创建过程的内存分配

一般情况下我们通过new指令来创建对象,当虚拟机遇到一条new指令的时候,会去检查这个指令的参数是否能在常量池中定位到某个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。如果没有,那么会执行类加载过程。通过执行类的加载,验证,准备,解析,初始化步骤,完成了类的加载,这个时候会为该对象进行内存分配,也就是把一块确定大小的内存从Java堆中划分出来,在分配的内存上完成对象的创建工作。

内存分配方式

  1. 指针碰撞

    假设Java堆中的内存是绝对规整的,用过的内存在一边,未使用的内存在另一边,中间有一个指示指针,那么所有的内存分配就是把那个指针向空闲空间那边挪动一段与对象大小相等的距离。

  2. 空闲列表方式

    如果Java堆内存中不是规整的,已使用和未使用的内存相互交错,那么虚拟机就必须维护一个列表用来记录哪块内存是可用的,在分配的时候找到一块足够大的空间分配对象实例,并且需要更新列表上的记录。需要注意的是,Java 堆内存是否规整是由所使用的垃圾收集器是否拥有压缩整理功能来决定的。

内存布局

主要可以划分为三个部分对象头,实例数据,对齐填充。

对象头

有两类信息

  1. 用于存储对象自身运行的数据,如哈希吗,GC分代年龄,锁状态标志,线程持有锁,偏向线程ID、时间戳等,被称为Mark Word。

    存储内容标识位状态
    对象哈希码,对象分代年龄01未锁定
    指向锁记录的指针00轻量级锁定
    指向重量级锁的指针10膨胀
    空,不需要记录11GC标记
    偏向线程ID,偏向时间戳01可偏向
  2. 类型指针,即对象指向它的类型元数据的指针,虚拟机可以通过这个指针来确定该对象是哪个类的实例。

访问定位

java程序会通过栈上的reference数据来操作堆上的具体对象,访问堆上的对象主要方式有使用句柄和直接指针两种

使用句柄

java堆中会划出一块内存作为句柄池,reference中存储的就是对象句柄地址,而句柄中包含了对象的实例数据和类型数据各自具体地址信息,然后可以利用该对象地址去访问对象和数据,总共要两次访问。

直接指针

reference中存储的就是对象地址和对象类型数据的指针,如果直接访问对象,只需要一次访问,避免了间接访问带来的开销(注意,如果是访问对象类型数据,还是需要两次访问,因为reference中存储的是对象类型数据的指针)

两者对比

使用直接指针方法访问速度比使用句柄快,由于对象访问在java中非常频繁,因此这节省了一次指针定位的时间开销,

怎么查看java虚拟机内存占用

jstat

用于监视虚拟机各种运行状态,可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即使编译等运行数据。命令为 jstat [option vmid [interval[s|ms] [count]]]

例子 jstat -gc 200 100 20 代表每100毫秒查询一次进程id为200的垃圾收集状况,一共查询20次。如果省略

100和20,代表只查询一次。

jinfo

实时查看和调整虚拟机各项参数。

jinfo [option]

jmap

用于生成堆转储快照 jmap [option]

option取值

选项作用
-dump生产java转储快照,格式为-dump:[live,]format=b,file=,其中live子参数说明是否只dump出存活对象。
-heap显示java堆详细信息,如使用哪种回收器,参数配置,分代状况。只在Linux/Solaris有效
-histo显示堆中对象统计信息,包括类,实例数量,合集容量

VisualVM

可以使用VisualVM可视化工具

线程上下文类加载器

JNDI服务可以使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器请求子类加载器完成类加载的行为,这种行为是为了打通双亲委派模型的层次结构来逆向使用类加载器。java涉及SPI的加载基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB,JBI等。目前是利用java.utils.ServiceLoader类,以META-INF/service中的配置信息,辅以责任链模式,来执行SPI加载。

破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

内存分配的线程安全问题

  1. 对分配内存空间的动作进行同步处理,通过CAS + 失败重试的方式保证更新指针操作的原子性
  2. 把分配内存的动作按照线程划分在不同的空间之中,即给每一个线程都预先分配一小段的内存,称为本地线程分配缓存(TLAB),只有TLAB用完并分配新的TLAB时,才需要进行同步锁定。 虚拟机是否使用TLAB,可以通过**-XX: +/-UserTLAB**参数来设定

对象被访问的时候是怎么被找到的

当创建一个对象的时候,在栈内存中会有一个引用变量,指向堆内存中的某个具体的对象实例。Java虚拟机规范中并没有规定这个引用变量应该以何种方式去定位和访问堆内存中的具体对象。目前常见的对象访问方式有两种,即句柄访问方式和直接指针访问方式,分别介绍如下。

  1. 句柄访问方式:在JVM的堆内存中划分出一块内存来作为句柄池,引用变量中存储的就是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。在内存垃圾收集之后,对象会移动,但是引用reference中存储的是稳定的句柄地址,但是句柄地址方式不直接,访问速度较慢。
  2. 直接指针访问方式:引用变量中存储的就是对象的直接地址,通过指针直接访问对象。直接指针的访问方式节省了一次指针定位的时间开销,速度较快。Sun HotSpot使用了直接指针方式进行对象的访问。

Class类文件怎么回收

回收条件

1、该类所有的实例都已经被回收,也就是说Java堆中不存在该类的任何实例;

2、加载该类的ClassLoader已经被回收;

3、该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

堆内存上的对象回收也叫做垃圾回收,那么垃圾回收什么时候开始

垃圾回收主要是完成清理对象,整理内存的工作。GC经常发生的区域是堆区,堆区还可以细分为新生代、老年代。新生代还分为一个Eden区和两个Survivor区。垃圾回收分为年轻代区域发生的Minor GC和老年代区域发生的Full GC。

Minor Gc

对象优先在Eden中分配,当Eden中没有足够空间时,虚拟机将发生一次Minor GC,因为Java大多数对象都是朝生夕灭,所以Minor GC非常频繁,而且速度也很快。

FULL Gc

Full GC是指发生在老年代的GC,当老年代没有足够的空间时即发生Full GC,发生Full GC一般都会有一次Minor GC。

动态对象年龄判定

如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于等于该对象年龄的对象即可晋升到老年代,不必要等到-XX:MaxTenuringThreshold。

空间分配担保

发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小。如果大于,则进行一次Full GC(老年代GC),如果小于,则查看HandlePromotionFailure设置是否允许担保失败,如果允许,那只会进行一次Minor GC,如果不允许,则改为进行一次Full GC

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/空白诗007/article/detail/960434
推荐阅读
相关标签
  

闽ICP备14008679号