技术

学习go 为什么要有堆栈 汇编语言 计算机组成原理 运行时和库 Prometheus client mysql 事务 mysql 事务 mysql 索引 坏味道 学习分布式 学习网络 学习Linux go 内存管理 golang 系统调用与阻塞处理 Goroutine 调度过程 重新认识cpu mosn有的没的 负载均衡泛谈 单元测试的新解读 《Redis核心技术与实现》笔记 《Prometheus监控实战》笔记 Prometheus 告警学习 calico源码分析 对容器云平台的理解 Prometheus 源码分析 并发的成本 基础设施优化 hashicorp raft源码学习 docker 架构 mosn细节 与微服务框架整合 Java动态代理 编程范式 并发通信模型 《网络是怎样连接的》笔记 go channel codereview gc分析 jvm 线程实现 go打包机制 go interface及反射 如何学习Kubernetes 《编译原理之美》笔记——后端部分 《编译原理之美》笔记——前端部分 Pilot MCP协议分析 go gc 内存管理玩法汇总 软件机制 istio流量管理 Pilot源码分析 golang io 学习Spring mosn源码浅析 MOSN简介 《datacenter as a computer》笔记 学习JVM Tomcat源码分析 Linux可观测性 学习存储 学计算 Gotty源码分析 kubernetes operator kaggle泰坦尼克问题实践 kubernetes垂直扩缩容 神经网络模型优化 直觉上理解机器学习 knative入门 如何学习机器学习 神经网络系列笔记 TIDB源码分析 什么是云原生 Alibaba Java诊断工具Arthas TIDB存储——TIKV 《Apache Kafka源码分析》——简介 netty中的线程池 guava cache 源码分析 Springboot 启动过程分析 Spring 创建Bean的年代变迁 Linux内存管理 自定义CNI IPAM 副本一致性 spring redis 源码分析 kafka实践 spring kafka 源码分析 Linux进程调度 让kafka支持优先级队列 Codis源码分析 Redis源码分析 C语言学习 《趣谈Linux操作系统》笔记 docker和k8s安全机制 jvm crash分析 Prometheus 学习 容器日志采集 Kubernetes 控制器模型 Kubernetes监控 容器狂占资源怎么办? Kubernetes资源调度——scheduler 时序性数据库介绍及对比 influxdb入门 maven的基本概念 《Apache Kafka源码分析》——server Kubernetes objects 源码分析体会 《数据结构与算法之美》——算法新解 Kubernetes源码分析——controller mananger Kubernetes源码分析——apiserver Kubernetes源码分析——kubelet Kubernetes介绍 ansible学习 Kubernetes源码分析——从kubectl开始 jib源码分析之Step实现 jib源码分析之细节 线程排队 跨主机容器通信 jib源码分析及应用 为容器选择一个合适的entrypoint kubernetes yaml配置 《持续交付36讲》笔记 mybatis学习 程序猿应该知道的 无锁数据结构和算法 CNI——容器网络是如何打通的 为什么很多业务程序猿觉得数据结构和算法没用? 串一串一致性协议 当我在说PaaS时,我在说什么 《数据结构与算法之美》——数据结构笔记 PouchContainer技术分享体会 harbor学习 用groovy 来动态化你的代码 精简代码的利器——lombok 学习 《深入剖析kubernetes》笔记 编程语言的动态性 rxjava3——背压 rxjava2——线程切换 spring cloud 初识 《深入拆解java 虚拟机》笔记 《how tomcat works》笔记 hystrix 学习 rxjava1——概念 Redis 学习 TIDB 学习 分布式计算系统的那些套路 Storm 学习 AQS1——论文学习 Unsafe Spark Stream 学习 linux vfs轮廓 《自己动手写docker》笔记 java8 实践 中本聪比特币白皮书 细读 区块链泛谈 比特币 大杂烩 总纲——如何学习分布式系统 hbase 泛谈 forkjoin 泛谈 看不见摸不着的cdn是啥 《jdk8 in action》笔记 程序猿视角看网络 bgp初识 calico学习 AQS——粗略的代码分析 我们能用反射做什么 web 跨域问题 《clean code》笔记 《Elasticsearch权威指南》笔记 mockito简介及源码分析 2017软件开发小结—— 从做功能到做系统 《Apache Kafka源码分析》——clients dns隐藏的一个坑 《mysql技术内幕》笔记 log4j学习 为什么netty比较难懂? 回溯法 apollo client源码分析及看待面向对象设计 学习并发 docker运行java项目的常见问题 Scala的一些梗 OpenTSDB 入门 spring事务小结 分布式事务 javascript应用在哪里 《netty in action》读书笔记 netty对http2协议的解析 ssl证书是什么东西 http那些事 苹果APNs推送框架pushy apple 推送那些事儿 编写java框架的几大利器 java内存模型 java exception Linux IO学习 netty内存管理 测试环境docker化实践 netty在框架中的使用套路 Nginx简单使用 《Linux内核设计的艺术》小结 Go并发机制及语言层工具 Linux网络源代码学习——数据包的发送与接收 《docker源码分析》小结 docker namespace和cgroup Linux网络源代码学习——整体介绍 zookeeper三重奏 数据库的一些知识 Spark 泛谈 链式处理的那些套路 netty回顾 Thrift基本原理与实践(二) Thrift基本原理与实践(一) 回调 异步执行抽象——Executor与Future Docker0.1.0源码分析 java gc Jedis源码分析 Redis概述 机器学习泛谈 Linux网络命令操作 JTA与TCC 换个角度看待设计模式 Scala初识 向Hadoop学习NIO的使用 以新的角度看数据结构 并发控制相关的硬件与内核支持 systemd 简介 quartz 源码分析 基于docker搭建测试环境(二) spring aop 实现原理简述 自己动手写spring(八) 支持AOP 自己动手写spring(七) 类结构设计调整 分析log日志 自己动手写spring(六) 支持FactoryBean 自己动手写spring(九) 总结 自己动手写spring(五) bean的生命周期管理 自己动手写spring(四) 整合xml与注解方式 自己动手写spring(三) 支持注解方式 自己动手写spring(二) 创建一个bean工厂 自己动手写spring(一) 使用digester varnish 简单使用 关于docker image的那点事儿 基于docker搭建测试环境 分布式配置系统 JVM执行 git spring rmi和thrift maven/ant/gradle使用 再看tcp 缓存系统 java nio的多线程扩展 《Concurrency Models》笔记 回头看Spring IOC IntelliJ IDEA使用 Java泛型 vagrant 使用 Go常用的一些库 Python初学 Goroutine 调度模型 虚拟网络 《程序员的自我修养》小结 VPN(Virtual Private Network) Kubernetes存储 访问Kubernetes上的Service Kubernetes副本管理 Kubernetes pod 组件 Go基础 JVM类加载 硬币和扑克牌问题 LRU实现 virtualbox 使用 ThreadLocal小结 docker快速入门

