JVM从入门到坚持

楔子

在生产开发中,20%的 JAVA 知识,就能完成 80%的工作。但是区分高手与新手的差异往往就在那 20%,JVM 就属于这 20%的知识,本篇文章准备和大家分享 JVM 入门的相关知识,可以理解为简单的导论。

从 hello world 的运行开讲

学习一门新语言一般都是从简单的 hello world 开始,所以,咱们先准备一个 HelloWorld.java 的文件。

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

打开终端 执行命令:
在这里插入图片描述

会发现目录下多了个 HelloWorld.class 文件,然后继续执行命令
在这里插入图片描述

输出结果
在这里插入图片描述

总结起来,程序的运行步骤如下:

1、首先把“.java”代码文件编译成“.class”字节码文件;

2、然后类加载器把“.class”字节码文件中的类给加载到 JVM 中 ;

3、接着是 JVM 执行类中的代码。

流程图如下:
在这里插入图片描述

JVM 类加载机制

提到类的加载,就离不开一个问题——JVM什么情况下会加载一个类呢?答案是使用这个类的时候。那什么时候会使用到这个类呢?一个是程序的入口main函数,一个是实例化这个对象的时候。从下面这一段代码,我们分析一下这个过程:

public class Test {
    public static void main(String[] args){
        UpDownloaderFactory factory = new UpDownloaderFactory();
    }
}

三个步骤

(1)步骤一

JVM 进程启动之后,会加载 Test.class 到内存里;

(2)步骤二

如果有“main()”方法,则从此入口开始执行程序;

(3)步骤三

执行程序时,发现需要使用 UpDownloaderFactory 去实例化对象,所以得把 UpDownloaderFactory.class 加载到内存里面来。

四个环节

类的加载大概就是这样一个情况,当然在进行加载的过程中,我们有 4 个概念需要大概了解下。

(1)验证

这个环节主要是确定 JVM 加载的.class 文件是符合规范的,就好比男人去男厕所,女人去女厕所,符合 JVM 规范的才能进入到 JVM。

(2)准备

这个环节主要是给类以及类变量分配内存空间。

(3)解析

这个环节主要是把符号引用替换为直接引用。

(4)初始化

这个环节主要主要做的是赋值。

注:针对初始化有一个重要的规则,就是如果初始化一个类的时候,发现他的父类还没初始化,那么必须先初始化他的父类。

流程图如下:
在这里插入图片描述

双亲委派机制

在介绍双亲委派机制的时候,不得不提 ClassLoader(类加载器)。我们可以从 java.lang 包下,找到源码入口。

ClassLoader.loadClass

/**
 *   加载具有指定二进制名称的类。 此方法的默认实现按以下顺序搜索类:
 *   调用 findLoadedClass(String)来检查类是否已经加载。
 *   在父类加载器上调用 loadClass 方法。 如果 parent 为 null,则使用虚拟机内置的类加载器。
 *   调用 findClass(String)方法来查找类。
 *   如果使用上述步骤找到了该类,并且解析标志为真,则此方法将在生成的 Class 对象上调用 resolveClass(Class)方法。
 *   鼓励 ClassLoader 的子类覆盖 findClass(String) ,而不是这个方法。
 *   除非被覆盖,否则在整个类加载过程中,此方法会同步 getClassLoadingLock 方法的结果。
 *   参数:
 *   name - 类的二进制名称
 *   解决 - 如果为真,则解决类
 *   返回:
 *   生成的 Class 对象
 *   抛出:
 *   ClassNotFoundException – 如果找不到类
 */
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // ①首先,检查是否已经被类加载器加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // ②存在父加载器,递归的交由父加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // ③直到最上面的Bootstrap类加载器或者Null
                    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.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

通过源码我们可以很好的分析出什么是双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:
在这里插入图片描述

小结:双亲委派就是向上委派,向下查找

JVM 内存区域划分

上一章我们讲了 JVM 的类加载机制,知道了类会被 JVM 加载到内存中以便使用,但是这些类被加载到内存以后,放在哪里去了呢?这个就要涉及到 JVM 的内存区域划分了。

我们以以下代码为例

class Application{
    public static void main(String[] args) {
        User user = new User();
        user.getName();
    }
}

首先我们加载了一个类,所以我们需要一块空间来存放类信息,这块空间在 JVM 中叫做元数据区(方法区)。

接着需要将.class 文件翻译成字节码,所以 JVM 使用字节码引擎,执行字节码指令,使用程序计数器,记录指令位置。

然后每一个程序计数器会对应一个线程,如果线程执行了一个方法,就会对这个方法创建一个栈帧。栈帧里包含了方法的局部变量。以上所有数据统称为 JAVA 虚拟机栈。

JAVA 虚拟机栈中的局部变量,会指向另一片区域中的对象地址,这一片存放对象的区域叫做 JAVA 虚拟机堆。

以上过程的流程图,总结如下:

在这里插入图片描述

JVM 分代模型

上一章我们讲了 JVM 的内存区域划分,这一章,咱们继续学习 JVM 的分代模式。先说结论,JAVA 的 JVM 一般分为:新生代(年轻代)、老年代、永久代。

