0. JVM结构图

3.png


1. 类加载器

image.png

1.1 类加载的五个过程

  • 加载:类加载器获二进制字节流,将静态存储结构转化为方法区的运行时数据结构,并生成此类的Class对象。
  • 验证:验证文件格式、元数据、字节码、符号引用,确保Class的字节流中包含的信息符合当前虚拟机的要求。
  • 准备:为类变量分配内存并设置其初始值,这些变量使用的内存都将在方法区中进行分配。
  • 解析:将常量池内的符号引用替换为直接引用,包括类或接口的解析、字段解析、类方法解析、接口方法解析。
  • 初始化:执行类中定义的Java程序代码(字节码)。

image.png

类的生命周期(7个):加载、验证、准备、解析、初始化、使用、卸载

1.2 Java类加载器及如何加载类

类加载器是实现通过一个类的全限定名来获取描述此类的二进制文件流的代码模块。
类的加载是通过双亲委派模型来完成的,双亲委派模型即为下图所示的类加载器之间的层次关系。
image.png

1.3 双亲委派机制

当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,
如果上级的类加载器没有加载,自己才会去加载这个类。

类加载器的类别
BootstrapClassLoader(启动类加载器)
c++编写,加载java核心库 java.*,构造ExtClassLoaderAppClassLoader。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作

ExtClassLoader (标准扩展类加载器)
java编写,加载扩展库,如classpath中的jrejavax.*或者
java.ext.dir 指定位置中的类,开发者可以直接使用标准扩展类加载器。

AppClassLoader(系统类加载器)
java编写,加载程序所在的目录,如user.dir所在的位置的class

CustomClassLoader(用户自定义类加载器)
java编写,用户自定义的类加载器,可加载指定路径的class文件

源码分析

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // 首先检查这个classsh是否已经加载过了
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. // c==null表示没有加载,如果有父类的加载器则让父类加载器加载
  11. if (parent != null) {
  12. c = parent.loadClass(name, false);
  13. } else {
  14. //如果父类的加载器为空 则说明递归到bootStrapClassloader了
  15. //bootStrapClassloader比较特殊无法通过get获取
  16. c = findBootstrapClassOrNull(name);
  17. }
  18. } catch (ClassNotFoundException e) {}
  19. if (c == null) {
  20. //如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
  21. long t1 = System.nanoTime();
  22. c = findClass(name);
  23. sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
  24. sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
  25. sun.misc.PerfCounter.getFindClasses().increment();
  26. }
  27. }
  28. if (resolve) {
  29. resolveClass(c);
  30. }
  31. return c;
  32. }
  33. }

委派机制的流程图
23.jpg
双亲委派机制的作用
1、防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
2、保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。


2.执行引擎


3.本地库接口


4.运行时数据区

4.1 JVM内存结构

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域。JDK. 1.8 和之前的版本略有不同,下面会介绍到。
JDK 1.8 之前:
image.png

JDK 1.8 :
image.png

a. 堆 Heap

Java虚拟机所管理的内存中最大的一块,唯一的目的是存放对象实例。由于是垃圾收集器管理的主要区域,因此有时候也被称作GC堆。

Java世界中“几乎”所有的对象都在堆中分配,但是,随着JIT编译期的发展与逃逸分析技术逐渐成熟,
栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。
从jdk 1.7开始已经默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存。

Java 堆是垃圾收集器管理的主要区域,因此也被称作
GC 堆(Garbage Collected Heap).从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。进一步划分的目的是更好地回收内存,或者更快地分配内存。**

JDK 7 版本之前,堆内存被通常被分为下面三部分:

  1. 新生代内存(Young Generation)
    1. 伊甸园区 Eden
    2. 幸存0区
    3. 幸存1区
  2. 老生代(Old Generation)
  3. 永生代(Permanent Generation) JDK8中已经被移除了,取而代之是元空间, 元空间使用的是直接内存

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

  • JDK 1.6及之前: 永久代, 常量池是在方法区
  • JDK 1.7: 永久代, 但是慢慢的退化了, 去永久代, 常量池在堆中
  • JDK 1.8及之后, 无永久代, 常量池在元空间


为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

  1. 整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。

    当你元空间溢出时会得到如下错误: java.lang.OutOfMemoryError: MetaSpace 你可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

  2. 元空间里面存放的是类的元数据,这样加载多少类的元数据就不由 MaxPermSize 控制了, 而由系统的实际可用空间来控制,这样能加载的类就更多了。

  3. 在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了

b. 方法区 Method Area

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

JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小

  1. -XX:PermSize=N //方法区 (永久代) 初始大小
  2. -XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError
  3. 异常:java.lang.OutOfMemoryError: PermGenCopy to clipboardErrorCopied

相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。

JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
下面是一些常用参数:

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小Copy to clipboardErrorCopied

与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

运行时常量池

运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有常量池表(用于存放编译期生成的各种字面量和符号引用)


c. 程序计数器 Program Counter Register

当前线程所执行字节码的行号指示器, 就是用来存储指向下一条指令的地址。
每一个线程都有一个独立的程序计数器,线程的阻塞、恢复、挂起等一系列操作都需要程序计数器的参与,
因此必须是线程私有的。

image.png

d. Java虚拟机栈 VM Stack

用于描述Java方法执行的模型。
每个方法在执行的同时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用至执行完成,对应于一个栈帧在虚拟机栈中从入栈到出栈。
image.png

e. 本地方法栈 Native Method Stack

与虚拟机栈作用相似,只不过虚拟机栈为执行Java方法服务,而本地方法栈为执行Native方法服务,
比如在Java中调用C/C++。

f. 非堆 Non-Heap

Non-Heap 本质上还是一个Heap, 只是一般不归GC管理, 里面划分为3个内存池

元空间 Metaspace
元空间并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。而且也可能导致 OutOfMemoryError错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel) 与缓存区(Buffer) 的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。

本机直接内存的分配不会受到 Java 堆的限制,但是,
既然是内存就会受到本机总内存大小以及处理器寻址空间的限制。

CCS Compressed Class Space
存放lcass信息的, 和Metaspace有交叉

Code Cache
存放JIT 编译器编译后的本地机器代码