java运行时数据区域和类结构详解
Java运行时数据区域
java运行时数据区可以分为:方法区、虚拟机栈、本地方法栈、堆和程序计数器
线程私有:虚拟机栈、本地方法栈、程序计数器
线程共享:方法区、堆
程序计数器
一块较小的内存空间,当前线程所执行字节码的行号指示器,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
每条线程都拥有一个独立的程序计数器。
Java虚拟机栈
线程私有的,它的生命周期与线程相同。
每个方法被执行时,java虚拟机都会创建一个栈帧,用于存储 局部变量表、操作数栈、动态链接、方法出口等信息
动态链接:符号在运行中转化为直接引用的过程,就是动态连接(预支对应的静态连接,是指类加载阶段将静态的符号引用转成)。
本地方法栈
作用于java虚拟机栈类似,不过作用的是本地的native 方法。
java堆
线程共享的一块内存区域,用来存放对象实例。“几乎”所有的对象都分配在堆中。
由于及时编译,特别是逃逸分析技术日益请打,对象也不一定分配在堆中(可能栈上分配和标量替换)。
java堆中可以划分出多个线程私有的分配缓冲区(TLAB)来提高对象分配效率,这个TLAB只保证该线程才能在此分配,但是所有线程都是可以进行访问的。
方法区
线程共享,存放虚拟机加载的类型信息、常量、静态变量、即时编辑器编译后的代码缓存等数据。
方法区的运行时常量池:存放 类加载器中加载Class文件中的常量池表。
java对象内存分配
字节码new 指令 -> 检查常量池 ->类加载器(加载、连接(检查、准备、解析)、初始化)
检查后,就要为新生对象进行内存分配了。分配策略:
逃逸分析
分析对象的作用域是否在本方法中,如果只有在本方法中,那么他可以栈上分配,逃逸分析jdk7以后是默认开启的。
new 的对象不一定在堆中,他可能在栈上分配和标量替换
栈上分配:JVM调优方式之一,方法的对象如果不逃逸在外,那么它可以分配在栈上,他的生命周期与方法调用一致,减小GC的压力。
标量替换:如果对象不存在逃逸,JVM可能不会创建该对象,而是将该对象变量分解成若干个成员变量所替换,这样就可以在栈帧或寄存器上分配(不用连续的空间),jdk7默认开启。标量替换优先于栈上分配。
TLAB:线程本地分配缓存区(也是堆中)
Eden中分配内存时,如果多个线程都同时分配内存,会造成指针碰撞情况,为了提高对象分配效率,使用TLAB。
线程初始化时,会申请一点指定大小的内存,只提供当前线程进行内存分配,这样每个线程都单独拥有一个空间。
TLAB是虚拟机在堆内存的eden划分出来的一块专用空间。
TLAB没有没有足够空间来满足操作时,需要向当前线程重新申请新的TLAB
java类文件结构
class 字节码的文件结构,严格按照顺序记性解析
类型 | 名称 | 备注 |
---|---|---|
u4 | magic | 魔数,识别Class文件格式,值为:0XCAFEBABE |
u2 | minor_version | 副版本号 |
u2 | major_version | 主版本号,45-?,JDK13为57,JDK8为52 |
u2 | constant_pool_count | 常量池计算器 |
cp_info | constant_pool | 常量池,class资源库 |
u2 | access_flags | 访问标志,public、final等9个标志。有16个标志位,每一位标识一种访问标志。 |
u2 | this_flags | 类索引,常量池中的索引值 |
u2 | super_class | 父类索引,常量池中的索引值 |
u2 | interfaces_count | 接口计数器 |
u2 | interfaces | 接口索引集合,常量池中的索引值 |
u2 | fields_count | 字段个数 |
field_info | fields | 字段集合, 字段标志(public、static等)、字段名常量索引、描述常量索引(类型) |
u2 | methods_count | 方法计数器 |
method_info | methods | 方法集合,和字段集合差不多,方法标志、方法名索引、方法描述索引(返回类型、方法参数列表) |
u2 | attributes_count | 附加属性计数器 |
attribute_info | attributes | 附加属性集合 |
常量池
常量池分为:字面量和符号引用
字面量:文本字符串、final常量值等
符号引用:
- 类、接口全限定名
- 字段、方法的名称和描述符
- 方法句柄和类型
- 动态调用点和动态常量
常量池项目类型:
属性表
Class 文件、字段表、方法表都可以携带自己的属性表集合,描述某些场景专有的信息
属性(部分)有:
比如Code属性,
类加载机制
类加载过程:
加载 -> 链接 (验证、准备、解析) -> 初始化
加载:用类加载器加载字节码
验证:验证字节码的合法性(满足约束条件)
准备:被加载类的静态字段分配内存
解析:符号引用解析成实际引用。
初始化:初始化常量、静态类
类加载器:
启动类加载器:加载最基础的最重要的类,如JRE的lib下的jar包中的类
扩展类加载器:他的弗雷是启动类加载器,主要加载相对次要但又通用的类,如JRE的lib/ext下的jar的类
应用类加载器:他的父类是扩展类加载器,负责加载应用程序路径下的类。(指虚拟机参数 -cp/-classpath、系统变量 java.class.path或环境变量 CLASSPATH 所指定的路径)
同一字节流经过不同类加载器加载,也会得到两个不同的类。
双亲委派模式:让父加载器尽量加载
双亲委派模式的破坏:
1)如果上层类加载器加载的类 加载 下层的类加载器加载的类
java引入了上下文类加载器,可以打通弗雷加载器去请求子类加载器加载的行为。如JNDI调用服务代码的时候。
2)OSGI热部署,使用网状的类加载模式。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。