JVM快速入门

JVM入门

    • JVM探究
      • 1. JVM的位置
      • 2. JVM体系结构
      • 3. 类加载器
      • 4. 双亲委派机制
      • 5. 沙箱安全机制
      • 6. native关键字
      • 7. PC寄存器
      • 8. 方法区
      • 9. 栈
      • 10. 三种JVM
      • 11. 堆
      • 12. 新生区、老年区
      • 13. 永久区
      • 14. 堆内存调优
      • 15. GC:垃圾回收机制
        • 15.1 常用算法
      • 16. JMM
      • 17. 总结

JVM探究

面试题:

  • 请你谈谈你对JVM的理解?Java8虚拟机的更新
  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
  • JVM常用调优参数有哪些?
  • 内存快照如何抓取?怎么分析Dump文件?
  • 谈谈JVM中,类加载器你的认识?
    解答见本文最后 17.总结

1. JVM的位置

JVM快速入门_第1张图片

2. JVM体系结构

栈和计数器中不会存在垃圾,只有方法区和堆中存在,因此调优主要是针对堆。
JVM快速入门_第2张图片

3. 类加载器

作用:加载class文件

分类:
1.虚拟机自带的加载器
2.启动类加载器(根加载器)
3.扩展类加载器 — jre/lib/ext文件中
4.应用程序加载器(系统类加载器)— jre/lib/rt.jar

流程:
1.类加载器收到类加载的请求
2.将这个请求向上委托父类加载器去完成,一直向上委托,直到启动类加载器
3.启动类加载器检查是否能够加载当前这个类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子类加载器进行加载
4.重复步骤3
Class Not Found ~

4. 双亲委派机制

目的:保证安全
流程:由 应用程序加载器 到 扩展类加载器 到 根加载器 中找,最终执行根加载器

5. 沙箱安全机制

6. native关键字

凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层的C语言库!
过程:会进入本地方法栈,本地方法栈会调用本能地方法接口JNI,JNI的作用是扩展Java的使用融合不同的编程语言为Java所用
Java诞生的时候C和C++横行,为了调用其在内存中专门开辟了一块标记区域:Native Method Stack本地方法栈,登记native方法,最终执行的时候加载本地方法通过JNI

现在常用的调用其他语言接口的方法:Socket,WebService,http~

7. PC寄存器

程序计数器:Program Counter Register
每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的字节码(用来存储指向下一条指令的地址,也即要执行的指令代码)在指令引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计

8. 方法区

方法区:Method Area
方法区是被所有的线程共享的,所有的字段和字节码信息,以及一些特殊方法如构造函数,接口代码也在此定义,简单的说,所有定义的方法的信息都保存在该区域,此区域属于共享空间
静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中,但是实例变量存在于堆内存中,和方法区无关即static,final,Class,常量池

9. 栈

栈内存,主管程序运行。生命周期和线程同步
线程结束,栈内存也就释放了,对于栈来说不存在垃圾回收问题
一旦线程结束,栈就over了!

栈包含:8大基本类型 + 对象引用 + 实例的方法
栈运行原理:栈帧
栈满了:StackOverflowError

栈 堆 方法区 的交互关系

10. 三种JVM

  • Sun公司 HotSpot
  • BEA公司JRockit
  • IBM公司Oracle-J9VM

11. 堆

堆:Heap,一个JVM只有一个堆内存,堆内存的大小是可以调节的
类加载器读取了类文件之后,一般会把什么东西放到堆中?–引用类型的真实对象
堆中存的是对象。栈中存的是基本数据类型和堆中对象的引用

堆中内存可以细分为三个区域:

  • 新生区
  • 养老区
  • 永久区
    JVM快速入门_第3张图片
    GC垃圾回收主要是在伊甸园区和养老区
    假设内存满了,OutOfMemoryError,堆内存不够!
    JDK8以后,永久存储区改了一个名字,元空间

12. 新生区、老年区

新生区:类 诞生和成长的地方,甚至死亡;
新生区又分为:伊甸园区和幸存者区(0区,1区)
所有的对象都是在伊甸园区new出来的

老年区:

13. 永久区

这个区域是常驻内存的。用来存放JDK自身携带的Class对象,Interface元数据,存储的是java运行时的一些环境或类信息这个区域不存在垃圾回收
JDK1.6之前:永久代,常量池在方法区
JDK1.7 :永久代,但慢慢退化了,去永久代,常量池在堆中
JDK1.8之后:无永久代,常量池在元空间

JVM快速入门_第4张图片
JVM快速入门_第5张图片
元空间逻辑上存在,物理上不存在

14. 堆内存调优

