Java面试必知:jvm垃圾回收机制

JVM的基本组成

  • 类加载器
  • 运行时数据区
  • 执行引擎
  • 本地接口
    在这里插入图片描述
    在这里插入图片描述

运行时数据区

程序计数器

作用: 确定程序执行哪行语句

  • 程序计数器为线程私有,每个线程都有自己独立的程序计数器且互不干扰
  • 程序计数器为唯一一片不会发生内存泄露的区域

作用:存放对象实例和数组

  • 存放所有java对象实例的属性数据.**所有线程共享堆内的内容 **
  • 堆所占内存最大
  • 当出现new时,在堆内存区域,开辟空间
  • 堆内存的开辟,只需要逻辑上连续就行,不需要物理上连续
虚拟机栈

作用:调用时存放java方法(局部变量表,操作数栈,动态链接,方法出口)

  • **JVM为每个运行的线程创建一个独立的方法栈,因此栈区一定是线程安全的 **
  • 属于线程私有,生命周期与线程相同
  • 先进后出
  • 当方法调用,将方法调入栈内存中,方法执行结束弹出.
本地方法栈

作用:调用时存放Native方法

  • native指本地方法(直接和操作系统打交道的方法 如C和C++)
  • 被java调用
方法区

作用:存放类信息,常量和静态变量,编译后的代码

 String s1=new String("hello");
        String s2="hello";
        String s3="hello";
        System.out.println(s1==s2);
        System.out.println(s2==s3);

运行结果: false true
原因: String s1=new String(“hello”);在堆区开辟内存,String s2=“hello”;以常量的形式在方法区开辟空间,String s3=“hello”;在常量池找到相同字符串,所以s2,s3指向同一内存空间.

内存分配与回收策略

判断对象已死的算法
  • 在堆内存中存放着多个java对象,垃圾回收器需要判断哪些对象存活,哪些对象死亡,并且将死亡对象进行垃圾回收
引用计数器算法
  • 给对象添加一个引用计数器,每有一个地方引用该对象时,计数器+1,当引用失效时,计数器-1.当计数器为0时,该对象不再被引用
  • 当一个对象被回收后,被该对象所引用的其他对象的引用计数都应该相应减少
  • 企业级开发不使用此算法,因为存在一定问题.
  • 存在问题:当对象objA通过属性引用objB;而objB又通过属性引用objA.俩个对象相互引用.
  • 此时引用计数器算法无法进行垃圾回收
可达性分析算法
  • 为主流使用算法
  • 以GC Roots的对象为起始点,往下搜索,所有GC Roots 引用或间接引用的对象不能被回收
  • 当一个对象到GC Roots 没有任何引用链相连,证明此对象不可用(被回收)

在java中,GC Root的对象包括以下几种:

  1. 虚拟栈中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中的常量引用的对象
  4. 本方法栈中JNI(native方法)的引用的对象
引用的分类
  • 强引用
    • 类似于Object o=new Object(); 只要引用一直存在就不会被回收
  • 软引用
    • 还有用但并非必须的对象
    • 在内存溢出异常发生之前,对其进行二次回收
    • 使用SoftReference类来实现软引用
  • 弱引用
    • 仅存活到下一次垃圾回收前
    • 使用WeakReference类来实现弱引用
  • 虚引用
    • 为对象设置虚引用的唯一目的是该对象被垃圾回收时收到一个系统通知
    • 使用PhantomReference类来实现虚引用
清理内存的算法
标记清除算法
  • 将回收对象依次标记然后全部清理
  • 造成大量的内存碎片,内存利用率不高
  • 标记的效率和清除的效率也不高
标记整理算法
  • 将存活对象向同一个方向移动.移动后,将之后的内存(回收对象)进行清理
复制算法(不单独使用)
分代垃圾回收
  • 分代分为年轻代和老年代, 年轻代又分为Eden区和Survivor区,通常默认比例为8:1:1,每次只保留10%的空间用作预留区域,90%可以用作新生对象
  • 每一次垃圾回收之后,对象存活的年龄+1当经历15次垃圾回收任然存活的对象进入老年代
  • 另一种进入老年代的方式:担保机制,当新生代的空间不够的时候,对象进入老年代
  • 新生代的垃圾回收叫Minor GC,老年代的叫Full GC
  • 在新生代,每次垃圾回收都有大量的对象死去,只有少量存活,适合采用复制算法
  • 在老年代,因为对象存活率高,且没有其他内存进行分配担保.就必须采用标记-清理或标记-整理进行回收
垃圾收集器
Serial收集器
  • 这是一个单线程收集器
  • 它在进行垃圾回收时,其余工作线程必须全部停止工作,等待它清理完,再工作
  • 是最基本,最悠久的收集器
  • Server模式下的新生代收集器
