Java虚拟机(JVM)

面试常问

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

1、JVM体系结构

在这里插入图片描述
99%的JVM调优都是在堆中,Java栈、本地方法栈、程序计数器是不会有垃圾存在的。

2、类加载器

类加载器负责加载所有的类,其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。

JVM预定义有三种类加载器,当一个 JVM启动的时候,Java开始使用如下三种类加载器:

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()来获取系统类加载器。如果没有特别指定,则用户自定义的类加载器都以此类加载器作为父加载器。由Java语言实现,父类加载器为ExtClassLoader。

类加载器加载Class大致要经过如下8个步骤:

  1. 检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
  2. 如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
  3. 请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
  4. 请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
  5. 当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
  6. 从文件中载入Class,成功后跳至第8步。
  7. 抛出ClassNotFountException异常。
  8. 返回对应的java.lang.Class对象。

3、类加载机制

JVM的类加载机制主要有如下3种。

  • 全盘负责:所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
  • 双亲委派:所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
  • 缓存机制。缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

4、双亲委派机制

在这里插入图片描述

工作原理

  1. 类加载器收到类加载的请求
  2. 将这个请求向上委托给父类加载器去完成,一 直向上委托,知道启动类加载器
  3. 启动加载器检查是否能够加载当前这个类,能加载就结束, 使用当前的加载器,否则, 抛出异常,通知子加载器进行加载
  4. 重复步骤3
    即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成。

双亲委派机制的优势
Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改

5、沙箱安全机制

什么是沙箱?

Java安全模型的核心就是Java沙箱(sandbox),什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。沙箱主要限制系统资源访问,那系统资源包括什么?——CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。
在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱Sandbox)机制。如下图所示JDK1.0安全模型
在这里插入图片描述
但如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略,允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型
在这里插入图片描述
在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示
在这里插入图片描述

当前最新的安全机制实现,则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain),对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示最新的安全模型(jdk 1.6)
在这里插入图片描述
组成沙箱的基本组件

  • 字节码校验器(bytecode verifier) :确保Java类文件遵循Java语言规范。这样可以帮助Java程序实现内存保护。但并不是所有的类文件都会经过字节码校验,比如核心类。
  • 类裝载器(class loader) :其中类装载器在3个方面对Java沙箱起作用
    • 它防止恶意代码去干涉善意的代码,如双亲委派机制
    • 它守护了被信任的类库边界,如双亲委派机制
    • 它将代码归入保护域,确定了代码可以进行哪些操作,如沙箱安全机制
  • 存取控制器(access controller) :存取控制器可以控制核心API对操作系统的存取权限,而这个控制的策略设定,可以由用户指定。
  • 安全管理器(security manager) : 是核心API和操作系统之间的主要接口。实现权限控制,比存取控制器优先级高。
  • 安全软件包(security package) : java.security下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    • 安全提供者
    • 消息摘要
    • 数字签名
    • 加密
    • 鉴别

6、Native关键字

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

7、本地方法栈(Native Method Stack)

它的具体做法是Native Method Stack中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。[本地库]

8、本地接口(Native Interface)

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序, Java在诞生的时候是C/C++横行的时候,想要立足,必须有调用C、C++的程序,于是就在内存中专门开辟了块区域处理标记为native的代码,它的具体做法是在Native Method Stack 中登记native方法,在( Execution Engine )执行引擎执行的时候加载Native Libraies。

9、PC寄存器

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

10、方法区( Method Area)

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

  • 方法区在JVM启动时就会被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可拓展
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误:java.lang.OutOfMemoryError:PermGen space 或者 java.lang,OutOfMemoryError:Metaspace,比如:关闭JVM就会释放这个区域的内存
  • 在jdk7及以前,习惯上把方法区称为永久代,jdk8开始,使用元空间取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存
  • 本质上,方法区和永久代并不等价。仅是对hotSpot而言的。《java虚拟机规范》对如何实现方法区,不做统一要求。例如:BEA JRockit/IBM J9中不存在永久代的概念
  • 使用永久代,会导致Java程序更容易OOM(超过-XX:MaxPermSize上限)

11、栈( Stack)

栈又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
栈遵循 先入后出(LIFO,last-in-first-out) 的原则。
对栈的操作主要有两种,一是将一个元素压入栈,push方法,另一个就是将栈顶元素出栈,pop方法。

  • 栈内存的生命周期和线程同步。
  • 线程结束,栈内存也就会释放(栈空)
  • 对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就结束
  • 栈是线程级的,每个线程拥有独自的栈,相互隔离。而堆是共享的

