JVM 简介

JVM 是Java Virtual MachineJava 虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入 Java 语言虚拟机后,Java 语言在不同平台上运行时不需要重新编译。Java 语言使用 Java 虚拟机屏蔽了与具体平台相关的信息,使得 Java 语言编译程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

Java 虚拟机有自己完善的硬件架构,如处理器、堆栈等,还具有相应的指令系统。

Java 虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java 语言的可移植性正是建立在 Java 虚拟机的基础上。任何平台只要装有针对于该平台的 Java 虚拟机,字节码文件(.class)就可以在该平台上运行。这就是“一次编译,多次运行”。

Java 虚拟机不仅是一种跨平台的软件,而且是一种新的网络计算平台。该平台包括许多相关的技术,如符合开放接口标准的各种 API、优化技术等。Java 技术使同一种应用可以运行在不同的平台上。

Java 平台可分为两部分,即 Java虚拟机(Java virtual machine,JVM)Java API 类库。

1、JVM 的种类

Sun 公司的 HotSpot

BEA 公司的 JRockit

IBM 公司的 J9 VM

2、JVM 虚拟机内存结构图

JVM 概念

1、类加载器

什么是类加载

通过 javac.java 文件编译成 .class 字节码文件后,则需要将 .class 加载到 JVM 中运行,哪么是谁将 .class加载到 JVM的呢?那就是类加载器啦。

类加载器类型

  • Bootstrap ClassLoader(启动类加载器):该类加载器由 C++实现的。负责加载 Java 基础类,对应加载的文件是%JRE_HOME/lib/ 目录下的 rt.jarresources.jarcharsets.jar和 class 等。
  • Extension ClassLoader(标准扩展类加载器):继承URLClassLoader。对应加载的文件是%JRE_HOME/lib/ext 目录下的 jar 和 class 等。
  • App ClassLoader(系统类加载器):继承URLClassLoader。对应加载的应用程序 classpath 目录下的所有 jar 和 class 等。
  • CustomClassLoader(用户自定义类加载器):由 Java 实现。我们可以自定义类加载器,并可以加载指定路径下的 class 文件。

什么是双亲委派机制

双亲委派机制是当类加载器需要加载某一个.class字节码文件时,则首先会把这个任务委托给他的上级类加载器,递归这个操作,如果上级没有加载该.class 文件,自己才会去加载这个.class。

加载过程简述

当一个类加载某个类.class(需要编译即 javac Xx.java >> Xx.class )的时候,不会直接去加载

而是自定义会委托应用/系统,应用/系统会委托扩展,扩展会委托启动类加载器尝试去加载

如果启动类加载器不加载这个,就交给扩展,扩展不行就应用/系统,一层层的下去,然后最终加载到这个.class 类。

双亲委派优点

  • 通过委托的方式,保证核心.class 不被篡改。可避免用户自己编写的类动态替换 Java 的核心类,保证了 Class 的执行安全,如 java.lang.String
  • 防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)

小结

题目:可不可以自己写个 String 类,也叫 java.lang.String

可以,但在应用的时候,需要用自己的类加载器去加载,否则,系统的类加载器永远只是去加载 jre.jar 包中的那个java.lang.String 。因为在类加载中,会根据双亲委派机制去寻找当前 java.lang.String 是否已被加载。由于启动类加载器已在启动时候加载了所以不会再次加载,因此使用的 String 是已在 java 核心类库加载过的 String,而不是新定义的 String。

2、执行引擎

负责解释 JVM 内部命令,翻译给操作系统执行

3、本地方法接口

Java 开发中会碰到声明为 native 的方法

为什么存在 native 方法呢?Java 不是完美的,Java 的不足除了体现在运行速度上要比传统的 C++慢许多之外,Java 无法直接访问到操作系统底层(如系统硬件等),为此 Java 使用 native 方法来扩展 Java 程序的功能

1
private native void start();

4、本地方法栈

内存中的一块区域负责登记 native方法

5、PC 寄存器

是线程私有的,是一个指针,指向方法区中的方法字节码,指向下一个方法所在的地址,直到整个方法全部结束,PC 寄存器就结束了

PC 寄存器在 Java栈 里面,不会被垃圾回收器回收,因为生命周期短,PC 寄存器被回收了的话方法就没法调用了

6、方法区

静态变量(类变量) + 常量 + 类信息(.class 构造方法/接口定义) + 运行时常量池 存在方法区中

