了解JVM

JVM,全称Java Vitural Machine,即Java虚拟机。JVM是Java程序运行的地方,本质还是一款软件。Java之所以能一处编译到处运行,是因为Java提供了不同操作系统的JVM。从它是一款软件,我们可以看出JVM运行在操作系统之上的。

java jvm教程 视频 jvm通俗易懂_java jvm教程 视频


所以我们要知道Java是跨平台的,但是JVM是不跨平台的。这里给大家分享一个小技巧,我们看自己的电脑是不是新的,可以在DOS下输入Java -version验证一下即可,如果出现Java的版本信息,那自然不是新的,原因上面也提到过,JVM是在操作系统之上的。

综上所述,我们可以得出一个结论:
Java是不能直接操作计算机硬件的,只能通过Native方法间接操作。

JVM体系架构图

java jvm教程 视频 jvm通俗易懂_java jvm教程 视频_02


我们从架构图可以看到,一个Java源程序首先要经过编译成字节码文件然后再由类加载器加载到内存中,最后由执行引擎执行!

类加载器(ClassLoader)

类从加载到虚拟机内存中开始,到卸载除内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。

我们首先来看看一个类加载到 JVM 的一个基本结构:

java jvm教程 视频 jvm通俗易懂_加载_03

加载
“加载’'是”类加载“过程的一个阶段。在此阶段,虚拟机需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

连接
上面说到,验证、准备、解析统称为连接。
1. 验证
验证时连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
2.准备
准备阶段是正式为类变量(static修饰的变量) 分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这里所说的初始值"通常情况"下是数据类型的零值,即数据类型的默认初始值,假设一个类变量的定义为:

public static int value=123;

那变量value在准备阶段过后的初始值为0而不是123,因为这个时候尚未执行然和任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器"< clinit > ()"发方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。

基本数据类型的零值

数据类型

零值

int

0

long

0L

short

(short) 0

char

‘\u0000’

byte

(byte) 0

boolean

false

float

0.0f

double

0.0d

reference

null

3.解析
解析阶段是虚拟机将常量池的符号引用替换为直接引用的过程,符号引用以一组符号来描述所引用的目标, 符号可以是任何形式的字面量, 只要使用时能够无歧义的定位到目标即可. 个人理解为变量名就相当于一个符号引用,Java在编译的时候不知道对象的具体内存地址,只能通过一个名字来引用它。而到解析的时候,就会将这个符号引用直接转转为真正的直接引用(可以理解为指向地址的指针),找到对应的直接地址!

初始化
初始化阶段是类加载过程的最后异步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者是字节码)。

static

public class Demo02 {
    public static void main(String[] args) {
        System.out.println(MyChild1.str2);
        // 运行的结果
        /**
         * MyParent1 static
         * MyChild1 static
         * hello,str2
         */
    }
}

class MyParent1{
    public static String str = "hello,world";
    static {
        System.out.println("MyParent1 static");
    }
}

class MyChild1 extends MyParent1{
    public static String str2 = "hello,str2";
    static {
        System.out.println("MyChild1 static");
    }
}

final

public class Demo03 {
    public static void main(String[] args) {
        System.out.println(MyParent02.str);
    }
}

class MyParent02{
    public static final String str = "hello world";

    static {
        System.out.println("MyParent02 static"); // 这句话会输出吗?
    }

}
结果是不会输出
public class Demo04 {
    public static void main(String[] args) {
        System.out.println(MyParent04.str);
    }
}

class MyParent04{

    public static final String str = UUID.randomUUID().toString();

    static {
        System.out.println("MyParent04 static"); // 这句话会输出吗?
    }

}

结果是,会

小结:
类被加载了不一定就会执行静态代码块,只有一个类被主动使用的时候,静态代码才会被执行!

当一个类被主动使用时,Java虚拟就会对其初始化,如下六种情况为主动使用:

  1. 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
  2. 当调用某个类的静态方法时
  3. 当使用某个类或接口的静态字段时
  4. 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
  5. 当初始化某个子类时
  6. 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
    Java编译器会收集所有的类变量初始化语句和类型的静态初始化器,将这些放到一个特殊的方法中:clinit。