架构

学习rpc 可观察性 生命周期管理 openkruise学习 BFF 基于Kubernetes选主及应用 《许式伟的架构课》笔记 Kubernetes webhook 发布平台系统设计 k8s水平扩缩容 Scheduler如何给Node打分 Scheduler扩展 controller 组件介绍 openkruise cloneset学习 kubebuilder 及controller-runtime学习 pv与pvc实现 csi学习 client-go学习 kubelet 组件分析 调度实践 Pod是如何被创建出来的? 《软件设计之美》笔记 mecha 架构学习 Kubernetes events学习及应用 CRI 《推荐系统36式》笔记 资源调度泛谈 系统设计原则 grpc学习 元编程 以应用为中心 istio学习 下一代微服务Service Mesh 《实现领域驱动设计》笔记 serverless 泛谈 《架构整洁之道》笔记 处理复杂性 那些年追过的并发 服务器端编程 网络通信协议 《聊聊架构》 书评的笔记 如何学习架构 《反应式设计模式》笔记 项目的演化特点 反应式架构摸索 函数式编程的设计模式 服务化 ddd反模式——CRUD的败笔 研发效能平台 重新看面向对象设计 业务系统设计的一些体会 函数式编程 《左耳听风》笔记 业务程序猿眼中的微服务管理 DDD实践——CQRS 项目隔离——案例研究 《编程的本质》笔记 系统故障排查汇总及教训 平台支持类系统的几个点 代码腾挪的艺术 abtest 系统设计汇总 《从0开始学架构》笔记 初级权限系统设计 领域驱动理念入门 现有上传协议分析 移动网络下的文件上传要注意的几个问题 推送系统的几个基本问题 用户登陆 做配置中心要想好的几个基本问题 不同层面的异步 分层那些事儿 性能问题分析 当我在说模板引擎的时候,我在说什么 用户认证问题 资源的分配与回收——池 消息/任务队列

