JVM学习之内存与垃圾回收篇1

1 JVM与Java体系结构

1.0 Java发展重大事件

2000年,JDK 1.3发布,Java Hot Spot Virtual Machine正式发布,成为Java的默认虚拟机。
2006年,JDK 6发布。同年,Java开源并建立了OpenJDK。顺理成章,Hotspot虚拟机也成为了OpenJDK中的默认虚拟机。
2008年,Oracle收购了BEA,得到了JRockit虚拟机。
2010年,Oracle收购了Sun,获得了Java的商标和HotSpot虚拟机。
2011年,JDK 7 中正式启用G1垃圾收集器。
2017年,JDK 9 中G1成为默认的GC,代替CMS。IBM也开源了J9 虚拟机。
2018年,JDK 11 LTS版本,发布革命性的ZGC,调整jdk授权许可。
2019年, JDK 12发布,增加了Shenandoah gc。

1.1 虚拟机和Java虚拟机

虚拟机,就是虚拟的计算机,是用来执行一系列虚拟计算机指令的软件。大体可以分为系统虚拟机和程序虚拟机。

Visual Box,VMWare就是系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行的完整的操作系统软件平台。 Java虚拟机就是典型的程序虚拟机,它专门为执行单个计算机程序而设计。

无论是哪种虚拟机,其上所运行的软件都被限制于虚拟机提供的资源中。

Java虚拟机就是一台执行字节码(这个字节码可以是Java语言生成的,也可以是其他语言生成的)的虚拟计算机。

在这里插入图片描述

1.3 JVM整体结构

在这里插入图片描述

1.4 Java代码执行流程

java源码(xxx.java)
编译(前端编译):词法分析、语法分析、语法/抽象语法树、语义分析、注解抽象语法树和字节码生成器。
字节码(xxx.class)
Java虚拟机:类加载器,字节码校验器,字节码解释器和JIT编译器
二进制指令
操作系统

1.5 JVM架构模型

Java编译器输入的指令流基本上是一种基于栈的指令集架构,另外一种指令架构是基于寄存器的指令架构
栈式架构特点:

  1. 设计和实现更简单,适用于资源受限的系统。
  2. 避开了寄存器分配难题:使用零地址指令方式分配。
  3. 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小(单条指令更短),编译器容易实现。
  4. 不需要硬件支持,可移植性更好,更好实现跨平台。

寄存器式架构特点:
5. 典型应用是x86的二进制指令集:传统pc以及Android的Davlik虚拟机。
6. 指令集架构完全依赖硬件,可移植性差。
7. 性能优秀和执行高效。
8. 花费更少的指令(指令条数少)去完成一项操作。
9. 大部分情况下, 基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主。基于栈的指令架构往往以零地址指令为主。

举例:两种指令架构下实现2+3

iconst_2  // 常量2入栈
istore_1
iconst_3 // 常量3入栈
istore_2
iload_1
iload_2
iadd // 常量2,3出栈,执行相加
istore_0  // 结果5入栈
mov eax,2 // eax 初始值设置为2
add eax,3 // 将寄存器内的值+3

1.6 JVM的生命周期

【启动】
Java虚拟机的启动是通过引导类加载器(Bootstrap class loader)创建一个初始类(initial class)来完成的,这个类由虚拟机的具体实现指定的。
【执行】
一个运行中的Java虚拟机有一个清晰的任务:执行java程序。
程序开始执行时它才运行,程序结束时它就停止。
执行一个Java程序的时候,真正执行的是一个Java虚拟机的进程。
【退出】

  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或者错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止。
  • 某线程调用Runtime类或者System类的exit方法,或者调用Runtime类的halt方法,并且Java安全管理器也允许这次exit或者halt操作。
  • JNI规范描述了用JNI Invocation API来加载或者卸载Java虚拟机时,Java虚拟机退出。

1.7 JVM发展历程

【Sun Classic VM】
1996年,JDK1.0发布了第一款商用的java虚拟机,Sun Classic VM。jdk 1.4的时候被淘汰掉了。
此VM只提供解释器
如果要使用jit编译器,需要进行外挂。一旦使用了JIT,JIT就会接管虚拟机的执行系统。解释器就不再工作。解释器和编译器不能配合工作。
现在HotSpot虚拟机中内置了此虚拟机。

【Exact VM】

  • jdk1.2时提供了此虚拟机。
  • Exact Memory Management:准确式内存管理。 虚拟机可以知道内存中某个位置的数据具体是什么类型。
  • 具备现代高性能虚拟机的雏形:(1)热点探测(2)编译器与解释器的混合工作模式。
  • 只在Solaris平台短暂使用过。 最终被Hotspot取代。

