shanX
文章31
标签17
分类9
JAVA-JVM基础

JAVA-JVM基础

JVM 探究

常见面试问题

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

  2. jvm 的体系结构

  3. 类加载器

  4. 双亲委派机制

  5. 沙箱安全机制

  6. Native

  7. PC 寄存器

  8. 方法区

  9. 三种 JVM

    java -version 查看 :

    Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)

  10. 新生区、老年区

  11. 永久区

  12. 堆内存调优

  13. GC

    1. ​ 常用算法
  14. JMM

JVM 体系结构

JVM架构图

双亲委派机制

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

以下为其他资料:

你得先知道
在介绍双亲委派机制的时候,不得不提 ClassLoader(类加载器)。说 ClassLoader 之前,我们得先了解下 Java 的基本知识。
Java 是运行在 Java 的虚拟机(JVM)中的,但是它是如何运行在 JVM 中了呢?我们在 IDE 中编写的 Java 源代码被编译器编译成.class 的字节码文件。然后由我们得 ClassLoader 负责将这些 class 文件给加载到 JVM 中去执行。
JVM 中提供了三层的 ClassLoader:

Bootstrap classLoader:主要负责加载核心的类库(java.lang.*等),构造 ExtClassLoader 和 APPClassLoader。

ExtClassLoader:主要负责加载 jre/lib/ext 目录下的一些扩展的 jar。

AppClassLoader:主要负责加载应用程序的主函数类

那如果有一个我们写的 Hello.java 编译成的 Hello.class 文件,它是如何被加载到 JVM 中的呢?别着急,请继续往下看。

双亲委派机制
搜索“ClassLoader”,然后打开“java.lang”包下的 ClassLoader 类。然后将代码翻到 loadClass 方法:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}
//              -----??-----
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 首先,检查是否已经被类加载器加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 存在父加载器,递归的交由父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 直到最上面的Bootstrap类加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

其实这段代码已经很好的解释了双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:

20201217213314510

从上图中我们就更容易理解了,当一个 Hello.class 这样的文件要被加载时。不考虑我们自定义类加载器,首先会在 AppClassLoader 中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的 loadClass 方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达 Bootstrap classLoader 之前,都是在检查是否加载过,并不会选择自己去加载。直到 BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出 ClassNotFoundException。那么有人就有下面这种疑问了?

为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被 Bootstrap classLoader 加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是 BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

总结了一张脑图如下:

2020121722082798

  • 双亲委派模型是每次收到类加载请求时,先将请求委派给父类加载器完成,如果父类加载器无法完成加载,那么子类尝试自己加载。
  • 双亲委派机制可以避免加载子类自定义的 Object 类、String 类等一些跟 jdk 命名相同的类。使得加载的类都是同一个。这样才安全。

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载其中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的 Class),子类加载器才会尝试自己去加载。

沙箱安全机制

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

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

在 java 中将执行程序分为本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信任的。对于授信的本地代码,可以访问一切本地资源,而对于非授信的代码远程在早期的 java 实现中,安全依赖于沙箱机制,如下图所示 JDK 1.0 安全模型:

shaxiang1

在 java 1.2 版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制,如下图所示:

JDK 1.2 安全模型

shaxiang1

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

shaxiang1

组成沙箱的基本组件:

  • 字节码校验器(bytecode verifier):确保 java 类文件遵循 java 语言规范,这样可以帮助 java 程序实现内存保护,但并不是所有的类文件都会经过字节码校验,比如核心类;
  • 类装载器(class loader):其中类装载器在 3 个方面对 java 沙箱起作用;
    • 他防止恶意代码去干涉善意的代码;
    • 他守护了被信任的类库边界;
    • 它将代码归入保护域,确定了代码可以进行哪些操作。

虚拟机为不同的类加载器载入的类提供不同的命名空间,命名空间由一系列唯一的名称组成,每一个被装载的类将有一个名字,这个命名空间是由 java 虚拟机为每一个类装载器维护的,他们互相之间甚至不可见。

​ 类装载器采用的机制是双亲委派模式。

  1. 从最内层 JVM 自带类加载器开始加载,外层恶意同名类得不到加载从而无法使用;
  2. 由于严格通过包来区分了访问域,外层恶意的类通过内置代码也无法获得权限访问到内层类,破坏代码就自然无法生效。
  3. 存取控制器 (access controller) : 存取控制器可以控制核心 API 对操作系统的存取权限, 而这个控制的策略设定,可以由用户指定。
  4. 安全管理器(security manger):是核心 API 和操作系统之间的主要接口,实现权限控制,比存取控制器优先级高。
  5. 安全软件包(security package):java.security 下的类和扩展包下的类,允许用户为自己的应用增加新的安全特性,包括:
    1. 安全提供者;
    2. 消息摘要;
    3. 数字签名; keytools
    4. 加密;
    5. 鉴别;

