JVM探究

358 阅读15分钟

JVM探究

  • 请你谈谈你对JVM的理解?Java8虚拟机和之前的变化更新?
  • 什么是OOM,什么是栈溢出StackOverFlowError?怎么分析?
  • JVM的常用调优参数有哪些?
  • 内存快照如何抓取,怎么分析Dump文件?
  • 谈谈JVM中,类加载器你的认识?

1. JVM的位置

2. JVM的体系结构

image-20200302235804308.png

其中GC调优99%主要是在方法区和堆中进行。

3. 类加载器

作用:加载Class文件

image-20200302235751587.png

  1. 虚拟机自带的加载器
  2. 启动类(根)加载器 BootstrapClassLoader
    • c++编写,加载java核心库 java.*,构造ExtClassLoader和AppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
  3. 扩展类加载器 ExtClassLoader
    • java编写,加载扩展库,如classpath中的jre ,javax.*或者 java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。
  4. 应用程序加载器 AppClassLoader
    • java编写,加载程序所在的目录,如user,dir所在位置的class
  5. 用户自定义类加载器 CustomClassLoader
    • Java编写,用户自定义的类加载器,可加载指定路径的class文件

比如当你自建java.lang.String类的时候,当你调用自创的String类的方法时候,会直接报错,因为Java会先调用boot,ext两个类加载器,Java自带的String方法会先调用,自己出的就先报错了。

4. 双亲委派机制

  1. 类加载器收到类加载请求
  2. 将这个请求向上委托给父类加载器去完成,一直向上委托,直到启动类(根)加载器
  3. 启动加载器检查是否能够加载当前类,能加载就结束,使用当前的加载器,否则,抛出异常,通知子加载器进行加载
  4. 重复3步骤,直到加载或抛出异常 经典错误:ClassNotFound就是这样来的 getClassLoader 如果返回null 则Java调用不到,因为底层使用C,C++写的

5. 沙箱安全机制

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限制在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问。

其中系统资源包括:CPU,内存,文件系统,网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

6. Native

Java底层代码中,凡是带了native关键字的,说明Java的作用范围达不到,需要回去调用底层的C语言的库!会进入本地方法栈,调用本地方法接口(JNI)

JNI作用:扩展Java的使用。融合不同的编程语言为Java所有!最初:C,C++。

JVM在内存中专门开辟了一块标记区域:Native Method Stack(本地方法栈),作用是登记native方法,在最终执行的时候,加载本地方法库中的方法通过JNI

比如currentTimeMillis方法。

public static native long currentTimeMillis();

或者用Java调用打印机等硬件,管理操作系统等。

现在调用其他语言的接口:Http Rest、Socket、WebService、RPC等

7. PC寄存器

	程序计数器:Pogram Counter Register

	程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型中(仅是概念模型,各种虚拟机会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支,循环,跳转,异常处理,线程恢复等基础功能都需要依赖这个计数器来完成。

	由于Java虚拟机的多线程操作时通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(现在的多核处理器来说应该是一个内核)都会只执行一个线程中的指令,因此为了每个线程切换时可以恢复到正确的执行位置,每个线程都需要一个独立的程序计数器,各个线程之间的计数器互相独立,互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

	如果线程正在执行一个Java方法,这个计数器记录的就是正在执行的虚拟机字节码指令的地址;如果执行Native方法,这个计数器值则为空(Undefined)。此内存区域是唯一一个在Java虚拟机中没有规定任何OutOfMemoryError情况的区域。

8. 方法区

	方法区(Method Area)是被所有线程共享的区域,所有字段和方法字节码,以及一些特殊方法,如构造函数,接口代码也在此定义。简单来说,所有定义的方法的信息都保存在该区域,此区域属于共享区域。

	静态变量、常量、类信息(构造方法,接口定义)、运行时常量池存在在方法区中,但是实际变量存在堆内存中,与方法区无关。

static、final、Class、运行时常量池。说白了就放这四个东西。生成的对象在栈中,对象信息由指针指向堆内存中。

9. 栈

当一个新的线程创建时,JVM会为这个线程创建一个新的Stack。一个Java Stack在一个个独立的栈帧中存储了线程的状态。JVM只会在Java Stack中做两个操作:push 和 pop.

	数据结构栈模型:先进后出,后进先出

	和栈相似的由队列,先进先出(FIFO),后进后出

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

	栈中存在的东西:

	栈中存在一个个方法的栈帧,每个线程都有会新建一个栈,如下图,Stack1在栈的顶部,那么他就是正在执行的栈帧。

	注意:Java栈默认大小为1024kb,当栈满了之后就会发生栈溢出StackOverflowError错误。比如递归方法,不注意跳出的判断条件,就会发生栈溢出错误。

image-20200302235731108.png

  1. 8个基本类型;

  2. 对象的引用;

  3. 实例的犯法。

     思考:
    
  4. 为什么main方法先执行后结束?

     正式因为栈的特性,程序的入口是main方法,第一个启动,main方法第一个进入,后续调用其他方法,栈先进后出,所以main第一个启动进入栈,最后一个出,完成。
    
  5. 栈溢出是什么导致的?

10. 三种JVM

  • Sun公司 HopSpot Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)
  • BEA JRockit
  • IBM J9VM 我们学习都是基于HopSpot JVM的,其他的JVM有可能不同。

11. 堆

Heap,一个JVM只有一个堆内存,但是堆内存的大小是可以调节的。

类加载器读取了类文件后,一般会把什么放在堆中?类,方法,常量,变量,保存我们所有引用类型的真实对象。

堆内存细分为三个区域:

  • 新生区 新生区分为伊甸园区和幸存者0区,幸存者1区

  • 养老区

  • 永久区

    image-20200302235717437.png

GC垃圾回收,主要是在伊甸园区和老年区,当方法被应用之后,显示在伊甸园区,轻GC,如果未被回收,会被保存到幸存者0,1区。还未被回收被保存到老年区。在老年区会进行重GC。当老年区也满了之后,GC来不及回收,会抛出错误OutOfMemoryError,堆内存溢出。

内存溢出和内存泄漏区别:

  • 内存溢出:申请内存空间,超出最大堆内存空间。
  • 内存泄露:其实包含内存溢出,堆内存空间被无用对象占用没有及时释放,导致占用内存,最终导致内存泄露。 情况:静态static修饰对象。 解决:减少常量的定义(具体看服务器内存情况)

12. 新生区、老年区

新生区

类:诞生,成长的地方,甚至死亡。

新生区分为伊甸园区、幸存者0区、幸存者1区

  • 伊甸园区 所有的对象都是在伊甸园区new出来的

13. 永久区

永久区是常驻内存的区域,用来存放一些JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息。关闭虚拟机,永久区就会关闭。

一般情况下,永久区不会出现垃圾,也不会出现OOM现象,但是以下情况时,有可能会永久区也会出现内存溢出。

加载了大量的第三方jar包、Tomcat部署了过多的应用、大量动态生成的反射类不断被加载。

  • JDK1.6之前:永久代,常量池在方法区;
  • JDK1.7:永久代,但是慢慢退化了,去永久代,常量池在堆中。
  • JDK1.8:元空间,常量池在元空间。

在JDK8以后,永久存储区改名为元空间,有微小的区别。

方法区其实就是在永久区/元空间中,元空间逻辑上存在于堆中,物理上不存在于堆中。

小结:

堆区:

  1. 存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
  2. jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

栈区:

  1. 每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
  2. 每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
  3. 栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

方法区:

  1. 又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
  2. 方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

14. 堆内存调优

默认情况下,jvm分配的总内存(最大内存)为电脑内存的1/4,而初始化的内存为1/64

而这些配置是可以更改的。

  • -Xms JVM默认内存
  • -Xmx JVM最大内存

在一个项目中,突然出现了OOM故障,name该如何排查,研究为什么出错?

  • 能够看到代码第几行出错,内存快照分析工具—MAT,JProfiler
  • Debug,一行一行找

MAT,JProfiler

内存分析工具

  • 分析Dump文件,快速定位内存泄漏
  • 获得堆中的数据
  • 获得大的对象
  • ......

15. GC

GC的本质是在JVM的堆和方法区这两块进行回收,并且大部分在新生代,小部分在老年代进行GC回收。极少在方法区。

GC分为两种:

  • 轻GC(普通GC、Minor GC) 大部分是在新生代的伊甸园区,偶尔from、to两个区
  • 重GC(Full GC、全局GC) 全局进行GC,包括新生代和老年代

常用算法

  1. 引用计数法

给堆里面每个对象分配一个计数器,计数对象的引用次数

image-20200302235453302.png

  1. 复制算法

关于伊甸园区幸存者区与复制算法的介绍

