JVM
2023-04-15 23:11:54 1 举报
AI智能生成
JVM思维导图,不包含GC
作者其他创作
大纲/内容
五、执行引擎
解释器interpreter
概念
将代码一行行解析,启动直接运行,但速度慢,而且会有重复代码
流程
解释器:输入的代码 -->[解释器 解释执行] -->执行结果
JIT编译器:输入的代码 -->[编译器 编译] -->编译后的代码 -->[执行] -->执行结果
JIT执行编译后的代码比解释器快,但如果代码只执行一次(如只被调用一次,例如类的构造器(class initializer或者没有循环的代码)则解释器比JIT编译器快
编译器JIT compiler
概念
解析器在解析代码时,当虚拟机发现某些代码运行比较频繁时(也就是热点代码),JIT即时编译器就会把这些代码片段全部编译打包成可执行文件。开始执行时间会比较晚
检测热点代码
回边计数器 --> 就是计算循环体执行的次数
方法调用计数器
优化代码
逃逸分析、锁消除、锁膨胀、方法内联、空值检查消除、类型检测消除、公共子表达式消除
逃逸分析
概念
通俗来讲,当对象的指针被多个方法或线程引用,称为逃逸分析
通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上
同步消除
JIT判断同步块里的锁对象如果只被一个线程调用(假如对象是在方法中,并且存在同步锁),JVM会消除该锁,以此来提高性能。(JDK8开始默认开启锁消除)通过-XX:+EliminateLocks可以开启同步消除。
标量替换
JIT发现当一个对象不会被外界访问到,就会将对象分成若干个标量(若一个数据已经分解到不能再小了,这种称为标量。如果可以再继续分解则称为聚合量),比如基本数据类型
栈上分配
如果一个对象没有发生逃逸,那这个对象会被标量替换,这个对象的成员信息会被分配到线程栈,而不会在堆上分配,这样对象会随着栈帧的销毁而销毁,从而减少GC的压力,节约空间,提升性能
分类
HotSpot集成了两个JIT compiler ---- C1及C2(或称为Client及Server,C++实现)目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用 "-client" 或 "-server" 参数去强制指定虚拟机运行在Client模式或Server模式。
参数
开启逃逸分析:-XX:+DoEscapeAnalysis(JDK8中,默认开启)
关闭逃逸分析:-XX:-DoEscapeAnalysis
查看逃逸分析结果:-XX:+PrintEscapeAnalysis
C1(Client Compiler)
运行在客户端,编译速度快,输出代码优化程度较低
C2(Server Compiler)
运行在服务端,编译速度慢,输出代码质量好
Graal编译器(JDK10)
替代C2编译器
四、对象
对象的创建过程
JVM接收new指令,类加载检测
①检测new指令的参数是否能在常量池中定位到类的符号引用。
②检测这个符号引用代表的类是否已被过加载、解析和初始化,没有则先对该类进行类加载
对象内存分配
创建一个对象所需要的内存(在类加载完成时就能确定),具体的分配方式根据堆内存的整齐性确定,而堆内存的整齐性则由当前程序采用GC算法决定。
分配方式
①指针碰撞(堆整齐)
②空闲列表(堆不整齐)
并发环境下的对象创建问题
问题描述
对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况
问题解决
1. CAS+失败重试机制保证更新操作的原子性(CAS自旋)
2. 本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),每个线程在Java堆中预先分配一小块内存
值初始化
JVM会初始化分配好的内存,将其设为零值(不包括对象头,如果使用了TLAB, 这一步会提前到内存分配阶段进行)
设置对象头
①markword:储存对象自身的运行时数据,如:hashcod、GC分代年龄、锁标志、锁信息等;
②klass pointer:类型指针,指向它对应的类元数据,JVM用这个确定属于哪个类的实例
执行<init>
最后执行<init>函数,也就是类的构造函数,主要是堆属性赋值
对象在内存的存储结构
对象头(Header)
存储信息
1.MarkWord(8字节)
存储对象自身的运行时数据,如哈希码、对象分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;
2.Klasspointer
类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
3.Array Length
如果对象是一个Java数组,那么对象头还必须有一块用于记录数组长度的数据。因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组大小
指针压缩
64位不开指针压缩的问题
占用更多内存,容易内存溢出
存储数据的空间相应变少,GC概率增大,频率增加
概念
在64位的虚拟机中,对象头的标记字段占64位,而类型指针又占64位。一个对象额外占用的字节就是16个字节。以Integer对象为例,它仅有一个int类型的私有字段,占4个字节。因此,每个Integer的额外开销至少400%,这也就是Java为什么要引入基本数据类型的原因之一。为了减少内存开销,64位Java虚拟机引入了压缩指针概念(对应虚拟机选项 -XX:+UseCompressedOops,默认开启),将堆中原本64位的Java对象指针压缩成32位的。这样一来,对象头的类型指针也会被压缩成32位,使得对象头大小从16字节降低为12字节。压缩指针不仅可以作用对象头的类型指针,还可以作用引用类型的字段,引用类型的数组
原理
默认情况下,Java虚拟机中32位的指针可以寻址到2的32次方,也就是32GB的内存空间(超过32位会关闭压缩指针)。在对压缩指针解引用时,我们需要将其左移3位,再加上一个固定的偏移量,便可以寻址到32GB地址空间伪64位指针了。
关闭指针压缩
参数
-XX:-UseCompressedOops
实例数据(Instance Data)
对象真正存储的有效信息
对齐填充(Padding)
自动内存管理系统要求对象的大小必须是8字节(一缓存行大小)的整数倍,避免一缓行存多个对象,其中一个或多个对象修改导致所有对象频繁重新存入缓存行中
一个 Java 空对象Object o = new Object() 在内存占多少字节?
16字节(markword:8个字节+class pointer:4个字节+对齐补充:4个字节)
新生代与老年代的对象存储变换
在新生代达到一定条件的对象会进入老年代
1.S0和S1区域交换一次,年龄+1。当对象年龄达到15岁进入老年代(cms默认为6岁)
2.大对象直接进入(可设置大小判断标准)
3.S区相同年龄的对象大小总和超过S区空间一半,大于等于该年龄的所有对象直接进入
对象的生命周期
一、创建阶段
创建对象
二、应用阶段
特征
①系统至少维护着对象的一个强引用(Strong Reference)
②所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))
引用级别
强引用<--软引用<--弱引用<--虚引用
强引用(Strong Reference)概念
常见的new出来的对象,只要一直被引用,就不会被垃圾收集器回收所以在编码的时候,如果确定对象不再使用后,可以显式的将对象的引用清空:object=null,这样方便在GC时直接清除
软引用(Soft Reference)概念
是指使用SoftReference类型修饰对象,快要发生内存溢出前就会被清理的,适用于缓存(可以用来实现网页缓存,图片缓存)
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.List;
/**
* 对象引用
* VM Args: -Xms10m -Xmx10m 堆内存
*/
public class ReferenceTest {
// 定义List集合
private static List<Object> list = new ArrayList<Object>()
// 软引用测试
private static void testSoftReference() {
// 循环创建软引用对象
for (int i = 0; i < 10; i++) {
byte[] buff = new byte[1024 * 1024];
SoftReference<byte[]> sr = new SoftReference<byte[]>(buff);
list.add(sr);
}
System.gc(); //主动通知垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((SoftReference) list.get(i)).get();
System.out.println(obj);
}
}
public static void main(String[] args) {
testSoftReference();
}
}
弱引用(Weak Reference)概念
指使用WeakReference类型修饰的对象,每次GC都会回收弱引用,适用于threadLocal
private static void testWeakReference() {
// 循环创建弱引用对象
for (int i = 0; i < 10; i++) {
byte[] buff = new byte[1024 * 1024];
WeakReference<byte[]> sr = new WeakReference<byte[]>(buff);
list.add(sr);
}
System.gc(); // 主动通知垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((WeakReference) list.get(i)).get();
System.out.println(obj);
}
}
虚引用(Phantom Reference)概念
指使用PhantomReference类型修饰的对象,不过在使用虚引用的时候是需要配合ReferenceQueue引用队列才能联合使用。虚引用的唯一作用是管理堆外内存,因为JVM无法直接清理堆外内存,所以提供一个虚引用,交给垃圾收集器的回收队列,这个队列就是用来标记哪些堆外内存需要回收,再调用C++释放空间
private static void testPhantomReference() {
ReferenceQueue queue = new ReferenceQueue();
// 循环创建弱引用对象
for (int i = 0; i < 10; i++) {
byte[] buff = new byte[1024 * 1024];
PhantomReference<byte[]> sr = new PhantomReference<byte[]>(buff, queue);
list.add(sr);
}
System.gc(); // 主动通知垃圾回收
for(int i=0; i < list.size(); i++){
Object obj = ((PhantomReference) list.get(i)).get();
System.out.println(obj);
}
System.out.println("--------------------------------------");
// 当虚引用被回收后,可以拿到被回收的对象
Reference ref;
while ((ref = queue.poll()) != null) {
System.out.println("被回收了: " + ref);
}
}
三、不可见阶段
概念
不可见阶段的对象在虚拟机的对象根引用集合中再也找不到直接或者间接的强引用,最常见的就是线程或者函数中的临时变量。程序不在持有对象的强引用
GC(Garbage Collector) roots
Class
由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象
Thread
活着的线程
Stack Local
Java方法的local变量或参数
JNI Local
JNI方法(即一般说的Native方法)的local变量或参数
JNI Global
全局JNI引用
Monitor Used
用于同步的监控对象
四、不可达阶段
概念
指对象不再被任何强引用持有,GC发现该对象已经不可达
如何确定一个对象是垃圾?
引用计数法
概念
对于每个对象都自带一个程序计数器,有对象引用自己时+1,取消引用时-1。当计数器为0时,该对象标记为垃圾可以被毁收
弊端
如果AB相互持有引用(循环引用),导致永远不能被回收,导致内存泄漏
可达性分析(图1246可达)
概念
通过GC Roots(栈中变量/方法区静态变量/Monitor持有者对象/常驻异常对象/JVM内部引用/方法区常量)的对象作为起始点向下搜索,搜索走过的路径被称为引用链,当一个对象到GC roots没有任何引用链时,即根不可达时该对象则被判断为不可用的可以被回收
五、收集阶段
条件
GC发现对象处于不可达阶段并且GC已经对该对象的内存空间重新分配做好准备,对象进程收集阶段。如果,该对象的finalize()函数被重写,则执行该函数
finalize()
如果对象被判定为确有必要执行finalize()方法,那么该对象将会被放置在一个名为F-Queue的队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize()方法,垃圾收集器将对该对象进行第一次标记,不做回收
若对象重新与引用链上的任何一个对象建立关联,即可达,就可以避免被垃圾收集器进行第二次回收时回收(任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行)
重写finalize的弊端(被try/catch/finally替代)
1.会影响到JVM的对象以及分配回收速度
2.可能造成对象再次复活,复活后的对象不属于代码管理的范畴
重写finalize()
public class Finalize {
private static Finalize save_hook = null;//类变量
public void isAlive() {
System.out.println("我还活着");
}
@Override
public void finalize() {
System.out.println("finalize方法被执行");
Finalize.save_hook = this;
}
public static void main(String[] args) throws InterruptedException {
save_hook = new Finalize();//对象
//对象第一次拯救自己
save_hook = null;
System.gc();
//暂停0.5秒等待他
Thread.sleep(500);
if (save_hook != null) {
save_hook.isAlive();
} else {
System.out.println("好了,现在我死了");
}
//对象第二次拯救自己
save_hook = null;
System.gc();
//暂停0.5秒等待他
Thread.sleep(500);
if (save_hook != null) {
save_hook.isAlive();
} else {
System.out.println("我终于死亡了");
}
}
}
六、终结阶段
对象的finalize()函数执行完成后,对象仍处于不可达状态,该对象进程终结阶段
七、空间重分配阶段
GC对该对象占用的内存空间进行回收或者再分配,该对象彻底消失
对象的三种状态
1. 可触及态(可达状态)
从根节点开始,可以搜索到这个对象,即可以访问到这个对象
2. 可复活态
从根节点开始,无论如何都不能访问到这个对象,即这个对象的所有引用都被释放,没有任何变量引用该对象了,但是该对象有可能在finalize()方法中再次被引用,从而复活
3. 不可触及态(不可达状态
对象的所有引用都被释放了,并且在对象的finalize()方法中没有复活
变量
局部变量(方法体中声明)
主要存储在栈内存
成员变量(方法体外声明)
静态变量(修饰符有static):主要存储在堆内区(JDK1.8之后)内存,只有一份
实例变量(修饰符没有static):主要存储在堆内存
一、字节码指令集(二进制展示)
1. 计算机原理层面的字节码指令
在计算机中,CPU指令就是指挥机器工作的指令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是执行指令的过程,也就是计算机的工作过程
通常一条CPU指令包括两方面的内容
操作码
操作码表示要完成的操作
操作数
操作数表示参与运算的数据及其所在的单元地址(这个单元地址可以是寄存器、内存等
2. Java虚拟机中的字节码指令(与CPU中指令类似)
2.1 概念
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成
.class文件
打开是二进制文件,可转化为16进制,每个16进制表示二进制流,代表一个操作指令
cafe baby 代表Java文件
0X34代表JDK8版本
2.2 JVM作用
编译执行字节码
2.3 JVM中的助记符
ICONST_0 -0000 0011 - Ox03
ALOAD_0 -0010 1010 -Ox2a
POP -0101 0111 - Ox57
A代表引用类型,I代表整数类型操作 其它类型均是其类型的首字母
2.4 字节码指令集
2.4.1 加载或存储指令
1. 将局部变量加载到操作栈中(ILOAD、ALOAD)
2. 从操作栈顶存储到局部变量表中(ISTORE、ASTORE)
3. 将常量加载到操作栈顶,极为高频
(ICONST、BIPUSH、SIPUSH、LDC)
(ICONST、BIPUSH、SIPUSH、LDC)
ICONST
加载的是1~5的数(!CONST与BIPUSH的加载界限)
BIPUSH
即Byte Immediate PUSH,加载-128~127之间的数
SIPUSH
即Short Immediate PUSH,加载-32768~32767之间的数
LDC
即Load Constant,在-2147483648~2147483647或者是字符串时,
JVM采用LDC指令压入栈中
JVM采用LDC指令压入栈中
2.4.2 指令运算
对两个操作栈帧上的值进行运算,并把结果写入操作栈顶,如IADD、IMUL等
2.4.3 类型转换指令
显式转换两种不同的数值类型 如I2L、D2F等
2.4.4 对象创建与访问指令
创建对象
NEW、NEWARRAY
访问属性
GETFIELD、PUTFIELD、GETSTATIC
检查实例类型指令
INSTANCEOF、CHECKCAST
2.4.5 操作栈管理指令
出栈
POP、POP2
复制栈顶元素并且入栈
DUP
2.4.6 方法调用与返回
INVOKE
invokeStatic
调用静态方法
invokeVirtual
用于调用对象的实例方法,根据对象的实际类型进行分派(多态,final,非静态)
ArrayList<String> list2 = new ArrayList<>();
list2.add("a");
invokeInterface
用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
List<String> list = new ArrayList<>();
list.add("a");
invokeSpecial
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
invokeDynamic
lambda,或者反射
RETURN
返回VOID类型
Java文件编译成字节码的编译过程
Java文件-->词法解析-->语法解析-->语义分析-->生成字节码
词法解析
通过空格分隔出单词、操作符、控制符等信息,将其形成token信息流,传递给语法解析器
语法解析
将词法解析得到的token信息流按照Java语法规则组装成一颗语法树
语义分析
检查关键字使用是否合理、类型是否匹配、作用域是否正确,语义分析完成后可生成字节码
字节码加载
解释执行
JIT编译执行
两者混合
解释器在启动时先解释执行,省去编译时间。JVM通过热点代码统计分析,识别高频的方法调用、循环体、公共模块等,基于强大的JIT动态编译技术,将热点代码转换成机器码,直接交给CPU执行
二、类加载
1. 类加载的生命周期
1.1 加载Load
读取类文件产生的二进制字节流并转换为特定的数据格式初步校验cafe baby、版本值、常量、文件长度、父类,然后创建对应的java.lang.Class类型
1.2 连接(Link)
验证(Verification)
校验目标类是否合规
准备(Preparation)
静态成员变量赋默认值(可能为null或0)
解析(Resolution)
类、方法,属性等符号引用(二进制字节码,例:java.lang.Object)解析为直接引用
1.3 初始化(initializing)
执行类构造器clinit方法
如果其中赋值依赖其他类,会立刻解析其他类
2. 类加载器(ClassLoader)
类加器具有等级制度,非继承关系,是以组合的关系来进行复用
全盘负责
当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非强调使用另外一个类加载器来载入
沙箱安全机制
目的
限制类在Java虚拟机中的唯一性例,防止恶意代码的加载,保证了JDK代码的安全性
例子
自定义一个Java.lang.String类,若没有规定不同类加载器对应加载不同类,那单一类加载器就要对自定义的String类与Sun定义的官方String类的优先级加载进行判断,影响效率.并且自定义的String类容易加上侵入代码,影响调用String类的程序的安全性
缓存机制
目的
确保所有加载过的Class都将在内存中缓存
当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改Class后,必须重启JVM,程序的修改才会生效。对于一个类加载器实例来说,相同全名的类加载一次,即loadClass方法不会被重复调用。
双亲委派机制
定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就返回成功;只有父类加载器无法完成此加载任务时,才由下一级去加载
流程
打破双亲委派
打破原因
JDK只能提供一个规范接口,而不能提供实现。提供实现的是实际的数据库提供商。提供商的库不可能放在JDK目录,但双亲委派机制限定类在JDK包类中存在,所以要打破。
实现方式
自定义类加载器
实现
自定义类加载器:继承classLoader,重写findClass
SPI(服务提供者接口,Service Provider Interface)
JDK提供接口,供应商提供服务。编程人员编码时面向接口编程,然后JDK能够自动找到合适的实现
Java在核心类库中定义了许多接口,并且还给出针对这些接口的调用逻辑,然而并未给出实现。
开发者要做的就是定制一个实现类,在META-INF/services中注册实现类信息,以供核心类库使用。
比如JDBC中的DriverManager
开发者要做的就是定制一个实现类,在META-INF/services中注册实现类信息,以供核心类库使用。
比如JDBC中的DriverManager
OSGi(开放服务网关协议,Open Service Gateway Initiative)
Java动态化模块化系统的一系列规范,实现代码热部署,代码热替换
Tomcat
Tomcat的类加载机制
Tomcat为什么要打破双亲委派
1. 一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立、相互隔离的
2. web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离
3. 部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM
4.支持修改JSP之后不用重启,让每一个JSP都有自己的类加载器,修改之后直接卸载掉这JSP文件的类加载器
打破双亲委派方式
1.不让父类加载:不重写findClass,而是直接重写loadClass方法也就是类加载方法,不委派给双亲加载(重写的时候就是把自带的去查找父类的代码删除,直接判断没有加载过就去自己的路径加载就可以,省去递归调用父类加载器的过程)
2.主要系统的核心类自己是不能加载的,还是需要借助父类加载器,所以需要加一个if判断是不是自己需要加载的类,不是的话直接委托给父类,是的话就直接自己加载
实现
CommonLoader
Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问
CatalinaLoader
Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
SharedLoader
各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader
各个Webapp私有的类加载器,加载路径中的class只对当前 Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;
JasperLoader
JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例, 并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
系统类的加载
如果是自己规定的文件夹下的文件,就不交给父类加载而是直接自己加载,不是这些类的还是使用双亲委派
总结
两个类对象是不是同一个,不仅仅是包名和类名,还要类加载器
获取类加载器的四种方式
获取当前类的类加载器
clazz.getClassLoader()
获取当前线程上下文的类加载器
Thread.currentThread().getContextClassLoader
获取系统的类加载器
ClassLoader.getSystemClassLoader()
获取调用者的类加载器
DriverManager.getCallerLoader()
描述一下 JVM 加载 class 文件的原理机制
答题思路
1.类 加载-->连接-->初始化
2.加载部分拓展
类加载器-->加载器内容-->双亲委派机制
3.连接部分拓展
连接-->验证-->准备(为静态变量分配内存并设置默认初始值)-->解析(将符号引用替换为直接引用)
4.初始化部分拓展
初始化-->
1.类存在直接的父类且该类没被初始化,初始化父类
2.类中存在初始化语句,依次执行初始化语句
三、JVM内存结构(重要)
1. 线程共有
1.1 堆内存
概念
JVM管理的内存中最大的存放的都是对象的实例(new 的对象,数组)被所有线程共享的区域,在虚拟机启动时创建
内存分区
1. 新生代
Eden(伊甸园):占堆8/10空间
From Survivor(幸存者Survivor0):占堆1/10空间
To Survivor(幸存者Survivor1):占堆1/10空间
2. 老年代
接收新生代存不下的对象
老年代也放不下会触发FGC,如果依旧放不下会触发OOM
参数-XX:+HeapDumpOnOutOfMemoryError
让JVM遇到OOM异常时能输出堆内信息,特别是对相隔数月才出现的OOM异常尤为重要
3. TLAB(Thread Local Allocation Buffer)
概念
在Eden区为每个线程开辟的一个缓冲空间,线程在创建对象时如果该缓冲区的大小能够承载对象大小则直接在该区域分配对象,能够避免多个线程同时创建对象时,由于竞争同一块堆内存时产生的资源消耗(避免全局锁)
为什么有TLAB
1. 堆区是线程共享区域,任何线程都可以访问到队伍中的共享数据
2. 由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的
3. 为避免多个线程操作同一地址,需要使用加锁机制,进而影响分配速度
参数
-XX: +/-UseTLAB
启动关闭TLAB
-XX:TLABWasteTargetPercent
设置TLAB占Eden空间大小
4. 字符串常量池
设计思想
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化
实现
(1)为了字符串开辟一个字符串常量池,类似于缓存区;
(2)创建字符串常量时,首先查询字符串常量池是否存在该字符串;
(3)存在该字符串,返回引用实例,不存在,实例化该字符串(即创建字符串对象)并放入池中;
字符串常量池的位置
JDK1.6及之前
永久代
JDK1.7
堆内存
常见三种字符串操作
(1)String s = "XXX";
只在字符串常量池
创建对象s时,JVM会先去常量池中通过equals(key)方法,判断是否有相同的对象:如果有,则直接返回该对象在常量池中的引用,如果没有,则会在常量池中创建一个新对象,再返回引用。这种方式创建的字符串对象,只会在常量池中。s最终指向常量池中的引用。
(2)String s = new String("XXX");
在堆和字符串常量池
这种方式会保证字符串常量池和堆中都有这个对象,没有就创建,最后返回堆内存中的对象引用。因为有"XXX"这个字面量,所以会先检查字符串常量池中是否存在字符串"XXX":不存在,先在字符串常量池里创建一个字符串对象"XXX",再去堆内存中创建一个字符串对象"XXX";存在,就直接去堆内存中创建一个字符串对象"XXX",最后,将堆内存中的引用返回。注:如果"XXX"这个字面量在字符串常量池中存在,则只会创建一个对象(在堆中创建),如果不存在,则会创建两个对象(在字符串常量池和堆中均创建)。
(3)String s1 = new String("XXX");String s2 = s1.intern();
String中的intern方法是一个native的方法,当调用intern方法时,如果常量池中已经包含一个等于此String对象的字符串(用equals(object)方法确定),则返回常量池中的字符串。否则,将intern返回的引用指向当前字符串s1(JDK1.6版本需要将s1复制到字符串常量池里)。
面试题
String s1 = new String("he") + new String("llo");
String s2 = s1.intern();System.out.println(s1 == s2);//true
在JDK1.6下输出是false,创建了6个对象,因为s1.intern会将字符串"hello"放入到字符串常量池,并创建新对象,返回新对象的引用地址,所以为false
在JDK1.7及以上的版本输出是true,创建了5个对象,因为s1.intern()返回的是s1对象的引用地址,并没有创建新对象,所以为true
String s = new StringBuffer("计算机").append("软件").toString();
System.out.println(s.intern() == s);//true
String s1 = new StringBuffer("ja").append("va").toString();
System.out.println(s1.intern() == s1);//false
在JDK1.6下,两个都为false
在JDK1.7及以上的版本一个为true,一个为false(java这个字符串在执行StringBuilder.toString()之前就已经出现过)
String s0 = "zhuge";String s1 = "zhuge";
String s2 = "zhu" + "ge";
System.out.println(s0 == s1);//true
System.out.println(so == s2);//true
因为例子中的s0和s1中的"zhuge"都是字符串常量,它们在编译期就被确定了,所以s0 == s1 为 true;而 "zhu" 和 "ge"也都是字符串常量,当一个字符串由多个字符串常量连接而成时,它自己肯定也是字符串常量,所以s2也同样在编译期就被优化为一个字符串常量 "zhuge",所以s2也是常量池 "zhuge" 的一个引用。所以我们得出s0 == s1 == s2;
String s0 = "zhuge";String s1 = new String("zhuge");
String s2 = "zhu" + new String("ge");
System.out.println(s0 == s1); //false
System.out.println(s0 == s2); //false
System.out.println(s1 == s2); //false
用new String()创建的字符串不是常量,不能在编译期就确定,所以new String()创建的字符串不放入常量池中,它们有自己的地址空间。s0还是常量池中 "zhuge" 的引用,s1因为无法在编译期确定,所以是运行时创建的新对象"zhuge"的引用,s2因为有后半部分new String("ge"),所以也无法在编译期确定,所以也是一个新创建对象"zhuge"的引用;明白了这些也就知道为何得出此结果了。
String s = "a" + "b" + "c";//就等价于String s = "abc";
String a = "a";
String b = "b";
String c = "c";
String s1 = a + b + c;
System.out.println(s == s1); //false
s1这个就不一样了,可以通过观察其JVM指令码发现s1的 "+" 操作会变成如下操作:StringBuilder temp = new StringBuilder();temp.append(a).apppend(b).append(c);String s = temp.toString();这就是符号 "+" 在JVM底层的源码操作,所以由此可知,当用符号 "+" 来拼接引用类型时,就会生成一个新的String对象
当字符串用 "+" 来拼接时,拼接后的字符串是否会生成新的对象,要根据 "+" 两侧的字符串来判断。
如果 "+" 两侧的字符串均为字符串常量(即有确切的常量值),则JVM会在编译期对其进行优化,会将两个字符串常量拼接为一个字符串常量,如果拼接后的字符串常量池中存在,则不创建对象,如果不存在,则创建对象;
如果 "+" 两侧的字符串有字符串引用存在,因为引用的值在JVM的编译期是无法确定的,所以 "+" 无法被JVM编译器进行优化,只有在程序运行期来动态分配并为拼接后的字符串创建新的对象(分配新的内存地址)。
基本数据类型放在堆里和栈里的情景
class Text{
int a=1;
}
基本类型的实例变量在堆上创建
public void text(){
int b=1;
}
基本类型的局部变量在栈上创建
参数
-X表示JVM参数 ms表示内存memory start 初始 mx表示memory max 最大建议两者调成一致的,因为堆内存存在压缩和释放,如果发生GC,频发压缩、释放会对服务器产生压力
1.2 方法区
方法区指向堆
方法区中会存放静态变量,常量等数据。如果是下面这种情况,就是典型的方法区中元素指向堆中的对象
1.8之前持久代(永久代)
存储包括类定义,结构,字段,方法(数据及代码)以及常量在内的类相关数据
内存回收目的
针对常量池的回收和类型的卸载
1..8之后元空间
与永久代区别
元空间并不在虚拟机中,而是使用本地内存。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 方法区|元数据区(内存控制)
* VM Args: -XX:PermSize=150m -XX:MaxPermSize=150m 【永久代】
* VM Args: -XX:MetaspaceSize=150m -XX:MaxMetaspaceSize=150m 【元空间】
**/ public class MetaspaceTest { public static void main(final String[] args) throws Exception {
while (true) {
Thread.sleep(5);
// 创建字节码增强器对象
Enhancer enhancer = new Enhancer();
// 设置父类
enhancer.setSuperclass(Object.class);
// 设置是否使用缓存
enhancer.setUseCache(false);
// 设置回调对象(方法拦截器)
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
return methodProxy.invoke(o, args);
}
});
enhancer.create();
}
}
}
存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码等数据
常量池
运行时常量池
一直在方法区,存储被 JVM 加载的类信息、 常量、 静态变量、 即时编译器编译后的代码和字面量,符号引用等数据
字符串常量池
1.7在方法区,1.8在堆
为什么jdk7之后字符串常量池要放入堆中呢?
因为永久代的回收效率很低,通常需要fullGc才进行回收,而fullGC是老年代触发majorCG后空间仍然不足或永久代快满才触发。放入堆中能够及时回收。
方法区主要回收的是无用的类,那么如何判断一个类是无用的类的呢?
类需要同时满足3个条件才能算是 “无用的类”
a-该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例
b-加载该类的 ClassLoader 已经被回收。
c-该类对应的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
永久代被代替的原因
1.永久代的大小不好设置(类信息放在永久代的话,加载时不知道有多少个类),而且如果类加载过多很容易导致OOM
2.永久代的回收效率低
2. 线程私有
2.1 栈内存
栈指向堆
如果在栈帧中有一个变量,类型为引用类型,比如Object obj=new Object(),这时候就是典型的栈中元素指向堆中的对象
2.1.1 虚拟机栈(JVM Stack)
作用
主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回
栈帧
局部变量表
作用
是存放基本数据类型储存数值本身,引用数据类型储存指向堆内存的引用指针
局部变量表中的变量不可直接使用,如需要使用的话,必须通过相关指令将其加载至操作数栈中作为操作数使用
内容
基本数据类型
用于存放int、long、short、float、double、char、byte、boolean八种基本数据类型,存放以变量槽(slot)为最小单位(32bit/位),double、long这两种基本数据类型需要2个slot储存,所以会出现线程安全问题;
reference
直接引用reference,存储的是堆对象的内存地址
访问对象的速度更快,节省了一次指针定位的时间开销
HotSpot使用的方式
句柄引用reference,存储的是堆中句柄池的句柄
reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的)时只会改变句柄中的实例数据指针,而reference本身不需要被修改
returnAddress
其它
操作数栈
作用
可以理解为PC寄存器,作为用于储存计算的临时数据存储区,当CPU执行load指令时能将数据加入到操作数栈
i++和++i的区别
i++先进行数据加载,然后进行运算
++i先运算字节码,然后进行结果赋值
两者汇编指令不一样
动态链接
作用
由于OOP思想中存在多态的概念,使得编译器在编译源代码时无法确定对象类型,只有在运行时才能确定对象,指向常量池中方法的引用
图示
每个栈帧都有一个指向运行时常量池该栈帧所属方法的引用。目的是当调用其它方法时,从运行时常量池找到对应的符号引用,并转成直接引用找到该方法。
方法出口
作用
记录方法结束时的出栈地址(正常执行结束时的返回地址或者由于报错结束时的异常地址)
退出三种方式
返回值压入上层调用栈帧
异常信息抛给能够处理的栈帧
PC计数器指向方法调用后的下一条指令
栈与栈帧的关系void a(){ b(); }
void b(){ c(); }
void c(){}
在<<Java虚拟机规范>>中,对虚拟机栈内存区域规定了两类异常状况
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
如果Java虚拟机栈容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常
/**
* 栈超出最大深度:StackOverflowError
* VM args: -Xss128k
**/
public class StackOverflowTest {
// 定义变量,记录栈的深度
private static int stackLength = 1;
// 定义方法
public static void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
try {
// 调用方法
stackLeak();
} catch (Throwable e) {
System.out.println("当前栈深度:" + stackLength);
e.printStackTrace();
}
}
}
2.1.2 本地方法栈(Native Stack)
概念
简单来讲就是Java调用非Java代码接口为虚拟机使用的native方法服务,方法通常是使用C/C++编写,然后编译成.dll或者.so文件,再由JNI(Java Native Interface)调用执行。
与虚拟机栈相同,HotSpot将虚拟机栈与本地方法栈合二为一。与虚拟机栈一样,本地方法栈也会在栈深度溢出时抛出StackOverflowError
与虚拟机栈区别
本地方法栈为虚拟机使用到的Native 方法服务
虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务
2.1.3 程序计数器
指向虚拟机字节码指令的位置
由于CUP在执行的时候会存在线程时间片切换的概念,所以CPU执行指令的时候是会中断的,程序计数器会记录当前线程执行停止的字节码指令位置(行号),以便于再次切换到改线程时能够恢复到正确的执行位置而避免重新执行。如果是Native方法,计数器值为空
分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖程序计数器
唯一一个无OOM的区域
Java中堆和栈有什么区别?
内容
栈
保存方法帧和局部变量
堆
对象
无论是成员变量,局部变量,还是类变量,它们指向的对象都存储在堆内存中
线程共享与否
栈
线程私有
堆
JVM的所有线程共享
异常错误不同
栈空间不足
java.lang.StackOverFlowError
堆空间不足
java.lang.OutOfMemoryError
空间大小
栈的空间大小远远小于堆的
为什么要把堆和栈区分出来?
(1)栈代表处理逻辑,堆代表存储数据
(2)堆和栈分离,让堆中的数据可以被多个栈共享,提供了数据交互的方式,堆的数据可以被多个栈访问,节约了存储空间
(3)栈只能向上增长的,限制了栈的存储能力,堆可以动态增长
(4)面向对象就是堆和栈的完美结合
3. 直接内存(本地内存)
特点
不受JVM GC管理,JVM直接访问内核空间,NIO,提高效率,实现zero copy
概念
直接内存并不是jvm运行时数据区的一部分,也不是jvm规范中定义的内存区域。
在 JDK 1.4 中新加入了 NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的 I/O 方式,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据
本机直接内存的分配不会受到Java堆大小的限制,受到本机总内存大小限制。配置虚拟机参数时,不要忽略直接内存,防止出现OutOfMemoryError异常解决了NIO进行管道传输Buffer时,发生fullGC导致buffer位置发生改变
和堆内存对比
直接内存创建和销毁更费性能,而IO读写的性能要优于普通的堆内存
直接内存是否会被GC?
会
显式调用Sysdtem.gc()强制执行FullGC进行回收
每个DirectByteBuffer对象在初始化时,都会创建一个对应的Cleaner对象这个Cleaner对象会在合适的时候执行unsafe.freeMemory(address),从而回收这块堆外内存当DirectByteBuffer设为null后,指向它的虚引用Cleaner就会进入pending队列,当发现队列里有Cleaner对象,就会调用方法把堆外的buffer给清掉
直接内存使用场景
有很大的数据需要存储,它的生命周期很长
适合频繁的IO操作,例如网络并发场景
优点
加快了复制的速度。因为堆内在flush到远程时,会先复制到直接内存(堆外内存),然后在发送;而直接内存(堆外内存)相当于省略掉这个工作
缺点
直接内存难以控制,如果内存泄漏,那么很难排查,且不适合储存很复杂的对象
收藏
收藏
0 条评论
下一页