JVM是现在面试中的常客,多是些概念性的东西需要把它记住,这个时候可不能拉跨!

先来几道题练练手,心里有个数

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

懵逼了吧,那就来系统学习一波!

在学习的过程中会遇到很多抽象的概念,需要我们结合图式梳理分析,参考下面的网站:

JVM的位置

JVM的体系结构

image-20200502100551288

image-20200502102116094

类加载器

作用

​ 加载Class文件

图示

image-20200502104222975

分类

  • 虚拟机自带的加载器

  • 启动(根)加载器(BootStrap)

    不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类

  • 扩展类加载器(Extension)

  • 应用程序(系统)加载器(System)

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {

private String name;

public static void main(String[] args) {
Person person = new Person();
Class<? extends Person> aClass = person.getClass();
System.out.println(aClass);
System.out.println(aClass.getClassLoader()); //AppClassLoader
System.out.println(aClass.getClassLoader().getParent()); //ExtClassLoader
System.out.println(aClass.getClassLoader().getParent().getParent());
//根加载器,java程序获取不到
}
}

image-20200502105839415

双亲委派机制

描述

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

AppClassLoader——>ExtClassLoader——>Bootstrap(最终执行)

目的

保证安全性

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
package java.lang;

public class String {

public String toString() {
return "Hello";
}

public static void main(String[] args) {
String s = new String();
s.toString();
}
}

执行结果程序会报错:在类 java.lang.String 中找不到 main 方法

分析:程序不会先执行在自己创建的类中的方法,而是先委托给其父类加载器,最后在 jdk 自带的 java.lang找到要完成的类加载任务,会选择该类执行而不是自己创建的类,进而由于找不到 main 方法而报错。

  • 类加载器收到类加载请求
  • 将这个请求向上委托给父类加载器完成,一直向上委托直到根类加载器
  • 启动加载器检查是否能够加载当前这个类,能加载就结束,使用当前的类加载器,否则就抛出异常,通知子加载器进行加载
  • 重复上一步

能不能自己写个类叫java.lang.System

答案:通常不可以,但可以采取另类方法达到这个需求。
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。

但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

沙箱安全机制

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

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

https://www.cnblogs.com/MyStringIsNotNull/p/8268351.html

Native

1
2
3
4
5
6
7
8
9
10
//简化版
public synchronized void start() {

boolean started = false;
//在调用该方法后线程启动成功
start0();
started = true;
}

private native void start0();

native关键字说明其修饰的方法是一个原生态方法,方法对应的实现不是在当前文件,而是在用其他语言(如C和C++)实现的文件中。Java语言本身不能对操作系统底层进行访问和操作,但是可以通过JNI接口调用其他语言来实现对底层的访问。

JNI是Java本机接口(Java Native Interface),是一个本机编程接口,它是Java软件开发工具箱(java Software Development Kit,SDK)的一部分。JNI允许Java代码使用以其他语言编写的代码和代码库。Invocation API(JNI的一部分)可以用来将Java虚拟机(JVM)嵌入到本机应用程序中,从而允许程序员从本机代码内部调用Java代码。

  • JNI作用:扩展Java的使用,融合不同的编程语言为Java所用

  • 在最终执行的时候,通过 JNI 加载本地方法库中的方法

PC寄存器

程序计数器是一个记录着当前线程所执行的字节码的行号指示器。

JAVA代码编译后的字节码在未经过JIT(实时编译器)编译前,其执行方式是通过“字节码解释器”进行解释执行。简单的工作原理为解释器读取装载入内存的字节码,按照顺序读取字节码指令。读取一个指令后,将该指令“翻译”成固定的操作,并根据这些操作进行分支、循环、跳转等流程

方法区

方法区是被所有线程所共享的,所有字段、方法字节码、特殊方法(构造函数、接口代码)等在此定义,属于共享空间

  • 静态变量:static
  • 常量:final
  • 类信息:Class
  • 常量池

程序员学习思想:

​ 程序 = 数据结构 + 算法 √ (持续学习)