ParNew收集器
  • Serial收集器的多线程版本(特指垃圾回收多线程).除了多线程进行垃圾回收之外,其他和Serial一样
  • Server模式下的新生代的首选虚拟机收集器
Parallel Scavenge收集器
  • 是一个新生代收集器,采用复制算法,也是并行的多线程的收集器
  • 和ParNew收集器不同的时,ParNew收集器注重的是回收时间最短.Parallel Scavenge收集器是关注的吞吐量
  • 吞吐量=(用户线程工作时间)/(用户线程工作时间+垃圾回收时间)
  • 自适应策略也是Parallel Scavenge收集器区别于ParNew收集器的重要一点
Serial Old收集器
  • 单线程
  • 采用标记-整理算法
Parallel Old收集器
  • 多线程
  • 采用标记-整理算法
  • 注重吞吐量
CMS 收集器
  • 并发 标记清除算法
  • 只有初始标记和重新标记需要暂停用户线程
    运行步骤如下图:在这里插入图片描述
    CMS收集器的三大缺点:
  1. CMS收集器对cpu资源非常敏感(在并发标记和并发清理阶段和用户线程一同进行,故对cpu敏感)
  2. 无法处理浮动垃圾(在并发清理阶段产生的垃圾)
  3. 因为基于标记-清理算法,会产生大量的垃圾碎片
G1收集器(最先进的收集器)

步骤:

  1. 内存分配
    • 将内存看为一个又一个的小块将其随机分配为以下四块:
    • Eden: 存放新生代对象
    • Survivor: Young垃圾回收后,采用复制算法将Eden区的存活对象 复制到Survivor区, Eden区清空 (若Suvivor区内存不够,多余存入老年区)
    • old: 存放老年代对象
    • Humongous: 存放大对象
  2. Young垃圾回收
    • 采用复制算法将Eden区的存活对象 复制到Survivor区, Eden区清空 (若Suvivor区内存不够,多余存入老年区) (采用分代回收 Eden+C(from)=>C(to))

以下为将要用到的名字解释:

  • Regin: 内存划分成的小块
  • Rset:标记其他regin引用当前regin
  • Cset:本次GC需要清理的Regin集合
  • RootRegin: GC Root所在的 Regin
  • STW: stop the world 工作线程停止工作
  1. Mix垃圾回收
    1. 初次标记: 标记GC Root直接引用的对象 并且标记RootRegin (STW)
    2. 扫描old区的Rset中是否含有 RootRegin,若有 则标记
    3. 并发标记 同CMS的并发标记,(标记GC Root 间接引用对象) 只不过遍历范围缩小步骤2.的标记, 查看2中标记的Rset里面的Regin 并找到标记,直到结束.
    4. 重新标记 同CMS 但标记算法采用SATB (效率高,速度快)(STW)
    5. 清理: 采用复制清理(只选出垃圾较多的old区进行清理)(STW)
jvm常用参数

在这里插入图片描述

问题

基本数据类型和引用数据类型的区别 重点


  1. 基本数据类型只有八种(byte short int long boolean char float double)
    其余的都属于引用数据类型又叫对象类型,如数组,字符串等

  2. 占用的内存空间不同

    基本数据类型的变量和对应的数据都存放在栈空间中
    引用数据类型的数据存在于堆空间中,而引用型变量存在于栈空间中,相当于一个指针指向堆空间的某个对象,存储的对象在堆空间中的首地址

  3. 生命周期不同

    基本型数据的作用范围是定义它的一对{}内,一旦出了大括号的范围,基本类型数据就被销毁
    引用型数据的作用范围是从创建对象开始(new),一旦数据没有被任何引用变量引用(null),则对象成为垃圾,一旦被垃圾回收器回收,引用类型数据就被销毁

Java中方法调用时参数的传递机制(重点)

无论何种数据类型,Java中均采用值传递的机制,没有引用传递的概念

  1. 基本类型参数,传递的是变量的数据值
  2. 引用类型参数,传递的是变量的引用地址值
  3. 与String类似,所有的包装类都是final类,即不可变类. 他们在做参数传递时,并不会改变原址内容,也不会改变实参指向,所以 实参不变.
  4. 总结: 实参为以下三种情况,值不会发生改变:1.基本数据类型.2.引用类型中的字符串.3.基本类型被包装后的包装类
public static void main(String[] args) {	
    int[] arr = {1, 2};	
    change(arr);
    System.out.println("arr[0]=" + arr[0] + " arr[1]=" + arr[1]);	
}

static void change(int[] arr){		
    arr[1] = 10; 	  //改变数组的内容 实参内容改变
   arr = new int[5];   //改变数组的指向  相当于将形参指向新开辟的内存空间,实参指向和内容不变
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值