招聘Java

招聘Java开发

标签


JVM类加载

2014年10月27日

前言

字节码结构

C在开发层面的平台相关性:C语言实现系统兼容性的思路很简单,那就是通过在不同的硬件平台和操作系统上开发各自特定的编译器,从而将相同的C语言源代码翻译为底层平台相关的硬件指令。虽然这种思路很棒,但是仍然有明显的缺点,当涉及系统调用时,开发者仍然要关注具体底层系统的API。在Linux平台上,开发者需要知道Linux平台所提供的创建线程的接口是pthread_create();而在Windows平台上,开发者需要知道Windows平台所提供的创建线程的接口是CreateThread()。另外,在Linux和Windows平台上,C程序需要引用不同的头文件,并且所调用的创建线程的两种API的入参和返回值也不相同。所以在开发层面上屏蔽底层差异的关键就是中间语言,C可以run anywhere,但不能write once。

以类似C struct 的方式来表达java 字节码文件的结构。

常量池(对应Hotspot C++ constantPoolOop)里放的是字面常量和符号引用

  1. 字面常量主要包含文本串以及被声明为final的常量。
  2. 符号引用包含类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符,因为Java语言在编译的时候没有连接这一步,所有的引用都是运行时动态加载的,所以就需要把这些引用的信息保存在class文件里。

字节码生成——ASM

从编译原理的层面看,生成 LLVM 的 IR 时,可以得到 LLVM 的 API 的帮助。字节码就是另一种 IR,而且比 LLVM 的 IR 简单多了,有ASM/Apache BCEL/Javassist 这个工具为我们生成字节码。ASM是一个开源的字节码生成工具/字节码操纵框架。Grovvy 语言就是用它来生成字节码的,它还能解析 Java 编译后生成的字节码,从而进行修改。

ASM 解析字节码的过程,有点像 XML 的解析器解析 XML 的过程:先解析类,再解析类的成员,比如类的成员变量(Field)、类的方法(Mothod)。在方法里,又可以解析出一行行的指令。

部分生成的字节码

Spring 采用的代理技术有两个:一个是 Java 的动态代理(dynamic proxy)技术;一个是采用 cglib 自动生成代理,cglib 采用了 asm 来生成字节码。Java 的动态代理技术,只支持某个类所实现的接口中的方法。如果一个类不是某个接口的实现,那么 Spring 就必须用到 cglib,从而用到字节码生成技术来生成代理对象的字节码。

系统的根据编程语言代码AST生成字节码

基于 AST 生成 JVM 的字节码的逻辑还是比较简单的,比生成针对物理机器的目标代码要简单得多,为什么这么说呢?主要有以下几个原因:

  1. 不用太关心指令选择的问题。针对 AST 中的每个运算,基本上都有唯一的字节码指令对应,直白地翻译就可以了,不需要用到树覆盖这样的算法。
  2. 不需要关心寄存器的分配,因为 JVM 是使用操作数栈的;
  3. 指令重排序也不用考虑,因为指令的顺序是确定的,按照逆波兰表达式的顺序就可以了;
  4. 优化算法,暂时也不用考虑。

类加载——按类名加载

最初的jdk 根本没有类加载的概念,jdk的核心类库直接调用 ClassFileParser::parseClassFile接口完成加载。加载的本质,从磁盘上加载,得到的是一个字节数组,然后按照自己的内存模型,把字节数组中对应的数据放到进程内存对应的地方。并对数据进行校验,转化解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。

