Java面试基础篇-JVM

Java 内存区域

Java虚拟机主要包含几个区域:

  • 堆:堆Java虚拟机中最大的一块内存,是线程共享的内存区域,基本上所有的对象实例数组都是在堆上分配空间。堆区细分为Yound区年轻代和Old区老年代,其中年轻代又分为Eden、S0、S13个部分,他们默认的比例是8:1:1的大小。
  • 栈:栈是线程私有的内存区域,每个方法执行的时候都会在栈创建一个栈帧,方法的调用过程就对应着栈的入栈和出栈的过程。每个栈帧的结构又包含局部变量表、操作数栈、动态连接、方法返回地址。
    1.局部变量表用于存储方法参数和局部变量。当第一个方法被调用的时候,他的参数会被传递至从0开始的连续的局部变量表中。
    2.操作数栈用于一些字节码指令从局部变量表中传递至操作数栈,也用来准备方法调用的参数以及接收方法返回结果。
    3.动态连接用于将符号引用表示的方法转换为实际方法的直接引用。
  • 元数据:在Java1.7之前,包含方法区的概念,常量池就存在于方法区(永久代)中,而方法区本身是一个逻辑上的概念,在1.7之后则是把常量池移到了堆内,1.8之后移出了永久代的概念(方法区的概念仍然保留),实现方式则是现在的元数据。它包含类的元信息和运行时常量池。
    Class文件就是类和接口的定义信息。
    运行时常量池就是类和接口的常量池运行时的表现形式。
  • 本地方法栈:主要用于执行本地native方法的区域
  • 程序计数器:也是线程私有的区域,用于记录当前线程下虚拟机正在执行的字节码的指令地址
    线程共享的区域:

    方法区
    直接内存(非运行时数据区的一部分)

Java对象的创建过程

当虚拟机遇见new关键字时候,实现判断当前类是否已经加载,如果类没有加载,首先执行类的加载机制,加载完成后再为对象分配空间、初始化等。
在这里插入图片描述
类加载过程:

  1. 首先校验当前类是否被加载,如果没有加载,执行类加载机制
  2. 加载:就是从字节码加载成二进制流的过程
  3. 验证:当然加载完成之后,当然需要校验Class文件是否符合虚拟机规范,跟我们接口请求一样,第一件事情当然是先做个参数校验了
  4. 准备:为静态变量、常量赋默认值
  5. 解析:把常量池中符号引用(以符号描述引用的目标)替换为直接引用(指向目标的指针或者句柄等)的过程
  6. 初始化:执行static代码块(cinit)进行初始化,如果存在父类,先对父类进行初始化
    对象分配内存空间和初始化的过程:
  7. 首先为对象分配合适大小的内存空间
  8. 接着为实例变量赋默认值
  9. 设置对象的头信息,对象hash码、GC分代年龄、元数据信息等
  10. 执行构造函数(init)初始化

堆内存中对象的分配的基本策略

  • 对象优先在Eden分配
    大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
  • 大对象直接进入老年代
    所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。
  • 长期存活的对象将进入老年代
    对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
  • 动态对象年龄判定
    如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
  • 空间分配担保
    新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。
    担保失败进行FullGC。
  • 栈上分配
    JVM允许将线程私有的对象打散分配在栈上,而不是分配在堆上。分配在栈上的好处是可以在函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统性能。栈上分配只是JVM虚拟机提供的一种优化技术,对象主要还是分配在堆上的。
  • TLAB(线程本地分配缓冲)
    JVM通过使用TLAB来避免多线程冲突,每个线程使用自己的TLAB,这样就保证了不使用同步,也不会出现线程安全问题,提高了对象分配的效率。

Minor Gc和Full GC 有什么不同呢

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

  • 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
  • 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的 Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。

怎么标记一个对象是否存活?

  1. 简单的通过引用计数法,给对象设置一个引用计数器,每当有一个地方引用他,就给计数器+1,反之则计数器-1,但是这个简单的算法无法解决循环引用的问题。
  2. Java通过可达性分析算法来达到标记存活对象的目的,定义一系列的GC ROOT为起点,从起点开始向下开始搜索,搜索走过的路径称为引用链,当一个对象到GC ROOT没有任何引用链相连的话,则对象可以判定是可以被回收的。

而可以作为GC ROOT的对象包括:

  • 栈中引用的对象
  • 静态变量、常量引用的对象
  • 本地方法栈native方法引用的对象

简单的介绍一下强引用,软引用,弱引用,虚引用

强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。

垃圾收集有哪些算法,各自的特点

标记-清除