类加载器加载的类就放到方法区,该区归所有线程共享

实例变量存在堆内存中,和方法区无关

7、Java 栈

队列: 先进先出

栈: 后进先出

栈也叫栈内存,在线程创建时创建,生命周期跟随线程生命周期

对于栈来说不存在垃圾回收问题,只要线程一结束该栈就 Over,是线程私有的。

8 种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配。

栈帧中主要保存 3 类数据

  • 本地变量(Local Variables):输入参数和输出参数以及方法内的变量;

  • 栈操作(Operand Stack):记录出栈、入栈的操作;

  • 栈帧数据(Frame Data):包括类文件、方法等等

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

public static void main(String[] args) {
System.out.println(show(0));
}

public static int show(int a) {
a++;
if (a < 0) {
return a;
}
a = show(a);
return a;
}
}

运行会报栈溢出异常:Exception in thread “main” java.lang.StackOverflowError

8、栈+堆+方法区的交互关系

1、新生代

新生代主要用来存放新生的对象。一般占据堆空间的 1/3。

在新生代中,保存着大量的刚刚创建的对象,但是大部分的对象都是朝生夕死,所以在新生代中会频繁的进行 MinorGC,进行垃圾回收。新生代又细分为三个区:Eden 区、SurvivorFrom、ServivorTo 区,三个区的默认比例为:8:1:1。

  • Eden 区:Java 新创建的对象绝大部分会分配在 Eden 区(如果对象太大,则直接分配到老年代)。当 Eden 区内存不够的时候,就会触发 MinorGC(新生代采用的是复制算法),对新生代进行一次垃圾回收。
  • SurvivorFrom 区和 To 区:在 GC 开始的时候,对象只会存在于 Eden 区和名为 From 的 Survivor 区,To 区是空的。一次 MinorGc 过后,Eden 区和 SurvivorFrom 区存活的对象会移动到 SurvivorTo 区中,然后会清空 Eden 区和 SurvivorFrom 区,并对存活的对象的年龄+1;如果对象的年龄达到 15,则直接分配到老年代。MinorGC 完成后,SurvivorFrom 区和 SurvivorTo 区的功能进行互换。下一次 MinorGC 时,会把 SurvivorTo 区和 Eden 区存活的对象放入 SurvivorFrom 区中,并计算对象存活的年龄。

总结

新生代到老年代的两种方式

  • Java 新创建的对象太大,不进行 MinorGC,就直接被分配到了老年代
  • 默认的设置下,当对象的年龄达到 15 岁时,也就是躲过 15 次 GC 的时候,他就会转移到老年代去

2、老年代

老年代主要存放应用中生命周期长的内存对象。老年代比较稳定,不会频繁的进行 MajorGC。

而在 MajorGC 之前才会先进行一次 MinorGc,使得新生的对象进入老年代而导致空间不够才会触发。当无法找到足够大的连续空间分配给新创建的较大对象也会提前触发一次 MajorGC 进行垃圾回收腾出空间。

在老年代中,MajorGC 采用了标记—清除算法:首先扫描一次所有老年代里的对象,标记出存活的对象,然后回收没有标记的对象。MajorGC 的耗时比较长。因为要扫描再回收。MajorGC 会产生内存碎片,当老年代也没有内存分配给新来的对象的时候,就会抛出 OOM(Out of Memory)异常。

3、永久代(Java8 中已经被移除)

永久代指的是永久保存区域。主要存放 Class 和 Meta(元数据)的信息。Class 在被加载的时候被放入永久区域,它和存放的实例的区域不同,在 Java8 中已经被移除,取而代之的是一个称之为“元数据区”(元空间)的区域。元空间和永久代类似,都是对 JVM 中规范中方法的实现。

元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存的限制。类的元数据放入 native memory,字符串池和类的静态变量放入 java 堆中。这样可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

采用元空间而不用永久代的原因:

  • 为了解决永久代的 OOM 问题,元数据和 class 对象存放在永久代中,容易出现性能问题和内存溢出。
  • 类及方法的信息等比较难确定其大小,因此对于永久代大小指定比较困难,大小容易出现永久代溢出,太大容易导致老年代溢出(堆内存不变,此消彼长)。
  • 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

分为 3 块 新生代 老年代 元空间(Java8 之前)

