JDK、JRE、JVM

一、JDK、JRE、JVM 有什么关系?

JDK(Java development toolkit)——相当于是 Java 的库函数,是编译、运行 Java 程序的工具包,是一切 Java 应用程序的基础,所有 Java 应用程序是构建在这个之上的。(汽车)

JRE(Java Runtime Environment)——Java 运行环境,也就是 Java 平台。所有的 Java 程序都要在 JRE 下才能运行。JDK 的工具也是 Java 程序,也需要 JRE 才能运行。(汽车动力系统)

JVM(Java Virtual Machine)——Java 虚拟机,是 JRE 的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM 有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java 语言最重要的特点就是跨平台运行。使用 JVM 就是为了支持与操作系统无关,实现跨平台。(汽车轮胎)

二、JVM 虚拟机

2.1 JVM 基本结构

思维导图(语雀)

2.2 JVM 线程共享区和非线程共享区

概括来说,JVM 初始运行的时候都会分配好 Method Area(方法区) 和Heap(堆) ,而 JVM 每遇到一个线程,就为其分配一个 Program Counter Register(程序计数器) , VM Stack(虚拟机栈)和Native Method Stack(本地方法栈),当线程终止时,三者(虚拟机栈,本地方法栈和程序计数器)所占用的内存空间也会被释放掉

非线程共享的三个区域的生命周期与所属线程相同,而线程共享的区域与 JAVA 程序运行的生命周期相同,所以这也是系统垃圾回收的场所只发生在线程共享的区域(实际上对大部分虚拟机来说只发生在 Heap(堆区)上)的原因。

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class AppMain {
/**
* 运行时,JVM把AppMain的信息都放到方法区
* main方法本身也放入方法区内
* @param args
*/
public static void main(String[] args) {
// test1是引用,所以放到栈区中,而Sample是自定义对象,会被放到堆里
Sample test1 = new Sample("测试1");
Sample test2 = new Sample("测试2");

test1.printName();
test2.printName();
}
}

class Sample {
private String name;

public Sample(String name) {
this.name = name;
}

/**
* printName方法本身放到方法区中
*/
public void printName() {
System.out.println(name);
}
}

2.3 JVM 启动流程(略)

  1. Java 虚拟机启动的命令是通过 java + xxx(类名,这个类中要有 main 方法)或者 javaw 启动的。
  2. 执行命令后,系统第一步做的就是装载配置,会在当前路径中寻找 JVM 的 config 配置文件。
  3. 找到 JVM 的 config 配置文件之后会去定位 jvm.dll 这个文件。这个文件就是 Java 虚拟机的主要实现。
  4. 当找到匹配当前版本的 jvm.dll 文件后,就会使用这个 dll 去初始化 JVM 虚拟机。获得相关的接口。之后找到 main 方法开始运行。

三、Java 执行过程

3.1 解释执行和编译执行

解释执行:一边将程序翻译成计算机可以执行的指令,一边交给计算机执行,翻译一句,执行一句。(饭馆点菜,上一个吃一个)

编译执行:将整个程序翻译成计算机及可以理解的指令,然后交给计算机执行。(饭馆订桌,菜上齐后一并吃)

3.2 Java 的执行过程

Java 严格来讲是一种“半解释半编译”的语言

User.java
↓*(javac源码编译器解释执行,即静态编译)*
User.class 字节码(ByteCode),ByteCode 是 JVM 唯一能够识别的指令,JVM 将 ByteCode 翻译成真正
能够执行的机器码**(PS:在这一步同时还会有一些对我们编写的 Java 源码的优化操作)**
↓*(JVM 编译执行)*
机器码(machine code),这是电脑 CPU 可直接解读的数据,JVM 在不同的硬件平台上有不同实现,以
达到所谓“一次编写,到处运行”的目标

3.3 Java 运行比 C++ 慢?

这种说法是在 JIT 编译器出现之前的情况。

由于 Java 程序最初是仅仅通过解释器解释执行,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。于是后来在虚拟机中引入了JIT 编译器(即时编译器,即时编译被内嵌于 java 字节码执行引擎之中,可以算的上是 jvm 的一个内存组件),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行特别的优化,完成这项任务的正是 JIT 编译器。

3.4 JIT 编译器中的 C1、C2 即时编译器

  • C1 编译:将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。
  • C2 编译:也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。C2 编译主要是针对热点代码,对之做出更优的编译。

注:实施分层编译后,C1 和 C2 将会同时工作,许多代码会被多次编译*,用 C1 获取更高的编译速度,用 C2 来获取更好的编译质量,且在解释执行的时候解释器也无须再承担收集性能监控信息的任务*

四、新生代、老年代、JavaCG 回收机制

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。

堆的内存模型大致为:

从图中可以看出: 堆大小 = 新生代 + 老年代。

JVM 三大性能调优参数: -Xms、-Xmx、-Xss

  • 参数 –Xms 指定堆的默认值(最小值),参数 -Xmx 指定堆的最大值;
  • 参数 -Xss 指定每个线程分配的内存大小【JDK 1.4 为 256KB;JDK 1.5+ 为 1M】,这决定了进程的数量。

4.1 新生代和老年代

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2。其中,新生代 ( Young ) 被细分为 Eden(伊甸园)和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 fromto,以示区分。

注:JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。( from 和 to 区域位置是平等的)

4.2 JavaCG 垃圾回收机制

Java 的垃圾回收机制使用的是分代收集算法。

  1. 新创建的对象会被分配到 Eden 区;
  2. Eden 区满了会触发一次 Minor GC,仍存活的对象复制到 From 区,年龄+1(对于一些比较大的对象,需要分配一块较大的内存空间,会直接进入到老年代),清空 Eden 区,此时 To 区是空的;
  3. 重复1、2步骤,直到 From 区满,触发一次 Minor GC,Eden 区和 From 区仍存活的对象复制到 To 区,年龄+1,清空 Eden 区和 From 区,同时交换 From 区和 To 区;
  4. 重复1、2、3步骤,直到对象年龄达到某个值(默认是 15 岁,可通过参数设置),则会进入到老年代。

Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法

由于老年代的对象都是比较稳定的热点代码,所以不会那么容易被回收。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间会更长。

*另外,*标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。





  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 Liangxj
  • 访问人数: | 浏览次数: