JVM相关总结

JVM内存结构


前言

最近两周,系统性回顾了一下JVM相关知识点,突然想起自己入坑近四年还未写过一片正儿八经的博客,想想异常惭愧…总的来说,好记性不如烂笔头,本篇博客主要讲述了JVM的概念和内存模型的定义、理解,适当总结,总能对自己有所帮助~


一、JVM是什么

JVM是Java Virtual Machine(Java虚拟机)的缩写,顾名思义,它是一个虚构出来的计算机。和正常的计算机一样,它也有一个指令集,在运行时操作各种内存区域。虚拟机有很多种,不同厂商提供了不同实现,只要遵循虚拟机规范即可,目前我们所说的虚拟机一般指的是Hot Spot,下文也以此为基础进行展开。

二、JVM的作用

Java程序之所以可以实现跨平台,主要就是因为JVM实现的。JVM本身是不认识Java代码的,需要在编译java程序时将写好的源程序通过编译器编译生成.class文件(又称为字节码文件),之后就是通过JVM内部的解释器将字节码文件解释成为具体平台上的机器指令执行,所以,主要在我们不同的操作系统上装上对应的JVM环境,就可以实现java程序的跨平台特性。

三、JVM内部结构

JVM内部体系结构大致分为三部分:类装载器(ClassLoader)子系统,运行时数据区和执行引擎。

1. 类装载器

上文提到,JVM本身是不识别Java代码的,而是通过将源程序编译后的字节码文件解析成平台上的机器指令,而类装载器就是用来解析字节码文件的。

类的生命周期主要可以分为以下几步:

  1. 加载(Loading):找 Class 文件
  2. 验证(Verification):验证格式、依赖
  3. 准备(Preparation):静态字段、方法表
  4. 解析(Resolution):符号解析为引用
  5. 初始化(Initialization):构造器、静态变
    量赋值、静态代码块
  6. 使用(Using)
  7. 卸载(Unloading)

Class loader有多种,总结来说可以分为4类,其中三类是系统自带的,另外一种是开发人员通过自定义来实现的类加载器(需要继承系统ClassLoader)

详细分类如下:

1. 启动类加载器(BootstrapClassLoader2. 扩展类加载器(ExtClassLoader3. 应用类加载器(AppClassLoader)
4. 开发者自定义加载器

注意:这儿存在一个优先级,便于后续理解双亲委派模型,称之为父子关系吧(上述四种的层级是从高到低的)
  • Bootstrap: 是虚拟机自身的一部分,它负责将<JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中。注意,由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。它等于是所有类加载器的爸爸。

  • Extension: 加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类。它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

  • AppClassLoader: 也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java-classpath或-Djava.class.path指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
    有需要时我们也可以通过继承继承Java. lang. ClassLoader来自定义自己的类加载器。

  • 自定义加载器:可以通过继承继承Java. lang. ClassLoader来自定义自己的类加载器

自定义加载器伪代码如下:

public class ClassLoaderTest extends ClassLoader {

    public static void main(String[] args) throws Exception {
        // 相关参数
        final String className = "Hello";
        final String methodName = "hello";
        // 创建类加载器
        ClassLoader classLoader = new ClassLoaderTest();
        // 加载相应的类
        Class<?> clazz = classLoader.loadClass(className);
        // 看看里面有些什么方法
        for (Method m : clazz.getDeclaredMethods()) {
            System.out.println(clazz.getSimpleName() + "." + m.getName());
        }
        // 创建对象
        Object instance = clazz.getDeclaredConstructor().newInstance();
        // 调用实例方法
        Method method = clazz.getMethod(methodName);
        method.invoke(instance);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 如果支持包名, 则需要进行路径转换
        String resourcePath = name.replace(".", "/");
        // 文件后缀
        final String suffix = ".class";
        // 获取输入流
        InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(resourcePath + suffix);
        try {
            // 读取数据
            int length = inputStream.available();
            byte[] byteArray = new byte[length];
            inputStream.read(byteArray);
            // 通知底层定义这个类
            return defineClass(name, byteArray, 0, byteArray.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        } finally {
            close(inputStream);
        }
    }

    // 关闭
    private static void close(Closeable res) {
        if (null != res) {
            try {
                res.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

其中用到的Hello类代码如下:
使用javac将之编译成class文件便可用上述代码进行测试

public class Hello {

  public void hello(){
     System.out.println("Hello, classLoader!");
  }

}

上面自定义加载器执行结果如下:
在这里插入图片描述

加载器的一些特点

加载器特点:
1. 双亲委托
2. 负责依赖
3. 缓存加载

这儿主要说一下双亲委托(委派)吧,这一点在面试时似乎也经常会被问到:
如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。这里的双亲其实就指的是父类,没有mother。父类也不是我们平日所说的那种继承关系,只是调用逻辑是这样。
假如使用自定义加载器加载一个类,顺序分别如下:

自定义加载器 -> AppClassLoader -> ExtClassLoader ->BootstrapClassLoader

一层一层找,如果找到顶级都发现这个类没有被加载过,就只有自己加载咯

2. 运行时数据区

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都有各自的用途以及创建和销毁的时间,有的区域随着虚拟机进行的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁,上述的就被统称为运行时数据区。后续要讲到的JVM内存模型(JMM也主要是针对这块儿区域进行分析)。

3. 执行引擎

(1)物理机的执行引擎是直接建立在处理器、缓存、指令集合操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式
(2)执行引擎的任务是将字节码指令解释编译为对应平台上的本地机器指令来让一个Java程序运行起来,使之在操作系统上直接运行


四、JVM内存模型

1. JVM内存模型定义

Java内存模型英文全称是"Java Memory Model",简称JMM。它不是实际存在的一种内存结构,可以理解为是一个规范,一个抽象概念。JMM 规范明确定义了不同的线程之间,通过哪些方式,在什么时候可以看见其他线程保存到共享变量中的值;以及在必要时,如何对共享变量的访问进行同步。这样的好处是屏蔽各种硬件平台和操作系统之间的内存访问差异,实现了 Java 并发程序真正的跨平台。

简单罗列其中的一些概念:
所有的对象(包括内部的实例成员变量),static 变量,以及数组,都必须存放到堆内存中。局部变量,方法的形参/入参,异常处理语句的入参不允许在线程之间共享,所以不受内存模型的影响。多个线程同时对一个变量访问时【读取/写入】,这时候只要有某个线程执行的是写操作,那么这种现象就称之为“冲突”。
可以被其他线程影响或感知的操作,称为线程间的交互行为, 可分为: 读取、写入、同步操作、外部操作等等。 其中同步操作包括:对 volatile 变量的读写,对管程(monitor)的锁定与解锁,线程的起始操作与结尾操作,线程启动和结束等等。 外部操作则是指对线程执行环境之外的操作,比如停止其他线程等等。
JMM 规范的是线程间的交互操作,而不管线程内部对局部变量进行的操作。

2. JVM运行时区域内存结构

按线程访问权限进行划分,可以分为两类,分别是线程私有区(线程栈)和线程共享区(堆内存)。

1)线程栈

每个线程只能访问自己的线程栈,相互独立,不能访问其他线程的局部变量属性等,线程栈主要可以分为下面几部分:

① 本地方法栈

JVM在调用一些本地方法时需要给这些本地方法(指那些不是由java代码编写的方法)提供的内存空间,因为我们的java代码是有一定的限制的,
它有的时候不能直接跟我们的操作系统底层打交道,所以就需要一些用c或c++编写的本地方法来与操作系统更底层的api来打交道,java代码可
以间接地通过本地方法来调用到底层的一些功能,那这些本地方法运行的时候使用的内存就叫做本地方法栈。

② 程序计数器

记录当前方法执行到了哪里,以便CPU切换回来之后能够继续执行上次执行到的位置,而不会进行重复执行或者遗漏。

③ 虚拟机栈:

早期被叫做Java栈,它的生命周期和线程拥有它的线程保持一致,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack Frame) ,
对应着一次次的Java方法调用。栈帧是一个内存区块,是虚拟栈的基本元素,是一个数据集,维系着方法执行过程中的各种数据信息,每一个栈帧包含的内容有局部变量表、
操作数栈、动态链接、方法返回地址和一些额外的附加信息。在编译代码时,栈帧需要多大的局部变量表,多深的操作数栈都可以完全确定的,并写入到方法表的code属性中。

线程栈总结:
A. 线程私有
B. 和线程生命周期一致
C. 主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回

2)堆内存

堆内存中包含了Java代码中创建的所有对象,被所有线程所共用。按照是否被GC管理又划分为堆区(Heap)和非堆区(Non-Heap)。
Java7及之前,按逻辑主要划分为三部分:年轻代、老年代、永久代,Java8及之后,永久代改名被称为元空间,其中,年轻代和老年代属于堆区,被GC管理,元空间为非堆区。

① 堆区:

按照对象存活周期(经历GC次数)又被划分为年轻代和老年代(默认的,新⽣代 ( Young ) 与⽼年代 ( Old ) 的⽐例的值为 1:2,该值可以通过参数 –XX:NewRatio 来指定)

A. 年轻代

存放一些存活时间较短或者历经GC次数较少的数据,按照对象创建时间和GC相关策略,又将年轻代分为三部分(1一个新生区和两个存活区,默认大小比例 新生区:S0:S1 = 8:1:1,可以通过参数 –XX:SurvivorRatio 来设定)。

详细分类:
新生代(Eden空间)
几乎所有的Java对象都是在Eden区被new出来的,绝大部分的Java对象的销毁都在新生代进行了(局部变量,方法结束就被销毁了)

存活区:Survivor0空间和Survivor1空间 (有时也叫做from区、to区),两者之间有一部分必定是空的,主要是JVM在GC时,年轻代中大部分对象存活时间都比较短,所以年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当发生GC时就其中还活着的对象全部移动到另外一块,并且清空当前内存区域。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中

B. 老年代

当对象在新生代经历一定GC次数(年龄计数器)后仍然存活就会进入老年代,默认值是15次。可以设置参数: -XX;MaxTenuringThreshold=进行设置,当年龄计数器大于等于这个值时,如果这个对象还没被销毁就会存放到老年代中了。除了上述情况,还有一种动态年龄判断,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到 -XX:MaxTenuringThreshold 中要求的年龄。除此之外,HotSpot 虚拟机提供了
-XX:PretenureSizeThreshold参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作。当老年代内存不足时,会触发Major GC,进行老年代的内存清理,如果清理后内存仍然不足就会出现OOM(java. lang.OutOfMemoryError: Java heap space)

② 非堆区:

本质也属于堆区,但是一般不归GC管理,也被划分成3部分

A. 元数据区(Meta区)

Java8之前被称为永久代。

元数据空间和永久代的差别:
存储位置不同:永久代在物理上是堆的一部分,和新生代、老年代的地址是连续的,而元空间属于本地内存。
存储内容不同:在原来的永久代划分中,永久代用来存放类的元数据信息、静态变量以及常量池等。现在类的元信息存储在元空间中,静态变量和常量池等并入堆中,相当于原来的永久代中的数据,被元空间和堆内存给瓜分了

B. CCS(Compressed Class Space)

存放 class 信息的,和 Metaspace 有交叉

C. Code Cache

存放 JIT 编译器编译后的本地机器代码

3)直接内存

上述的Java堆内存根据生命周期进行分而治之,分区之后可以提高JVM垃圾收集的效率,更好地回收为了更好地分配。如果在堆中无法分配内存,并且堆也无法再扩展时,将会抛出OutOfMemoryError异常。
JVM在运行时,除了用到堆内内存外,还会用到直接内存。直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。
直接内存的使用: 常见于NIO,用于数据缓冲区 分配回收的代价较高,但是速度很快 不受JVM内存回收管理.

堆外内存的优点和缺点
优势

  1. 减少了垃圾回收的工作,因为垃圾回收会暂停其他的工作(可能使用多线程或者时间片的方式,根本感觉不到)
  2. 加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。

缺点

  1. 堆外内存难以控制,如果内存泄漏,那么很难排查
  2. 堆外内存相对来说,不适合存储很复杂的对象。一般简单的对象或者扁平化的比较适合。

五、总结

随着JDK的不断迭代,JVM的内存模型也发生了一些变化(目前主要是7->8元空间概念的变化),并且在G1 GC中打破了分区的概念,把内存划分成多个小块进行管理,在后续的博文中,将会对JVM垃圾回收(GC)做出一些总结介绍,也会对不同的一些GC算法、策略做出分析,走过路过的看官,如果发现文章中有不对的地方烦请指出,希望大家可以共同进步,祝大家工作顺利,钱多事少,下节见(哈哈,等有空的时候再总结,希望不会太远!)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值