调用运行期常量,接口和类都会触发初始化
调用编译期常量,都不会触发类或接口的初始化(因为已经放入调用类的常量池中。

ClassLoader 分类

1、java虚拟机自带的加载器

  • BootStrap 根加载器 (加载系统的包,JDK 核心库中的类 rt.jar)
  • Ext 扩展类加载器 (加载一些扩展jar包中的类)
  • Sys/App 系统(应用类)加载器 (我们自己编写的类)

2、用户自己定义的加载器

  • ClassLoader,只需要继承这个抽象类即可,自定义自己的类加载器

双亲委派机制

一层一层的让父类去加载,如果顶层的加载器不能加载,然后再向下类推!通俗来说,就好比你想买套房子,但是你首先想到的是向你爸爸要钱,你爸爸又找你爷爷要钱。最后你爷爷说他没钱,让你爸爸自己想办法,你爸爸也没钱,让你自己想办法。

双亲委派机制的作用就是保护Java的核心类不被用户自定义的相同名字的类而篡改。

JVM各部分说明

程序计数器

内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。

如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。

java jvm教程 视频 jvm通俗易懂_Java_04


bipush 将 int、float、String、常量值推送值栈顶;

istore 将一个数值从操作数栈存储到局部变量表;

iadd

imul

方法区

方法区(Method Area)与堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。Java虚拟机规范把方法区描述为堆的一个逻辑部分,为了和堆区分开来,所以它有一个别名叫Non-Heap(非堆)

JDK1.7之前

永久代:用于存储一些虚拟机加载类信息,常量,字符串、静态变量等等。。。。这些东西都会放到永久代中;

永久代大小空间是有限的:如果满了 OutOfMemoryError:PermGen

JDK1.8之后

彻底将永久代移除 HotSpot jvm ,Java Heap 中或者 Metaspcace(Native Heap)元空间;

元空间就是方法区在 HotSpot jvm 的实现;

元空间和永久代,都是对JVM规范中方法区的实现。

元空间和永久代最大的区别:元空间并不在Java虚拟机中,使用的是本地内存!

-XX:MetasapceSize10m

虚拟机栈

线程私有,生命周期和线程一致。描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError: 线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError: 如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

本地方法栈

区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。也会有 StackOverflowError 和 OutOfMemoryError 异常。 只要是带了native 这个关键字的,说明 java的作用范围达不到,只能去调用底层 C 语言的库!

Java7之前:

Heap 堆,一个JVM实例中只存在一个堆,堆的内存大小是可以调节的。可以存的内容:类、方法、常量、保存了类型引用的真实信息;

分为三个部分:

  • 新生区:Young (Eden-s0-s1)
  • 养老区:Old Tenure
  • 永久区:Perm

堆内存在逻辑上分为三个部分:新生、养老、永久(JDK1.8以后,叫元空间)

物理上只有 新生、养老;元空间在本地内存中,不在JVM中!

新生区

新生区 就是一个类诞生、成长、消亡的地方!

新生区细分: Eden、s(from to),所有的类Eden被 new 出来的,慢慢的当 Eden 满了,程序还需要创建对象的时候,就会触发一次轻量级GC(Minor GC;清理完一次垃圾之后,会将活下来的对象,会放入幸存者区(),… 清理了 20次之后,出现了一些极其顽强的对象,有些对象突破了15次的垃圾回收!这时候就会将这个对象送入养老区!运行了几个月之后,养老区满了,就会触发一次 Full GC;

养老区

大对象可以直接在老年代分配,15次都幸存下来的对象进入养老区,养老区满了之后,触发 Full GC。默认是15次,可以修改!

永久区(Perm)

放一些 JDK 自身携带的 Class、Interface的元数据;几乎不会被垃圾回收的;
JDK1.6之前: 有永久代、常量池在方法区;
JDK1.7:有永久代、但是开始尝试去永久代,常量池在堆中;
JDK1.8 之后:永久代没有了,取而代之的是元空间;常量池在元空间中

OutOfMemoryError:PermGen 在项目启动的时候永久代不够用了?加载大量的第三方包!

元空间:它是本地内存!