JVM

浅谈JVM

  • 什么是JVM?
  • JVM的作用
  • 体系结构
    • 位置
    • JVM的体系结构
  • 类加载器
    • 双亲委派机制
  • Native
  • 方法区
  • 栈(Stack)
  • Java堆(Java Heap)
  • 新建对象实例分析
  • GC(垃圾回收)
    • 引用计数法
    • 复制算法
    • 可达性分析算法
    • 标记清除算法
    • 标记压缩算法
    • 总结分析

什么是JVM?

JVM是Java Virtual Machine(Java虚拟机)的缩写,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。 简单来说,所有的Java程序都是在JVM中运行的。

JVM的作用

Java语言的一个非常重要的特点就是与平台的无关性。而使用Java虚拟机是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。

体系结构

位置

JVM_第1张图片

JVM的体系结构

JVM_第2张图片

官方图解
JVM_第3张图片

类加载器

作用:加载class文件

类型

  1. 虚拟机自带的加载器
  2. Bootstrap classLoader(根加载器):主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
  3. ExtClassLoader(扩展类加载器):主要负责加载jre/lib/ext目录下的一些扩展的jar
  4. AppClassLoader(应用程序加载器):主要负责加载应用程序的主函数类

它们的优先级关系如下所示
JVM_第4张图片

双亲委派机制

了解完类加载器的类型之后,就能明白双亲委派机制的原理了。这里只是简单介绍一下,如果想
深入了解,可以点击这里

双亲委派机制的作用原理

  1. 类加载器接收到类加载的请求
  2. 将这个请求委托给父类加载器去完成,一直向上委托,直到根加载器
  3. 启动加载器检查是否能加载这个类,能加载就使用当前加载器进行加载。不能的话,就通知子加载进行加载
  4. 重复步骤3

JVM_第5张图片

Native

如果你写了这样一行代码new Thread().start();,然后去查看源码,会发现这样一行代码private native void start0(); 。是不是感觉很奇怪。

我们知道java的底层是用C/C++写的。
如果一个方法带了native关键字,说明Java作用不到这个方法,它就会调用底层C语言的库。

在加载class文件时,这个方法会进入本地栈(native method Stack),调用本地方法接口(JNI),来加载本地方法库。

本地方法接口(JNI)的作用:扩展java的使用,融合不同的语言为Java使用。

方法区

1. 什么是方法区(Method Area)?

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。

2.方法区(Method Area)存储什么?

它存储已被Java虚拟机加载的类信息、常量(final修饰)、静态变量(static)、即时编译器编译后的代码等

在jdk7及以前,习惯上把方法区称为永久代,jdk8开始,使用元空间取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不再虚拟机设置的内存中,而是使用本地内存

JVM_第6张图片

栈(Stack)

栈(Stack):线程私有的内存区域。一个线程对应一个栈。线程一旦结束,栈就Over。

栈存储的是什么?

八大基本数据类型, 堆中对象的引用。

Java堆(Java Heap)

被所有线程共享的一块内存区域,在虚拟机启动时创建。Java堆(Java Heap)唯一目的就是存放对象实例。所有的对象实例及数组都要在Java堆(Java Heap)上分配内存空间。

关于堆的详细介绍,请点击这里

新建对象实例分析

JVM_第7张图片

代码实例

public class PersonTest {
     
    //a存放在栈中 a如果赋值,也存放在栈中
    public int a;
    //存放在方法区
    public static String name="dz";
    public static void main(String[] args) {
     
        //PersonTest 存放在方法区  test存放在栈   new PersonTest()实例化对象存放在堆中
        PersonTest test = new PersonTest();
        //存放在堆中
        test.a=1;
        System.out.println(test.a);
        //堆中没有name的值,则去栈获方法区中找
        System.out.println(test.name);
    }
}

GC(垃圾回收)

GC主要在方法区和堆中进行

接下来介绍几个GC常用的算法。

引用计数法

JVM_第8张图片
如图所示,每个对象创建的时候都会有一个计数器跟随,对象引用一次,计数器加1。进行GC时,如果计数器显示为0,则被清除。可以看出这种算法非常低效,因此JVM不使用这种算法。

复制算法

JVM_第9张图片
前边为了理解方便,使用了幸存者0区,幸存者1区。实际上叫from区和to区。只是名称不同而已。
幸存者from区和幸存者to区不是固定不变的,而是相互转换。记住一句话,谁空谁是to。

原理

  • 每次GC后,伊甸园区(Eden)都会为空。因为存活的对象会进入幸存者to区,没存活下来的直接清除。
  • 接着会将幸存者from区中的对象复制到幸存者to区。这时幸存者from区没有对象,就变成了幸存者to区
  • 默认情况下,如果一个对象经过15次轻GC,还没有死亡,就会进入老年区(代)。我们也可以使用这个参数**-XX:MaxTenuringThreshold=10**来控制对象进入老年代的时间。

复制算法的最佳使用场景:对象存活度较低的时候,即新生区。

好处:没有内存碎片
坏处:浪费了内存空间

可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、_object 7虽然互相有关联,但是它们到GCRoots是不可达的,所以它们将会被判定为是可回收的对象。

JVM_第10张图片
在Java语言中,可作为GC Roots的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表).中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI (即一般说的Native方法)引用的对象。

标记清除算法

原理

通过可达性分析算法,将需要清除的对象进行标记。在标记完成后统一回收被标记的对象。

它是最基础的算法,后边的算法都是基于此进行改进的。

缺点

一个是效率问题,标记和清除两个过程的效率都不高
另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

JVM_第11张图片

标记压缩算法

标记压缩算法主要是为了解决标记清除算法的空间问题。 即清除过后将对象都移到一边,这样就不会有内存碎片。但是时间复杂度又变高了,因为需要对内存再扫描一次。

JVM_第12张图片

总结分析

  • 内存效率(时间复杂度):复制算法>标记清除算法>标记压缩算法
  • 内存整齐度: 复制算法=标记压缩算法>标记清除算法
  • 内存利用率:标记清除算法=标记压缩算法>复制算法

没有最好的算法,只有最合适的算法。 GC采用的是分代收集算法。 即在不同的代使用不同的算法。

年轻代由于存活率低,所以使用复制算法。
老年代(养老区)由于区域大,存活率高,使用标记清除+标记压缩混合实现。 即进行几次标记清除后,有足够多的内存碎片,在进行一次标记压缩。

这里只是简单地介绍一下JVM的相关知识,还有很多没有涉及到。如果想了解的更详细一点。建议看《深入理解Java虚拟机》这本书。想要电子版资源的,请点击链接
提取码:y5yu

你可能感兴趣的:(JVM,算法,jvm,java,编程语言)