【HotSpot VM】

  • jdk1.3的时候成为了默认的虚拟机。
  • 占有绝对的市场地位,称霸武林。 Oracle jdk和OpenJDK中都是默认的虚拟机。
  • 使用热点探测技术。通过计数器找到最有编译价值的代码,触发即时编译或者栈上替换。通过编译器和解释器协同工作,在最优化响应时间和最佳执行性能中取得平衡。

【JRockit VM】

  • 专注于服务端应用。 不关注启动速度,内部不包含解释器,全部代码依靠即时编译器编译后执行。
  • 最快的JVM

【J9】

  • IBM公司所有
  • 号称是最快的虚拟机。 主要是在IBM的产品上使用效果比较好。

【Azul VM】

  • 与特定硬件平台绑定,软硬件配合的专有虚拟机。
  • 每个Azul VM实例都可以管理至少数十个CPU和数百个GB内存的硬件资源,并提供在巨大的内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特征。

【Liquid VM】

  • 与特定硬件平台绑定,软硬件配合的专有虚拟机。
  • Liquid VM不需要操作系统的支持,或者说它本身实现了一个专用操作系统的必要功能,如线程调度、文件系统和网络支持等。

【Apache Harmony】

  • 它的Java类库代码被吸收进了Android SDK中。

【Micorsoft JVM】

  • 初衷是为了在IE浏览器中支持Java Applets,只能在windows平台下运行,是当时windows平台下性能最好的Java VM
  • 1997年,因为侵犯商标,不正当竞争等罪名,此VM下架了。

【TaobaoJVM】

  • 阿里基于OpenJDK开发了自己定制化的AlibabaJDK
  • 是深度定制且开源的高性能服务器版的Java虚拟机。
  • GCIH(GC Invisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中搬移到heap之外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC回收频率和提升GC的回收效率的目的。
  • GCIH中的对象能够在多个虚拟机进程中实现共享。
  • 严重依赖intel的cpu,损失了兼容性,提高了性能。

【Dalvik VM】

  • Goolge开发的,应用于Android系统,并在Android2.2中提供了JIT
  • Dalvik VM只能称为虚拟机,不能称为“Java虚拟机”,它没有遵循Java虚拟机规范。
  • 执行的是dex(Dalvik Executable)文件,效率较高。dex文件可以通过classes文件转化而来。
  • 基于寄存器的指令架构
  • Android 5.0使用支持提前编译(Ahead of Time Compilation, AOT)的ART VM替换掉Dalvik VM。

【Graal VM】

  • 2018.04 Oracle Labs公开
  • Run Programmes Faster Anywhere
  • 跨语言全栈虚拟机,可以作为“任何语言”的运行平台。包括c和cpp。

2 类加载子系统

2.1 ClassLoader

在这里插入图片描述
ClassLoader是可以有多个的。

文件头特定的文件标识就是“咖啡baby”。
在这里插入图片描述
在这里插入图片描述
所有的类加载器不是继承关系,可以看做是等级关系,永远都是最高等级的bootstrap先加载对象,其加载不了的才轮到后面。

【Bootstrap类加载器】
bootstrap加载器加载jre/lib/rt.jarresources.jar或者sun.boot.class.path路径中的内容,Object类,String类,ArrayList等都是其加载的。
查看启动类加载器的加载路径

URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();

加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。

【Ext类加载器】
扩展类加载器加载的是 jre/lib/ext/*.jar中的内容。
如果用户创建jar包也放在了ext目录中, 也会自动由ext类加载器进行加载。

查看扩展类加载器的加载路径

String extDirs = System.getProperty("java.ext.dirs");
String dirArr = extDirs.split(";");

【系统类加载器】
应用程序类加载器加载Java编码中自定义的对象。
负责加载环境变量classpath或者系统属性java.class.path指定路径下的类库。
系统类加载器是程序中默认的类加载器,一般来说,Java应用的类都有它来完成。
可以通过如下方式获取到此类加载器:ClassLoader.getSystemClassLoader()

验证用例程序

package org.example.classloader;

import com.sun.nio.zipfs.JarFileSystemProvider;

public class ClassLoaderTest {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(o.getClass().getClassLoader()); // bootstrap class loader 获得不到的,返回值为null

        System.out.println("==========================================");

        MyObject myObject = new MyObject();
        System.out.println(myObject.getClass().getClassLoader());
        System.out.println(myObject.getClass().getClassLoader().getParent());
        System.out.println(myObject.getClass().getClassLoader().getParent().getParent());

        System.out.println("==========================================");

        JarFileSystemProvider provider = new JarFileSystemProvider();
        System.out.println(provider.getClass().getClassLoader());
        System.out.println(provider.getClass().getClassLoader().getParent());
    }
}

class MyObject {

}


null
==========================================
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@14ae5a5
null
==========================================
sun.misc.Launcher$ExtClassLoader@14ae5a5
null

从JVM规范的角度上讲,类加载器分为两种:引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-defined ClassLoader)。Java虚拟机规范中将所有的派生于ClassLoader的类加载器全部划归为自定义类加载器。

获取classLoader的方式:

  1. 获取一个类的ClassLoader
clazz.getClassLoader();
  1. 获取当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader();
  1. 获取系统的ClassLoader
ClassLoader.getSystemClassLoader();
  1. 获取调用者的ClassLoader
DriverManger.getCallerClassLoader();

2.2 用户自定义类加载器

2.2.1 为什么需要自定义类加载器

  • 隔离类加载器
  • 修改类加载方式
  • 扩展加载源
  • 防止源码泄漏

2.2.2 自定义类加载器的实现步骤

  1. 继承抽象类java.lang.ClassLoader, 建议把自定义的类加载逻辑放在findClass()方法中。
  2. 如果没有特别复杂的需求,可以直接继承URLClassLoader,这样可以避免自己去写findClass方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

2.3 双亲委派机制

在这里插入图片描述
助记:“往上捅”

用一个案例证明双亲委派机制保证了Java代码的安全性。

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}


错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

双亲委派机制的好处:

  • 避免类的重复加载
  • 保护程序安全,防止核心api被随意篡改。

2.3 类的加载过程

加载(Loading)-> 【验证(Verification)-> 准备(preparation)-> 解析(Resolution)】-> 初始化(Initilization)
--------------------------【这个就是链接过程】

【加载】

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

.class文件的来源:

  • 本地系统中
  • 网络获取,典型场景:Web Applet。
  • 从zip包中读取,是从jar、war中读取的基础。
  • 运行时计算生成,使用最多的是动态代理技术。
  • 由其他文件生成,典型场景:jsp应用
  • 从专有的数据库中提取.class文件,比较少见
  • 从加密文件中获取,典型的防止Class文件被反编译的保护措施。

【验证】

  1. 目的在于确保class文件中包含的信息符合当前虚拟机的要求,保证被加载的类的正确性,不会危害虚拟机自身的安全。
  2. 主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

【准备】

  1. 为类变量分配内存并且设置类变量的默认初始值,即零值
  2. 如果是被final修饰的static变量,就已经是一个常量了,常量在编译阶段已经分配值了,准备阶段会显示初始化。
  3. 这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量会随着对象一起分配到Java堆中。

【解析】

  1. 将常量池内的符号应用转换为直接引用的过程。
  2. 事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行。

【初始化】
4. 初始化阶段就是执行类构造器方法 <clinit>()的过程. <clinit>()不同于类的构造器(关联:构造器是虚拟机视角下的<init>()
5. <clinit>()不需要定义,是由javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 类构造器方法中的指令按照语句在源文件中出现的顺序执行。
6. 若一个类具有父类,JVM会保证子类的<clinit>()执行前,父类的<clinit>()方法已经执行完毕。
7. 虚拟机必须保证一个类的<clinit>()方法在多线程环境下被同步加锁。
8.

public class ClassInitTest {
	static {
		number = 20;
		// 报错:非法的前向引用
		// System.out.println(number);
	}

	// prepare : number = 0;
	// initial : 20 ---> 10
	private static int number = 10; 

	public static void main(String[] args) {
		System.out.println(ClassInitTest.number); // 10
	}
}

2.4 其他

JVM中表示两个class对象是否为同一个类的两个必要条件:

  • 类的完整类名必须一致,包括包名
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同

JVM必须知道一个类型是由启动类加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。 当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

Java程序对类的使用分为:主动使用和被动使用。
主动使用情况:
1 创建类的实例
2 访问某个类或者接口的静态变量,或者对该静态变量赋值
3 调用类的静态方法
4 反射(例如Class.forName(“xxxx”))
5 初始化一个类的子类
6 Java虚拟机启动时被标明为启动类的类
7 JDK7开始提供的动态语言支持:java.lang.invoke.MethodHandle实例的解析结果REF_getStatciREF-putStaticREF-invokeStatic句柄对应的类没有初始化,那么就需要初始化
除了主动使用的情况,都是被动使用,被动使用的时候不会导致类的初始化。

参考材料

[1] https://www.bilibili.com/video/BV1jJ411t71s?p=5&spm_id_from=pageDriver&vd_source=f4dcb991bbc4da0932ef216329aefb60

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值