JVM类加载,内存解析,分配
JVM类加载机制
类加载的时机
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段
类加载的过程
主要包括五个步骤:
-
加载
通过一个类的全限定名来获取定义此类的二进制字节流 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
-
验证
验证二进制文件准确性
-
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段; 例如:public static int value = 123; 该阶段赋值为0;
-
解析
将常量池内的符号引用替换为直接引用的过程
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在
-
初始化
会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源 例如:public static int value = 123; 该阶段赋值为123;
类加载器
-
引导类加载器/启动类加载器
加载 JDK 下 lib 包中的核心类文件 例如: rt.jar、tools.jar
-
扩展类加载器
加载 JDK 下 lib 包下ext文件下类文件
-
应用程序类加载器
加载 ClassPath 路径下的类文件
-
自定义类加载器
-
双亲委派机制
设计双亲委派模型有两个原因: 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
应用程序类加载器 在加载时会优先往上找
查看扩展类加载器是否已加载类, 已加载则直接返回,未加载则继续向上找
查看启动类加载器是否已加载类,已加载直接返回, 依旧未加载时,才使用应用程序类加载器加载类(存在自定义类加载器时,也是依次向上找.)
双亲委派机制的核心代码如下:
-
破坏双亲委派机制
在Tomcat里,一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本, 不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离. 此时双亲委派机制不适用与此情况,就需要打破机制. 其实就是破坏其核心代码即可
Tomcat中有自己定义的类加载器
JVM结构及内存模型
Java技术体系
JVM虚拟机结构图
内存模型
堆
虚拟机所管理的内存中最大的一块.Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建.此内存区域的唯一目的就是存放对象实例,Java世界里“几乎”所有的对象实例都在这里分配内存.
- 年轻代
- Eden : 通常对象会先存放的区域.
- survivo区 : 经过至少一次 minor gc 存放区域
- 老年代 : 大对象,长时间存活的对象存放区域
方法区(元空间)
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据.
Java虚拟机栈
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同.虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧.
- 栈帧
- 局部变量表: 是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量。在Java程序被编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量
- 操作栈: 它是一个后入先出(Last In First Out,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中。操作数栈的每一个元素都可以是包括long和double在内的任意Java数据类型。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。Javac编译器的数据流分析工作保证了在方法执行的任何时候,操作数栈的深度都不会超过在max_stacks数据项中设定的最大值
- 动态链接: 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)
- 方法返回地址: 一个方法开始执行后,只有两种方式退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者(调用当前方法的方法称为调用者或者主调方法),方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为“正常调用完成”
本地方法栈
与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地方法服务.
程序计数器
是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器.
内存分配
对象创建
-
类加载检查
new 对象时, 会检查参数是否命中常量池,和该类是否已加载,如果都没有,则需要先执行类加载过程
-
分配内存
在内存开辟空间
-
分配内存的方式
- 指针碰撞
堆内存是规整有序的,使用的放在一边排齐,空闲则再另一边,那就只需要有一个指针在分割线处,每次碰撞都是接向空闲区域挤出所需要的空间即可 - 空闲列表
堆内存并不规整,则需要维护一张空闲内存表,将足够大的空间划分给所需要的内存,并记录使用信息
- 指针碰撞
-
栈上分配
内存分配通常都是在堆上分配的,依靠GC回收
为减少临时对象在堆中的回收次数-
逃逸分析
指对象是否被外部访问,被外部访问的对象则被定为逃逸对象,
未逃逸对象则都属于临时对象,则在栈内进行分配内存,执行完后立即释放,从而减少GC次数.提高性能
-XX:+/-DoEscapeAnalysis 开启/关闭 逃逸分析功能(jdk7 之后是默认开启的) -
标量替换
通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配
开启/关闭标量替换参数(-XX:+/-EliminateAllocations),JDK7之后默认开启 -
标量与聚合量
标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量
栈上分配完全取决于逃逸分析与标量替换,其中有一个不开启,栈上分配都无效
-
-
-
初始化
为对象设置零值(默认值)
-
设置对象头
对象整体而言包括三部分:对象头、实例数据、对其填充
对象头主要包括两部分- Mark Word:用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等
- 类型指针(Klass元空间而非Class堆):即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
这里为了节省空间,64位JVM默认开启了指针压缩。可以通过对对象指针的存入堆内存时压缩编码、取出到cpu寄存器后解码方式进行优化(对象指针在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的内存配置(小于等于32G)
-
对象内存回收
不能再被任何途径使用的对像进行回收
-
引用计数法
- 给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效,计数器就减1;任何时候计数器为0 的对象就是不可能再被使用的
- 处理不了相互引用的问题.A对象 引用 B, B引用 A;这就导致计数不会 0
-
可达性分析算法
- 设定一个 root节点(gc roots),从第一个引用开始从子节点散开, 在计算是否为垃圾对象时,只需要判断是否有gc roots即可.没有的就为垃圾对象可进行回收
-
常见的引用类型
- 强引用:普通的变量引用; T t = new T();
- 软引用:将对象用SoftReference软引用类型的对象包裹,正常情况不会被回收,但是GC做完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
- SoftReference t = new SoftReference(new U());
- 例如浏览器的回退功能,缓存的页面内容,但不缓存也不要紧,可以重新访问链接展示页面,这种缓存页面就属于软引用.可有可无的对象
- 弱引用:将对象用WeakReference软引用类型的对象包裹,弱引用跟没引用差不多,GC会直接回收掉,很少用
- WeakReference t = new WeakReference(new U());
- 虚引用:虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系,几乎不用
-
-
垃圾收集算法
- 清除算法: 分为两个阶段:标记存活的对象,统一回收所有未被标记的对象。缺点:执行效率不稳定,标记和清除两个过程的执行效率都随对象数量增长而降低;内存空间的碎片化问题
- 复制算法: 将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。缺点是空间浪费。改进:把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor区
- 整理算法(能增加吞吐量): 其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存.