JVM类加载器与ClassNotFoundException和NoClassDefFoundError在”加载“阶段,虚拟机需要完成以下三件事:

  1. 通过一个类的全限定名来获取此类的二进制字节流。类似的 maven的基本概念 中提到URL construction scheme 概念,根据一个jar 的groupId + artifactId + version 即可构造一个http url ,从maven remote Repository 下载jar 文件。
  2. 将字节流代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中创建一个代表此类的java.lang.Class对象,作为方法区此类的各种数据的访问入口。

类加载器的双亲委派模型

ClassLoader源码注释:The ClassLoader class uses a delegation model to search for classes and resources.

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器,类加载器间的父子关系不会以继承关系实现,而是以组合的方式来复用父类加载的代码。通过这种层次模型,规定了类加载器优先级,可以避免类的重复加载,也可以避免核心类被不同的类加载器加载到内存中造成冲突和混乱,从而保证了Java核心库的安全。

双亲委派模型的工作过程:当一个类加载器收到类加载请求的时候,它会首先把这个请求委托给父类加载器去执行,因此所有的类加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器也无法找到时才会交给自己去加载。

Java类加载器 — classloader 的原理及应用使用场景:

  1. 热部署
  2. 热加载 spring boot devtools。
  3. 代码加密。基于java开发编译产生的jar包是由.class字节码组成,由于字节码的文件格式是有明确规范的。因此对于字节码进行反编译,就很容易知道其源码实现了。jar包加密的本质,还是对字节码文件进行加密操作。但是JVM虚拟机加载class的规范是统一的,因此在加载class文件之前通过自定义classloader先进行反向的解密操作,然后再按照标准的class文件标准进行加载
  4. 依赖冲突。阿里 pandora(潘多拉)通过自定义类加载器,为每个中间件自定义一个加载器来 解决依赖冲突

延迟加载

class X{
    static{   System.out.println("init class X..."); }
    int foo(){ return 1; }
    Y bar(){ return new Y(); }
}

The most basic API is ClassLoader.loadClass(String name, boolean resolve)

Class classX = classLoader.loadClass("X", resolve);

If resolve is true, it will also try to load all classes referenced by X. In this case, Y will also be loaded. If resolve is false, Y will not be loaded at this point.

ClassNotFoundException和NoClassDefFoundError

Why am I getting a NoClassDefFoundError in Java?

  1. ClassNotFoundException This exception indicates that the class was not found on the classpath.
  2. NoClassDefFoundError, This is caused when there is a class file that your code depends on and it is present at compile time but not found at runtime. Look for differences in your build time and runtime classpaths. 引起的原因比较少见,还未掌握到精髓。

ClassLoader 隔离