4、Minor GC 和 Major GC

  • 新生代 GC(Minor GC):指发生在新生代的垃圾收集动作,因为 Java 对象大多都具备朝生夕灭的特性,所以 Minor GC 非常频繁,一般回收速度也比较快。

  • 老年代 GC(Major GC / Full GC):指发生在老年代的 GC,出现了 Major GC,经常会伴随至少一次的 Minor GC(但非绝对的,在 ParallelScavenge 收集器的收集策略里就有直接进行 Major GC 的策略选择过程) 。MajorGC 的速度一般会比 Minor GC 慢 10 倍以上。

通过程序理解 JVM

1、堆分配参数

初始堆大小和最大堆大小可以设置为一样,可以减少 GC 回收次数 提升性能

参数说明
-XX:+PrintGC使用这个参数,虚拟机启动后,只要遇到 GC 就会打印日志
-XX:+UseSerialGC配置串行回收器
-XX:+PrintGCDetails可以查看详细信息,包括各个区的情况
-Xms设置 java 程序启动时初始堆大小
-Xmx设置 java 程序能获得的最大堆大小
-XX:+PrintCommandLineFlags可以将隐式或者显示传给虚拟机的参数输出

在实际工作中,我们可以将-Xms-Xmx 设置为相等,防止动态调整的时候因为没有内存而报错,同时可以减少程序运行时的垃圾回收次数,从而提高性能。最小设置单位为 m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Random;

public class Test01 {

public static void main(String[] args) {
test02();
}

public static void test02() {
//-Xms8m -Xmx8m -XX:+PrintGCDetails
//minor GC 和full GC触发
String str = "www.atguigu.com";
int i = 1;
while (true) {
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999);
System.out.println("循环: " + i);
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
System.out.println(str.getBytes().length);
i++;
}
}
}

2、堆分配参数

参数说明
-Xmn新生代大小
增大新生代后,将会减小年老代大小.此值对系统性能影响较大
Sun 官方推荐配置为整个堆的 3/8
-XX:NewRatio设置新生代和老年代的比例
-XX:SurvivorRatio设置 eden:from:to 的比例

总结:不同的堆分布情况,对系统执行会产生一定的影响,在实际工作中,应该根据系统的特点做出合理的配置。

基本策略:尽可能将对象预留在新生代,减少老年代的 GC 次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test02 {
public static void main(String[] args) {
//第一次配置
//-Xms20m -Xmx20m -Xmn1m -XX:SurvivorRatio=8 -XX:+PrintGCDetails -XX:+UseSerialGC
//第二次配置
//-Xms20m -Xmx20m -Xmn8m -XX:+PrintGCDetails -XX:+UseSerialGC
//第三次配置
//-XX:NewRatio=老年代/新生代
//-Xms21m -Xmx21m -XX:NewRatio=2 -XX:+PrintGCDetails -XX:+UseSerialGC
byte[] b = null;
//连续向系统申请10MB空间
for(int i = 0 ; i <10; i ++){
b = new byte[1*1024*1024];
}
}
}

3、堆溢出处理

在 java 程序的运行过程中,如果堆空间不足,则会抛出内存溢出的错误(Out Of Menory)OOM,一旦这类问题发生在生产环境,可能引起严重的业务中断,java 虚拟机提供了-XX:+HeapDumpOnOutOfMemoryError,使用该参数可以在内存溢出时导出整个堆信息,与之配合使用的还有参数, -XX:HeapDumpPath,可以设置导出堆的存放路径。

内存分析工具:jvisualvm

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
package com.atguigu.test;

import java.util.Random;

public class Test03 {

public static void main(String[] args) {
test03();
}

public static void test03() {
//-Xms15m -Xmx15m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\test03.hprof
String str = "www.atguigu.com";
int i = 1;
while (true) {
str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999);
System.out.println("循环: " + i);
System.out.println("max memory:" + Runtime.getRuntime().maxMemory());
System.out.println("free memory:" + Runtime.getRuntime().freeMemory());
System.out.println("total memory:" + Runtime.getRuntime().totalMemory());
System.out.println(str.getBytes().length);
i++;
}
}
}

4、方法区(了解)

方法区溢出 OutOfMemoryError:PermGen space

和 java 堆一样,方法区是一块所有线程共享的内存区域,它用于保存系统的类信息,方法区(永久区)可以保存多少信息可以对其进行配置,在默认情况下,-xxMaxPermSize为64MB,如果系统运行时生产大量的类,就需要设置一个相对合适的方法区,以免出现永久区内存溢出的问题。 -XX:PermSize=64M-XXMaxPermSize=64M