栈运行原理:栈帧
在这里插入图片描述
堆、栈、方法区:
在这里插入图片描述

12、堆(Heap)

堆(Heap)又被称为优先队列(Priority Queue),是计算机科学中一类特殊的数据结构的统称。堆通常是一个可以被看做一棵树的数组对象。在队列中,调度程序反复提取队列中第一个作业并运行,因而实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。

一个JVM只有一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,一般会把什么东西放到堆中?类, 方法,常量,变量~,保存我们所有引用类型的真实对象。

堆内存中分为三个区域:

  • 新生区(伊甸园区) Young/New
  • 养老区old
  • 永久区Perm

Tips:

  • 幸存者0区和幸存者1区是动态的,不停更换的
  • GC垃圾回收,主要在伊甸园区和养老区
  • GC垃圾回收分为轻量级和重量级:轻量级针对新生区、重量级针对养老区
  • 假设内存满了,堆内存不够,会报java.lang.OutOfMemoryError:Java heap space(OOM)
  • 在JDK8之后,永久存储区有另外一个名字:元空间。

在这里插入图片描述

GC垃圾回收,主要是在伊甸园区和养老区,假设内存满了,OOM,堆内存不够! java.lang.OutOfMemoryError:Java heap space

永久存储区里存放的都是Java自带的 例如lang包中的类 如果不存在这些,Java就跑不起来,在JDK8以后,永久存储区改了个名字(元空间)
堆内存溢出

13、新生区、老年区、永久区

新生区

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

重GC和轻GC
伊甸园满了就触发轻GC,经过轻GC存活下来的就到了幸存者区,幸存者区满之后意味着新生区也满了,则触发重GC,经过重GC之后存活下来的就到了养老区。

永久区
这个区域常驻内存,用来存放JDK自身携带的Class对象,Interface元数据,存储的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收。关闭虚拟机的时候才会释放这个区域的内存。

当一个启动类加载了大量的第三方jar包、Tomcat部署了太多的应用、大量动态生成的反射类不断的被加载,直到内存满,就会出现OOM。

  • JDK1.6之前:永久代(Perm),常量池在方法区中
  • JDK1.7:永久代(Perm),但是慢慢的退化了,提出了去永久代的概念,常量池在堆中
  • JDK1.8之后:无永久代(Perm),常量池在元空间

在这里插入图片描述

14、堆内存调优

测试代码:

public static void main(String[] args) {
    String s = "";
    while (true) {
        s += "11111111111111111111111111111111111111111111111111111";
    }
}

在这里插入图片描述
在这里插入图片描述
在一个项目中,突然出现了OOM故障,那么该如何排除 研究为什么出错~

  • 能够看到代码第几行出错:内存快照分析工具,MAT, Jprofiler
  • Dubug, 一行行分析代码!

MAT, Jprofiler的作用:

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

Jprofile使用:

  1. 在idea中下载jprofile插件
  2. 联网下载jprofile客户端
  3. 在idea中VM参数中写参数 -Xms1m -Xmx8m -XX: +HeapDumpOnOutOfMemoryError
  4. 运行程序后在jprofile客户端中打开找到错误 告诉哪个位置报错

命令参数详解:

// -Xms设置初始化内存分配大小/164
// -Xmx设置最大分配内存,默以1/4
// -XX: +PrintGCDetails // 打印GC垃圾回收信息
// -XX: +HeapDumpOnOutOfMemoryError // oom DUMP

15、垃圾回收机制(GC)

垃圾回收就是释放垃圾占用的空间,防止内存泄露。有效的使用可以使用的内存,对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。

GC常用算法

  • 复制算法

在这里插入图片描述
复制算法将内存划分为两个区间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空间区间)则是空闲的。

复制算法采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会的将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。

复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。所以复制算法的使用场景,必须是对象的存活率非常低才行,而且最重要的是,我们需要克服50%内存的浪费。

  • 标记 - 清除算法

在这里插入图片描述

标记-清除算法采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收,如上图。

标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。

  • 标记 - 压缩算法

在这里插入图片描述
标记-压缩算法采用 标记-清除 算法一样的方式进行对象的标记、清除,但在回收不存活的对象占用的空间后,会将所有存活的对象往左端空闲空间移动,并更新对应的指针。标记-压缩算法是在标记-清除 算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收(Minor GC)主要采用复制算法。而对于老年代的回收(Major GC),大多采用标记-压缩算法

总结:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值