在 Java 虚拟机中,类的唯一性是由类加载器实例以及类的全名一同确定的(即便是同一串字节流,经由不同的类加载器加载,也会得到两个不同的类。猜测一下,如果是一致的,ClassLoader 该如何实现呢?

  1. ClassLoader Class<?> defineClass(String name, byte[] b, int off, int len) 时,如果发现name 相同, 可以直接返回。但ClassLoader 是可以自定义实现的,很难约束开发必须遵守这个规则。
  2. defineClass 时直接覆盖,那问题就更严重了,开发就有机会恶意覆盖 一些已有的java 库中的类的实现。

在大型应用中,往往借助这一特性,来运行同一个类的不同版本。tomcat 在类加载方面就有很好的实践 Tomcat源码分析

Java对象在内存中的表示

磁盘表示 :java 源码文件 ==> 磁盘表示: 字节码文件 ==> 内存的c++表示: oop-kclass struct ==> 内存的二进制表示:数据结构和方法对应的机器指令。

java 对象的C++ 类表示——oop-klass model

当C、C++和Delphi等程序被编译成二进制程序后,原来所定义的高级数据结构都不复存在了,当Windows/Linux等操作系统(宿主机)加载这些二进制程序时,是不会加载这些语言中所定义的高级数据结构的,宿主机压根儿就不知道原来定义了哪些数据结构、哪些类,所有的数据结构都被转换为对特定内存段的偏移地址。例如C中的Struct结构体,被编译后不复存在,汇编和机器语言中没有与之对应的数据结构的概念,CPU更不知道何为结构体。C++和Delphi中的类概念被编译后也不复存在,所谓的类最终变成内存首地址。而JVM虚拟机在加载字节码程序时,会记录字节码中所定义的所有类型的原始信息(元数据),JVM知道程序中包含了哪些类,以及每个类中所关联的字段、方法、父类等信息(类型结构信息被带到了运行期)。这是JVM虚拟机与操作系统最大的区别所在。

在编译期生成的字节码文件中,Java 类结构的信息其实是被抹掉的,谁也无法一眼从二进制格式的字节码文件中看出一个Java 类的结构,但字节码文件通过其本身的格式规范,确保JVM 可以据此还原出原始的Java 类结构。字节码文件的解析包含3个主要的过程:常量池解析;字段解析;方法解析。通过字段解析,jvm 能够分析出java 类所封装的数据结构,通过方法解析 可以分析出java 类所封装的算法逻辑,而前两者很多与字符串 等相关的信息都封装于常量池中,因此要最先解析常量池。当常量池、字段、方法被解析完,则字节码文件的“精华”便被完全消化吸收。

struct iphone6s {
    int length;
    int width;
    int height;
    int weight;
    int ram;
    int rom;
    int pixel;
}
int main(){
    struct iphone6s iphone; // 定义变量
    iphone.length = 138;
    iphone.weight = 64;
    ...
    return 0
}
// 编译为汇编
main:
    pushl %ebp
    movel%esp, %ebp
    subl$32, %esp
    
    movel$138, -28(%ebp)
    movel$67, -24(%ebp)
    ...
    movel$0,%eax
    leave
    ret

正因为JVM 需要保存字节码中的类元信息,所以JVM自然而然就演化出了OOP-KLASS二分模型。KLASS 用来保存类元信息,保存在PERM 永久区,OOP 用来表示JVM所创建的类实例对象,分配在堆区。同时JVM 为了支持反射等技术,必须在OOP 中保存一个只恨,用来指向KLASS,这样就可以在运行期获取类的类型、父类、字段、方法等信息。这些有助于开发具有运行时动态特性的程序,例如根据类型来设计更为抽象和优雅的工厂模式,运行时动态生成字节码并执行其方法(ASM字节码编程),进而使得java 成为各种中间件、框架的首选。

深入理解多线程(二)—— Java的对象模型HotSpot是基于c++实现,而c++是一门面向对象的语言,本身具备面向对象基本特征,所以Java中的对象表示,最简单的做法是为每个Java类生成一个c++类与之对应。但HotSpot JVM并没有这么做,而是设计了一个OOP-Klass Model。

  1. OOP(Ordinary Object Pointer)用来描述对象实例信息
  2. Klass 用来描述java类,是虚拟机内部Java类型结构的对等体 为什么HotSpot要设计一套oop-klass model呢?答案是:HotSopt JVM的设计者不想让每个对象中都含有一个vtable(虚函数表)。oop的职能主要在于表示对象的实例数据,所以其中不含有任何虚函数。而klass为了实现虚函数多态,所以提供了虚函数表。

字节码文件是分段的,加载过程中,也会分段解析 字节码文件来创建和填充 instanceKlass 和methodOop 等, 在Java程序运行过程中,每创建一个新的对象,在JVM内部就会相应地创建一个对应类型的OOP对象。JVM内部定义了各种oop-klass,在JVM看来,不仅Java类是对象,Java方法也是对象,字节码常量池也是对象,一切皆是对象。JVM使用不同的oop-klass模型来表示各种不同的对象。

无论是oop还是klass,基本都被划分来描述instance/method/constantMethod/methodData/array/objArray/typeArray/constantPool/constantPoolCache 等,用来勾画一个java code 的全部:数据、方法、类型、数组和实例。

一个Java对象,它的存储是怎样的?

  1. 一般很多人会回答:对象存储在堆上。
  2. 稍微好一点的人会回答:对象存储在堆上,对象的引用存储在栈上。
  3. 一个更加显得牛逼的回答:对象的实例(instanceOopDesc)保存在堆上,对象的元数据(instanceKlass)保存在方法区,对象的引用保存在栈上。

内存布局

// Klass_vtbl 描述了虚函数表
class Klass : public Klass_vtbl{
    protected: 
        jint _layout_helper;    // 对象布局综合描述
        junit _super_check_offset;
        Symbol* _name;  // 类名
    public: ...
    protected:
        klassOop _secondary_super_cache;
        objArrayOop _secondary_supers;
        klassOop _primary_supers[_primary_super_limit];
        oop _java_mirror;   // 镜像类 Class
        klassOop _super;    // 父类
        klassOop _subklass; // 指向第一个子类
        klassOop _next_sibling; // 指向第一个兄弟节点
        jint _modifier_flags;   // 修饰符标识 例如static
        AccessFlags _access_flags;  // 访问权限标识,例如public
        objectArrayOop _methods   // 方法信息
        typeArrayOop _fields     // 字段信息
        oop   _class_loader     // 类加载器
        typeArrayOop  _inner_classes // 内部类
        int _nonstatic_field_size  // 非静态字段大小
        int _static_field_size  // 静态字段大小
        int vtable_len   // 虚方法表长度
}

instanceKlass 是java 类加载的最终产物,jvm 根据这个数据结构,可以获取java 类所定义的一切元素。jvm 在创建完instanceKlass 之后,又创建了一个与之对等的镜像类java.lang.Class。Class 是为了被java 程序调用,instanceKlass 是为了被jvm 内部访问。

class oopDesc {
 private:
  volatile markWord _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
}

每个 Java 对象都有一个对象头 (object header) ,由标记字段和类型指针构成。java对象头信息是跟对象自身定义的数据结构无关的,这些信息所记录的状态是用于JVM对对象的管理的(比如并发访问与gc)。

  1. 标记字段用来存储对象的哈希码, GC 信息, 持有的锁信息。
  2. 类型指针指向该对象的类 Class。

在 64 位操作系统中,标记字段占有 64 位,而类型指针也占 64 位,也就是说一个 Java 对象在什么属性都没有的情况下要占有 16 字节的空间,当前 JVM 中默认开启了压缩指针,这样类型指针可以只占 32 位,所以对象头占 12 字节, 压缩指针可以作用于对象头,以及引用类型的字段。

以 Integer 类为例,它仅有一个 int 类型的私有字段,占 4 个字节。因此,每一个 Integer 对象的额外内存开销至少是 400%。这也是为什么 Java 要引入基本类型的原因之一。

默认情况下,Java 虚拟机堆中对象的起始地址需要对齐至 8的倍数。如果一个对象用不到 8N 个字节,那么剩下的就会被填充。这些浪费掉的空间我们称之为对象间的填充(padding)。

  1. 对象内存对齐, 这样对象的地址 就可以压缩一下,比如address * 8 得到对象的实际地址。
  2. 对象字段内存对齐(有六七个对齐规则),让字段只出现在同一 CPU 的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。
  3. Java 虚拟机重新分配字段的先后顺序,以达到内存对齐的目的

其它

《揭秘Java虚拟机:JVM设计原理与实现》Java选择具备运行时类型识别的特性本身便从一个十分隐晦的层面制约了Java必须选择成为一门面向对象的编程语言,为何?类型本身就是一种“闭包”的技术手段,只有先从语法层面实现了“闭包”,才能实现“对象”的概念,否则,何来的属性、成员变量、类方法一说?类型是实现将若干属性和动作打包成为一个整体对象进行统一识别的策略。如果Java像C++那样,类型不作为属性和方法封装的唯一手段,开发者可以随心所欲地在类的外面定义变量和函数,那么对于这部分数据的“运行时识别”必然是一个难题,可能需要通过类似namespace或者filename这样的机制去实现动态反射了,但是这种反射想想都让人头大,不容易啊!

当一门编程语言实现了完全的闭包语法策略(使用类型包装可以认为是闭包的一种),便自然而然具备了自动内存管理的技术基础,或者说实现自动内存管理更加容易。所以闭包便成为很多具备自动内存回收特性的编程语言的语法基础,例如GO语言、Phthon、JavaScript等,虽然大家具体实现闭包的手段不同,但是殊途同归,都是为了能够让虚拟机在自动回收内存时尽量简单。

import 语句仅仅是个语法糖,且为了不写那一长串的全限定名,并没有任何关联的运行时行为,更不会导致类的加载,纯粹是为了方便写代码。