OOM如何解决
1.尝试扩大堆内存看结果
2.分析内存,看一下哪个地方出了问题(专业工具)
最直接也是最慢的是 一行一行分析代码
专业内存快照分析工具MAT,Jprofiler能看到代码第几行出错

MAT,Jprofiler的作用:
1.分析Dump内存文件,快速定位内存泄漏
2.获得堆中数据
3.获得大的对象

命令:
-Xms1m -Xmx8m -XX:PrintGCDetails
-Xms8m -Xmx16m -XX:HeapDumpOnOutOfMemoryError
其中:
-Xms 设置初始化内存大小 默认约为总内存1/64
-Xmx 设置最大分配内存大小 默认约为总内存1/4
-XX:PrintGCDetails 打印GC垃圾回收信息
-XX:HeapDumpOnOutOfMemoryError OOM Dump

15. GC:垃圾回收机制

垃圾回收只存在于堆和方法区中

JVM在进行GC时,并不是对三个区域统一回收,99%都在新生代

  • 新生区
  • 幸存区(from to)
  • 老年区
    GC分两类:轻GC,重GC

15.1 常用算法

标记清除法,标记压缩法,复制算法,引用计数法
引用计数法
对每一个对象引用次数进行标记
JVM快速入门_第6张图片

复制算法
幸存区谁空谁是to
JVM快速入门_第7张图片

好处:没有内存碎片
坏处:浪费了内存空间:多了一半空间永远是空to。假设对象100%存活(极端情况)
复制算法最佳使用场景:对象存活度较低的时候,即在新生区中

标记清除法
JVM快速入门_第8张图片
优点:不需要额外空间
缺点:两次扫描严重浪费时间,会产生内存碎片

标记压缩法
对标记清除法优化,防止内存碎片
JVM快速入门_第9张图片
总结:
内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 > 标记清除算法
内存利用率:标记压缩算法 > 标记清除算法 > 复制算法

最合适的算法
年轻代:存活率低,使用复制算法
老年代:存活率高,使用标记清除+标记压缩混合实现

16. JMM

17. 总结

常见的关于JVM的面试题

1 : JVM模型?
参考本文 2. JVM体系结构

2 :请你谈谈你对JVM的理解?
JVM是运行 Java 字节码的虚拟机。JVM有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

3 :Java8虚拟机的更新
撤销了永久带,引入了元空间:
=在HotSpot虚拟机中,jkd1.6时,设计团队把方法区设计为永久带,这样GC工作区域就可以扩展至方法区。这种策略可以可以避免为方法区单独设计垃圾回收机制,但是坏处就是,方法区的回收条件十分苛刻,而且回收效果也不好。
=在jdk1.7版本,设计团队也意识到这个问题,但是只将方法区中的字符串常量池移除永久带。
=到了最新的jdk1.8版本,就不再有永久带这个概念,并且用元空间来代替原来的永久代。
=元空间内的规则:元空间中类及其相关的元数据和类加载器生命周期一致,每个类加载器有专门的存储空间,不会单独回收某个类,位置也是固定的,但是当类加载器不再存活时会把它相关的空间全部移除。

4 :什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
OOM是OutOfMemoryError内存溢出错误,一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时会报错
StackOverFlowError是函数调用栈太深了,注意代码中是否有了循环调用方法而无法退出的情况

对于OOM问题,快照抓取,或扩大JVM内存
内存快照如何抓取?怎么分析Dump文件?
用JDK自带的jmap导出dump文件,导出hprof文件后,使用MAT进行内存镜像分析
用Xmx调整最大内存

对于栈溢出问题
1) 修复引发无限递归调用的异常代码, 通过程序抛出的异常堆栈,找出不断重复的代码行,按图索骥,修复无限递归 Bug。
2) 排查是否存在类之间的循环依赖。
3) 排查是否存在在一个类中对当前类进行实例化,并作为该类的实例变量。
4) 通过 JVM 启动参数 -Xss 增加线程栈内存空间, 某些正常使用场景需要执行大量方法或包含大量局部变量,这时可以适当地提高线程栈空间限制,例如通过配置 -Xss2m 将线程栈空间调整为 2 mb。

5 :JVM常用调优参数有哪些?
Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,大家可以通过选项"-Xmx"和"-Xms"来进行设置。
“-Xms"用于表示堆区的起始内存,等价于-xx:InitialHeapSize
“-Xmx"则用于表示堆区的最大内存,等价于-XX:MaxHeapSize
一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出outofMemoryError异常。
通常会将-Xms和-Xmx两个参数配置相同的值,其目的是 为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