算法分为“标记”和“清除”阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的 对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两 明显的问题:

  1. 效率问题(标记的过程需要遍历所有的GC ROOT,清除的过程也要遍历堆中所有的对象)
  2. 空间问题(标记清除后会产生大量不连续的碎片)

复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一 块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。 这样就使每次的内存回收都是对内存区间的一半进行回收。
但是带来了另外一个问题,可使用的内存空间缩小了一半!

标记-整理

针对老年代再用复制算法显然不合适,因为进入老年代的对象都存活率比较高了,这时候再频繁的复制对性能影响就比较大,而且也不会再有另外的空间进行兜底。所以针对老年代的特点,通过标记-整理算法,标记出所有的存活对象,让所有存活的对象都向一端移动,然后清理掉边界以外的内存空间。

常⻅的垃圾回收器有那些?

在这里插入图片描述
Serial:单线程版本收集器,进行垃圾回收的时候会STW(Stop The World),也就是进行垃圾回收的时候其他的工作线程都必须暂停

ParNew:Serial的多线程版本,用于和CMS配合使用

Parallel Scavenge:可以并行收集的多线程垃圾收集器

Serial Old:Serial的老年代版本,也是单线程

Parallel Old:Parallel Scavenge的老年代版本

CMS(Concurrent Mark Sweep):CMS收集器是以获取最短停顿时间为目标的收集器,相对于其他的收集器STW的时间更短暂,可以并行收集是他的特点,同时他基于标记-清除算法,整个GC的过程分为4步。

初始标记:标记GC ROOT能关联到的对象,需要STW
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,不需要STW
重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生改变的标记,需要STW
并发清除:清理删除掉标记阶段判断的已经死亡的对象,不需要STW

G1(Garbage First):G1收集器是JDK9的默认垃圾收集器,而且不再区分年轻代和老年代进行回收。
G1的回收过程分为以下四个步骤:

初始标记:标记GC ROOT能关联到的对象,需要STW
并发标记:从GCRoots的直接关联对象开始遍历整个对象图的过程,扫描完成后还会重新处理并发标记过程中产生变动的对象
最终标记:短暂暂停用户线程,再处理一次,需要STW
筛选回收:更新Region的统计数据,对每个Region的回收价值和成本排序,根据用户设置的停顿时间制定回收计划。再把需要回收的Region中存活对象复制到空的Region,同时清理旧的Region。需要STW

双亲委派模型

类加载器自顶向下分为:

Bootstrap ClassLoader启动类加载器:默认会去加载JAVA_HOME/lib目录下的jar
Extention ClassLoader扩展类加载器:默认去加载JAVA_HOME/lib/ext目录下的jar
Application ClassLoader应用程序类加载器:比如我们的web应用,会加载web程序中ClassPath下的类
User ClassLoader用户自定义类加载器:由用户自己定义
在这里插入图片描述

当我们在加载类的时候,首先都会向上询问自己的父加载器是否已经加载,如果没有则依次向上询问,如果没有加载,则从上到下依次尝试是否能加载当前类,直到加载成功。

双亲委派模型带来了什么好处呢?

双亲委派模型保证了Java程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据 类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不 被篡改。如果不用没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我 们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的
Object 类。

频繁FullGC怎么排查?

  1. jstat -gcutil或者查看gc.log日志,查看内存回收情况
  2. dump出内存文件在具体分析,比如通过jmap命令jmap -dump:format=b,file=dumpfile pid,导出之后再通过Eclipse Memory Analyzer等工具进行分析,定位到代码,修复。

JVM调优

参数含义

-Xms设置初始堆的大小,-Xmx设置最大堆的大小
-XX:NewSize年轻代大小,-XX:MaxNewSize年轻代最大值,-Xmn则是相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值
-XX:NewRatio设置年轻代和年老代的比值,如果为3,表示年轻代与老年代比值为1:3,默认值为2
-XX:SurvivorRatio年轻代和两个Survivor的比值,默认8,代表比值为8:1:1
-XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。
-XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
-XX:MaxDirectMemorySize当Direct ByteBuffer分配的堆外内存到达指定大小后,即触发Full GC

调优

  • 为了打印日志方便排查问题最好开启GC日志,开启GC日志对性能影响微乎其微,但是能帮助我们快速排查定位问题。-XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:gc.log
  • 一般设置-Xms=-Xmx,这样可以获得固定大小的堆内存,减少GC的次数和耗时,可以使得堆相对稳定
  • -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,方便排查问题
  • -Xmn设置新生代的大小,太小会增加YGC,太大会减小老年代大小,一般设置为整个堆的1/4到1/3
  • 设置-XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值