复制算法主要在新生代中使用,from区和to区,当伊甸园区触发一次GC之后,多余的对象会被垃圾回收掉,然后伊甸园区中还存活的对象,会被放入from区,然后伊甸园区继续生成新对象使用,当伊甸园区第二次满了之后进行GC,存活下来的对象和在from区的对象,全部放到to区,然后from区和to区置换,from区就有存活对象,Eden和to区为空。

所有的对象新生都会在Eden发生,然后当Eden快满了的时候触发轻GC,因为Eden:from:to=8:1:1,所以轻GC的频率会降低。但是缺点是会有10%的空间浪费,空间之间复制对象的开销。

注意的是,轻GC是在新生代三个区进行,因为to区一直为空,所以每次GC,Eden和from区都会触发GC,然后存活的对象进入to区,在轻GC 15 次(默认值可以自己调整)之后,还存活的对象将进入老年代。

  1. 标记清除算法

为每个对象存储一个标记位,记录对象的状态(或者或是死亡)。分为两个阶段,一个是标记阶段,标记阶段为每个对象更新标记位,检查对象是否死亡;第二个阶段是清除阶段,该阶段对死亡的对象进行清除,执行GC操作。

image-20200302235433302.png

优点

不需要额外的空间,不浪费空间

缺点

  1. 两次扫描,严重浪费时间;

  2. 会产生内存碎片。

  3. 标记整理算法

在标记清除算法的基础上,在对堆内的碎片化对象往一头进行移动,整理内存随便。

image-20200302235408976.png

优点:没有内存碎片

缺点:三次循环,浪费大量时间

优化标记清除整理算法

可以先进行多次标记清除,等内存随便多的时候,在进行标记整理。可以适当优化。比如先进行五次标记清除,再进行标记整理一次

总结

内存效率:复制算法 > 标记清除算法 > 标记整理算法

内存整齐度: 复制算法 = 标记整理算法 > 标记清除算法

内存利用率: 标记清除算法 = 标记整理算法 > 复制算法

这三个算法各有优劣,在不同的情况下使用不同的算法,才能更好的优化。

年轻代:

存活率低---> 复制算法

老年代:

区域大,存活率高

标记清除 + 标记整理算法

16. JMM

Java Memory Model: Java内存模型

17. 总结

题目:

  1. JVM的内存模型和分区?详细到每个区放什么。 程序计数器:每个线程私有,当前线程所执行的字节码的行号指示器,用来记录正在执行的虚拟机字节指令位置。 方法区:内部包含常量池,方法区主要存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码数据等。(即永久带),回收目标主要是常量池的回收和类型的卸载,各线程共享。 Java虚拟栈:存放基本数据类型,对象的引用,方法出口等,线程私有。 本地方法栈:和Java虚拟栈类似,只不过他服务于Native本地方法。线程私有。 堆:主要存放所有对象的实例,以及数组等。GC主要作用的区域,线程共享。

  2. 堆里面的分区有哪些?说说他们的特点。

  3. GC算法有哪些?怎么用。

  4. 轻GC,重GC分别在什么时候发生?

  5. 谈谈JVM中,对类加载器的认识 类加载器是JVM的组成部分之一。将字节码文件加载进JVM。 类加载分为四部分: BootStrapClassLoader,即跟类加载器,加载java运行时所需的类,如String,Integer等存在{java_home}/jre/lib/rt.jar包类的所有类。
ExtensionClassLoader,扩展类加载器,加载一些扩展类,即{java_home}/jre/lib/ext/*.jar包 AppClassLoader,系统加载类,加载自定义的类,级classpath下的所有类 ClassLoader 抽象类加载器:用户自定义的类加载器,用户定义的类加载器都要继承次ClassLoader Jvm默认采用的是双亲委派类加载机制,即先加载父类在加载子类,对上面四个类加载器采用自顶向下加载

  6. 在JVM中,如何判断一个对象是否死亡? 判断对象是否死亡有两种方法:1.引用计数法,2.可达性分析算法

      引用计数法是最简单最古老的算法,JVM为每个对象分配一个计数器,当对象被引用时,计数器就加1,当对象没有被引用或者离开作用域,计数器就减1。当计数器的值为0时,就代表该对象已经死亡
    
      可达性分析算法,是用GCROOTs 作为对象的起点开始往下搜索,能搜索到这个对象,就表示对象是可达的,不能搜素到表示对象是不可达的
    
      可作为GCRoots的对象包括下面几种:
    
            虚拟机栈中引用的对象
    
            方法区中类静态属性引用的对象
    
            方法区中常量引用的对象
    
            本地方法栈中引用的对象
    

    未完待续