6 :谈谈JVM中,你对类加载器认识?
1) 根类加载器(bootstrap class loader):它用来加载 Java 的核心类,是用原生代码来实现的,并不继承自 java.lang.ClassLoader(负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类)。
2)扩展类加载器(extensions class loader):它负责加载JRE的扩展目录,lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类。由Java语言实现,父类加载器为null。
3)系统类加载器(system class loader):被称为系统(也称为应用)类加载器,它负责在JVM启动时加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径。程序可以通过ClassLoader的静态方法getSystemClassLoader()来获取系统类加载器。

7 :新生区、老年区的转换过程,如何到老年区?
1)对象优先在Eden中分配,当Eden中没有足够的空间分配时会促发一次Minor GC。每次Minor GC结束后,Eden区会清空,因为它会把Eden中还依然存活的对象放到Survivor中,当Survivor中放不下时,则由分派担保进入老年代中。
2)大对象直接进入老年代中。-XX:+PretenuerSizeThreshold 控制”大对象的“的大小。即当创建的对象大于这个临界值时,则该对象直接进入老年代。
3)长期存活的对象将进入老年代。虚拟机对每个对象定义了一个对象年龄(Age)计数器。当年龄增加到一定的临界值时,就会晋升到老年代中,该临界值由参数:-XX:MaxTenuringThreshold来设置。如果对象在Eden出生并在第一次发生Minor GC时仍然存活,并且能够被Survivor中所容纳的话,则该对象会被移动到Survivor中,并且设Age=1;以后每经历一次Minor GC,该对象还存活的话会被移动到另一个Survivor区中,并且Age=Age+1。
动态对象年龄判定:如上所示,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

8 :JVM中垃圾回收的算法?
1)复制算法:时间效率较高、空间利用率低、不会产生空间碎片。主要操作是复制“活对象”,故适合于每次回收时大量对象死去只有少量存活的情况(年轻代)
2)标记-清除算法:时间效率低、空间利用率高、会产生空间碎片。主要操作是清除“死对象”,故适合于回收时大量对象仍存活只有少数死去的情况(老年代)
3)标记-整理算法。时间效率低、空间利用率高、不会产生空间碎片。适用情况同上
4)分代收集算法:就是上面几种用在堆的不同的分代区域中。年轻代:复制算法;老年代:标记-清除算法、标记-整理算法。

9 : 一次完整的GC过程?

10 :new一个对象的在JVM中的整体过程?
1)类加载检查:具体来说,当 Java 虚拟机遇到一条字节码 new 指令时,它会首先检查根据 class 文件中的常量池表(Constant Pool Table)能否找到这个类对应的符号引用,然后去方法区中的运行时常量池中查找该符号引用所指向的类是否已被 JVM 加载、解析和初始化过

  • 如果没有,那就先执行相应的类加载过程
  • 如果有,那么进入下一步,为新生对象分配内存

2)分配内存:就是在堆中给划分一块内存空间分配给这个新生对象用。具体的分配方式根据堆内存是否规整有两种方式:

  • 堆内存规整的话采用的分配方式就是指针碰撞:所有被使用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,分配内存就是把这个指针向空闲空间方向挪动一段与对象大小相等的距离
  • 堆内存不规整的话采用的分配方式就是空闲列表:所谓内存不规整就是已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,JVM 就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的连续空间划分给这个对象,并更新列表上的记录,这就是空闲列表的方式

3)初始化零值:对象在内存中的布局可以分为 3 块区域:对象头、实例数据和对齐填充,对齐填充仅仅起占位作用,没啥特殊意义,初始化零值这个操作就是初始化实例数据这个部分,比如 boolean 字段初始化为 false 之类的
4)设置对象头:这个步骤就是设置对象头中的一些信息
5)执行 init 方法:最后就是执行构造函数,构造函数即 Class 文件中的 () 方法,一般来说,new 指令之后会接着执行 < init >() 方法,按照构造函数的意图对这个对象进行初始化,这样一个真正可用的对象才算完全地被构造出来了

11 :软引用、弱引用、强引用、虚引用
1) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。
2) 如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
3)弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
4)虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

参考:
【狂神说Java】JUC并发编程最新版通俗易懂
Java8新特性之虚拟机的改变
JVM
StackOverFlowError(栈溢出)
OOM分析
jvm之java类加载机制和类加载器(ClassLoader)的详解
新生代 怎么转移到老年代
JVM内存垃圾回收方法
高频八股:new 一个对象在堆中的历程
Java基础篇 - 强引用、弱引用、软引用和虚引用
java对象的强引用,软引用,弱引用和虚引用

你可能感兴趣的:(java,java)