通过以下代码和相关流程图,咱们先简单的认识下这 3 个分代模型。

代码:

class Application{
    // 静态变量属于类
    private static Man man = new Man();
    public static void main(String[] args) {
        getName();
        // 因为在 while 死循环中,Man 的对象一直被 Application 的 man 引用着
        // 所以他会存在于老年代中不会被 GC 回收
        while (true){
            man.createMan();
        }
    }
    private static void createMan(){
        man.createMan();
    }
    private static void getName(){
        // 局部变量
        User user = new User();
        user.getName();
    }
}

流程图:

在这里插入图片描述

小结:

(1)新生代一般是短期存在的对象。

(2)存在时间长的会进入老年代。

(3)一般情况下,新产生的对象,会存在于新生代中。

常见的 JVM 参数

说到 JVM 参数,大家的第一印象一般就是 JVM 调优,作为入门导论,该章不具体阐述如何调优,仅仅列出 JVM 最常见的基础参数,如下图所示:
在这里插入图片描述

注:如果您想获取更多关于 JVM 调优的知识,小七提供了一组 demo (如果能顺手给个 start 就更好啦,小七会不定时更新的~)

什么情况下对象会被垃圾回收掉

JVM 通过一种可达性分析算法来判断对象是否可以被回收,也就是说看看对象都是谁在引用他,是否有一个 GCRoots。总而言之,只要你的对象被方法的局部变量或者类的静态变量引用着,那么他就不会被回收。

新生代复制算法及其优化

通过学习前面的章节,咱们了解了 JVM 分代模型有哪些,但是大家肯定会有个疑问,JVM 为什么要这么分代呢?实际上这跟 GC 回收的算法有关,这一章,咱们就来看看新生代的垃圾回收算法。

新生代的垃圾回收算法是基于复制算法思想的。说人话就是,首先把新生代的内存分为两块,一块放存活对象,一块备用,两块空间交替使用。但是这样有个问题,比如 1G 的空间,等分成两块,那么就会有大概 500M 的空间处于空闲状态。为了减少空间的浪费,根据内存的使用频率,Java 做了优化,将空间分为了两块 Survivor 区,和一块 Eden 区。

如果 Eden 区快满了,此时就会触发垃圾回收,把 Eden 区中的存活对象都一次性转移到一块空着的 Survivor 区。接着 Eden 区就会被清空,然后再次分配新对象到 Eden 区里。如果下次 Eden 区满了,那么再次触发 GC,就会把 Eden 区和放着上一次 GC 后存活对象的 Survivor 区内的存活对象,转移到另外一块 Survivor 区去。这样 3 块内存循环使用,按 8:1:1 分配的话,90%的空间都得到了有效的利用。

以上内容,总结的流程图如下:

在这里插入图片描述

新生代进入老年代的条件

上一章,咱们简单的介绍了新生代垃圾回收的算法,这一章咱们讨论一下,什么情况下,新生代会进入老年代。

(1)躲过 15 次垃圾回收,就进入老年代

(2)动态年龄判断

年龄1+年龄2+年龄3+年龄N的对象加起来的空间,大于survivor区域的一半,就会让年龄N和年龄N以上的对象进入老年代。

注:动态年龄判断是年龄从小到大对象的占据空间的累加和,而不是某一个特定年龄对象占据的空间。

(3)大对象直接进入老年代

这个也很好理解,你的对象都那么大了,在新生代倒来倒去不是浪费空间吗,不如早点去老年代待着。

(4)YuangGC 后,对象太多,无法放入 survivor 区

(5)空间担保原则

如果老年代的可用内存大小是小 于新生代的所有对象的总大小的,看是否设置了参数 “-XX:-HandlePromotionFailure”( 设置空间担保)。

有这个参数,就会继续尝试下一步判断,就是看看老年代的可用内存是否大于之前每一次Young GC后进入老年代的对象总和的平均大小。剩余的存活对象的值大于survivor区的内存大小,并且小于老年代可用内存大小,那么就会进入老年代。

以上内容,小七绘制了一个流程图,方便大家记忆理解:

在这里插入图片描述

stop the world

如果说"hello world"代表是一个程序的开始,那么"stop the world",就代表着程序的暂停。

新生代垃圾回收器

在这里插入图片描述

老年代垃圾回收器

在这里插入图片描述

触发 FullGC 的条件

在这里插入图片描述

G1 垃圾回收器

本质上来说,G1垃圾回收器依然是一个分代垃圾回收器。但是它与一般的回收器所不同的是,它引入了额外的概念,Region。G1垃圾回收器把堆划分成一个个大小相同的Region。在HotSpot的实现中,整个堆被划分成2048左右个Region。每个Region的大小在1-32MB之间,具体多大取决于堆的大小。
在这里插入图片描述

后记

这篇文章作为小七JVM学习的第一篇文章,主要整理的是一些基本概念,对于实战调优并没有涉及。一方面是JVM调优需要针对具体项目具体分析,没有一套万能配置;另一方面是小七的实战经验也的确不足,暂时就没在本篇呈现相关内容了。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

第七人格

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值