Native

  • native : 凡是带了 native 关键字的,说明 java 的作用域达不到了,会去调用底层 C 语言的库;
  • 会进入本地方法栈;
  • 调用本地方法接口 JNI;
  • JNI 作用:扩展 java 的使用,融合不同 java 语言为 java 所用! 最初:C, C++;
  • java 诞生的时候 C, C++横行,想要立足,你需要有调用 C, C++ 的程序;
  • 它在内存区域中专门开辟了一块标记区域:Native Method Stack, 登记 Native 方法;
  • JAVA 程序驱动打印机,管理系统!掌握即可,在企业应用中较为少见;
  • 调用其他接口:Socket WebService http

Native Method Stack

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

PC 寄存器

程序计数器:Program Counter Register

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

方法区

Method Area 方法区

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

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

方法区就存这些内容 static final Class 常量池

栈:数据结构

程序 = 数据结构 + 算法

栈:先进后出,后进先出:类似桶

队列:先进先出(FIFO:First Input First Output)

为什么 main()最先执行最后结束

栈:栈内存,主管程序的运行,生命周期和线程同步;

线程结束,栈内存也就是释放,对于栈来说,不存在垃圾回收问题;

一旦线程结束,栈就 Over;

栈 — 存储的内容:8 大基本类型 + 对象引用 + 实例的方法;

站运行原理:栈帧

栈满了:StackOverflowError

栈 + 堆 + 方法区

java 对象实例化的过程 — 百度

shaxiang1

走进 HotSpot 和堆

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

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

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

  • 新生区(伊甸园区) Young/New
  • 老年区 old
  • 永久区 Perm 1.8 取消了 永久区 perm ,变成了 元空间

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

​ 假设内存满了,OOM,堆内存不够!java.lang.OutOfMemoryError:java heap space

在 JDK 8 以后,永久存储区改了个名字(元空间);

新生区

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

新生区 minor gc (轻 GC) 15 次还活着的,放入养老区,养老区满了时 full gc (重 GC)

永久区

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

  • jdk1.6 之前:永久代,常量池在方法区;

  • jdk1.7: 永久代,但是慢慢地退化了,去永久代,常量池在堆中;

  • jdk1.8 之后:无永久代,常量池在元空间;

元空间是在逻辑上存在的,不占用 jvm 内存,而是占用计算机内存;

OOM 故障:

  • 尝试扩大堆内存;
  • 通过专业工具分析内存 ;

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

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

MAT,Jprofiler 作用:

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

通过命令参数运行 jar 包 Jprofiler

-Xms 设置初始化内存分配大小 1/164

-Xmx 设置最大分配内存,默认 1/4

-XX:+PrintGCDetails 打印 GC 垃圾回收信息

-XX:+HeapDumpOnOutOfMemoryError //oom Dump

-Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

GC: 垃圾回收

GC 的作用区域只有堆、元空间、方法区;

jvm 再进行 GC 时,并不是对这三个区域统一回收,大部分时候,回收都是新生代:

  • 新生代
  • 幸存区
  • 老年区

GC 两种类:轻 GC(普通的 GC),重 GC(全局 GC)

题目:

  • JVM 的内存模型和分区-详细到每个分区做什么?
  • 堆里面的分区有哪些?Eden,form to, 老年区,说说他们的特点!
  • GC 的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器;
  • 轻 GC,重 GC 分别在什么时候发生;

引用计数法

shaxiang1

复制算法

shaxiang1

shaxiang1

  • 好处:没有内存碎片;
  • 坏处:浪费了内存空间:多了一半空间永远是空 to,假设对象 100%存活(极端情况)

复制算法最佳使用场景:对象存活度较低的时候;新生区;

标记清除算法

shaxiang1

优点:不需要额外空间;

缺点:两次扫描,严重浪费时间,会产生内存碎片。

标记清除压缩算法

shaxiang1

多了一次扫描;

一般会先多进行几次清除,内存碎片变多了,再进行压缩;

总结

内存效率:复制算法 > 标记清除算法 > 标记压缩算法;

时间复杂度:复制算法 > 标记清除算法 > 标记压缩算法;

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

内存利用率:标记压缩算法 = 标记清除算法 > 复制算法;

分代收集法

年轻代:存活率低,复制算法;

老年代:区域大,存活率高, 标记清除算法 + 标记压缩算法 混合实现

JVM 调优就是调整 程序计数次数(默认 15),加大老年代空间等;

上面仅仅为 JVM 基础

本文作者:shanX
本文链接:https://rhymexmove.github.io/2021/04/26/0aa35a941987/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可