​ 程序 = 框架 + 业务逻辑 ×(很容易被淘汰,吃饭用的)

一个小问题:为什么 main 方法先执行,最后结束?

一个图解释清楚:image-20200502115532577

再想想递归调用出现栈溢出是怎么肥四?

image-20200502115354355

对于栈来说,不存在垃圾回收问题

栈存放的内容

  • 8大基本类型
  • 对象引用
  • 实例的方法

image-20200502120644868

类实例化的过程

image-20200502122508381

三种JVM

了解即可

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

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

类加载器读取了类文件后,一般会把什么东西放在堆中?

  • 方法
  • 常量
  • 变量

保存引用类型的真实对象

堆内存中还要细分为三个区域:

  • 新生区(伊甸园区)
  • 养老区
  • 永久区
image-20200502143710520

GC垃圾回收主要是在伊甸园区和养老区!

堆内存满了会爆出 OutOfMemoryError:Java heap space

JDK8以后,永久区——>元空间

新生区、老年区

新生区

对象诞生、成长甚至死亡的地方

  • 伊甸园区

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

  • 幸存区

    • 0区
    • 1区

经过研究,99%的对象都是临时对象!

永生区(元空间)

这个区域是常驻的,用来存放 JDK 自身携带的 Class 对象。Interface 元空间,存储的是Java运行时的一些环境或类信息,这个区域不存在 GC。关闭 JVM 时这个区域的内存被释放。

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

堆内存调优

image-20200503161549133

Dump文件:

使用命令 -XX:HeapDumpOnxxxError-XX:HeapDumpOnxxxException

可以生成关于异常的文件,使用插件 Jprofilter 可以对其进行分析

排除OOM错误故障的方法:

  • 能够看到代码第几行出错:内存快照分析工具,MAT(Eclipse)、Jprofiler(IDEA)
  • Debug

MAT、Jprofiler作用

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

GC:垃圾回收

一些GC的问题

  • JVM的内存模型和分区(详细到每个区放什么)
  • 堆里面的分区有哪些?Eden、from、to、老年区,说说他们的特点
  • GC算法有哪些?怎么用?
  • 轻GC 和 重GC 分别在什么时候发生

GC的作用区域

image-20200503194655724

JVM在进行GC时,并不是对新生代、幸存区(from、to)、老年去进行统一回收,大部分回收的都是新生代

GC的分类

  • 轻GC(普通GC)
  • 重GC(全局GC)

GC的算法

1.引用计数法

每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。

2.复制算法

image-20200503200832689

当一个对象经历了 15次 GC还没有被清除,就会进入老年代。

(可以使用参数 -XX:MaxTenuringThreshold=?来进行调节)

  • 好处:没有内存碎片
  • 坏处:浪费了空间
  • 使用场景:对象存活度较低的区域—新生区。

3.标记清除算法

image-20200503203352254

  • 优点:不需要浪费额外的空间
  • 缺点:两次扫描,严重浪费时间,会产生内存碎片

4.标记压缩算法

对标记清除算法的优化

image-20200503203646419

总结

内存效率

复制算法 > 标记清除算法 > 标记压缩算法

内存整齐度

复制算法 = 标记压缩算法 > 标记清除算法

内存利用率

标记压缩算法 = 标记清除算法 > 复制算法

  • 思考:难道没有最优的算法嘛?
  • 答案:莫得,只有最合适的
  • GC:分代收集算法

完结撒花~~~~~~~~~

狂神,永远滴神!!!

完了之后抽空深入了解JVM,看《深入理解JVM原理》

参考

狂神说Java:https://www.bilibili.com/video/BV1iJ411d7jS

程序媛想事儿:https://www.cnblogs.com/lanxuezaipiao/p/4138511.html

不止吧:https://www.cnblogs.com/b3051/p/7484501.html

re-phoenix:https://www.cnblogs.com/manayi/p/9290490.html

春_:https://blog.csdn.net/weixin_43736084/article/details/103937547

纯洁的微笑:https://www.cnblogs.com/ityouknow/p/5614961.html