一、意义
- java虚拟机从软件层面屏蔽了底层硬件指令层面的细节
- 不同的操作系统,不同的位数需要对应不同的JVM
- java在运行时实现跨平台,不需要重复写源代码,但c/c++虽然能在不同操作系统运行,但编写源代码时需要使用不同系统的API
- 只要字节码文件遵循JVM的规范,JVM不单运行java语言编写(其他语言亦可)并编译的字节码文件
二、 作用
Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台的机器指令,每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里
三、位置
四、底层结构
1. 图解
==HotSpot VM 采用解释器和即使编译期并存的架构,即混合模式==
=========详细图解===========
2. 流程
Java源代码如何生成机器码?
针对前端编译器javac编译后的字节码文件:
- 解释执行: 一边解释,一边执行,不会生成中间文件
- 编译执行: 先编译,再执行,需要编译器,JIT(Just In Time)即时编译器。分类有优化编译更为保守的Client Complier(C1编译模式),和优化编译更为激进的Server Complier(C2编译模式)
HotSpot虚拟机有三种运行模式:混合模式(默认)、解释模式、编译模式
HotSpot虚拟机的执行引擎采用混合模式**(热点代码执行编译模式,编译成本地机器码,进行缓存)**对字节码文件进行执行。
目前也有从源代码直接到机器码的AOT编译器(提前编译器),个人理解为一步完成了javac和JIT的功能,.class到.so文件使用jaotc工具
Java的指令都是根据栈来设计的
栈的特点:跨平台性、指令集小、但相较于寄存器结构的指令结构指令多、执行性能较差
指令多,且因为只有入栈出栈操作,不需要存储地址(零地址),增加了指令分派次数和内存读写次数,HotSpot设计了栈顶缓存(ToS)技术,将栈顶元素全部缓存在物理CPU的寄存器(指令少,执行速度快)中,以降低对内存的读写次数,提升执行引擎的执行效率
3. 工具
字节码文件可以通过JVM编译为机器码文件,javap -c命令可以实现反汇编,将.class文件反汇编(准确说,是解析操作,将二进制数据解析为能看懂的数据格式,理解时注意区别于反编译操作)为JVM指令
javap -v命令可以输出class文件的附加信息,查看指令集信息,相较于-c命令更详细
jps命令查看当前java进程
jstat -gc PID可以查看PID进程的垃圾回收器情况
jinfo -flag NewRatio PID 可以查看PID进程的新生代与老年代占比情况
4. 关系图解
五、JVM特点
1. 生命周期
-
虚拟机的启动 Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的
-
虚拟机的执行
- 执行程序的时候,执行的其实是一个java虚拟机进程
- 虚拟机的退出
- 程序正常执行结束
- 执行过程中遇到异常或错误而异常终止
- 操作系统出现错误而导致java虚拟机进程终止
- 某线程调用Runtime类或System类的exit方法,或Runtime类的halt方法,并且Java安全管理器也允许这次exit或halt操作
- 此外,JNI规范描述了用JNI Invocation API来加载或卸载Java虚拟机时,Java虚拟机的退出情况
2. 常见虚拟机
JRockit VM 、 J9 、 Classic VM 、 Exact VM
六、类加载子系统
1. 三个阶段
-
加载
-
链接
1). 验证
2). 准备
3). 解析
-
初始化
2. 加载
类加载器:有引导类加载器(C/C++编写)和自定义类加载器(JVM规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器, Java 编写),自定义类加载器分为扩展类加载器、系统类加载器(我们自定义的类即通过此加载器加载)
class文件通过类加载器加载到JVM中,被称为DNA元数据模板,存放在方法区
系统类加载器由扩展类加载器加载,扩展类加载器由引导类加载器加载,核心类库
(如String.class)都是由引导类加载器加载,只加载包名为java, javax, sun等开头的类, java中String.class.getClassLoader()拿不到引导类加载器
自定义类加载器
-
隔离加载类
中间件都有自己的jar包,如果项目中引用不同框架,对jar包的依赖可能重复,发生冲突,因此需要使用自定义类加载器进行隔离加载
-
修改类加载的方式
-
扩展加载源
除了从文件系统中加载class文件,还可以从其他设备(如:数据库、机顶盒) 加载
-
防止源码泄露
.class容易被反汇编,因此可以对字节码文件加密,使用自定义的类加载器
自定义类加载器的实现步骤
- 继承抽象类
java.lang.ClassLoader,1.2之前重写loadClass()方法,1.2之后 重写findClass()方法 - 如果没有复杂需求,可以继承
URLClassLoader,避免自己编写findClass()方法及获取字节码流
过程:
- 通过类的全限定名获取此类的二进制字节流
- 将字节流代表的静态存储结构转换为方法区的运行时数据结构(7.0之前时永久代具体技术实现,8.0开始是元空间具体技术实现,使用本地内存缓存)
- 在内存中生成一个代表这个类的java.lang.Class对象(考虑反射机制),作为方法区这个类的各种数据的访问入口
3. 链接
3.1. 验证
-
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全
Class文件开头以CA FE BA BE可满足校验要求
-
主要包括四种验证: 文件格式验证,元数据验证,字节码验证,符号引用验证
3.2. 准备
-
为类变量分配内存并设置该类变量的默认初始值
-
final static修饰的变量实际是常量,在编译的时候就会分配,准备阶段会显式初始化
-
此处也不会为实例变量(非static修饰的成员变量)分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中
3.3. 解析
-
将常量池内的符号引用转换为直接引用的过程
-
事实上,解析操作往往会伴随着JVM在执行完初始化之后再执行
-
符号引用就是一组符号来描述所引用的目标,符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中,直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
-
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等,对应常量池中的CONSTANT_Class_info 、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
4. 初始化
-
初始化阶段就是执行类构造器方法
<clinit>()的过程 -
此方法不需定义,是javac编译期自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来
-
构造器方法中指令按语句在源文件中出现的顺序执行
//3. 链接时的 3.2 准备步骤为类变量赋初值 number =0 --> number =20 --> number =10 static{ // number重新赋值语句可以出现在前面 number =20; //但输出number会报错,非法的前向引用 //System.out.println(number); } private static int number =10; -
<clinit>()不同于类的构造器(关联:构造器是虚拟机视角下的<init>()) -
若该类具有父类,JVM会保证子类的
<clinit>()执行前,父类的<clinit>()已经执行完毕 -
虚拟机必须保证一个类的
<clinit>()方法在多线程下被同步加锁,保证了这个类的<clinit>()只执行一次
5. 双亲委托机制
工作原理
- 类加载器收到类加载请求,并不会自己先去加载,而是把这个请求委托给父类加载器执行
- 如果父类加载器还有父类加载器,继续向上委托,请求最终将到达顶层的启动类加载器
- 如果父类加载器可以完成类加载任务,则成功返回,倘若父类加载器无法完成此加载任务,子加载器才尝试自己加载,这就是双亲委派模型。
//新建java.lang.String类
public class String{
static{
System.out.println("自定义类加载器还没加载我,就因双亲委托机制被引导类加载器加载了rt包下的String类");
}
//报错,rt包下的String类没有main方法
public static void main(String[] args){
System.out.println("Hello,String");
}
}
双亲委派机制的优势
-
a. 避免重复加载
-
b. 防止核心包被篡改(沙箱安全机制第一层,字节码校验器实现第二层,安全管理器实现第三层)
七、运行时数据区
0. 图解
1. 栈(线程私有)
不同线程开辟不同的线程私有空间,在不同线程的私有空间中,其中的栈空间,在idea中可通过观察frame窗口栏,了解到程序的栈空间情况。栈空间根据多个方法分别存放多个对应栈帧,栈帧中包含局部变量表、操作数栈、动态链接、方法出口等
没有发生逃逸的对象,即方法执行结束后就弹栈的对象,可以定义为未逃逸对象,这种对象可以分配到栈上,无需分配到堆
-
局部变量表(Local Variables)
定义为一个数字数组,包括基本数据类型,对象引用地址(reference),以及returnAddress类型。不同于普通数组的单元称为变量,局部变量表最基本的存储单元称为Slot(变量槽)
32bits以内的类型只占用一个slot(包括returnAddress类型),64bits的类型(long和double)占用两个slot
变量槽是可以重复利用的,当局部变量过了其作用域,将被回收,以供再次使用,到达节省资源的目的
局部变量必须赋值,而static修饰的成员变量,即类变量在链接的准备阶段有赋初值操作,非static修饰的成员变量,即实例变量随着对象创建,会在堆空间中分配实例变量空间,并进行赋初值操作
构造方法和实例方法创建的栈帧的局部变量表中,该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列,类方法无此this存放处,因此不能调用this
局部变量表的大小是在编译期确定下来的,保存在方法的Code属性的maximum local variables数据项中,在方法运行期间不会改变局部变量表的大小
在栈帧中,与性能调优关系最为密切的部分就是局部变量表
⭐局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象,都不会被回收
-
操作数栈(Operand Stack,表达式栈)
代码追踪
顺序为左上到右下,右下局部变量表的索引0位置存储this
-
动态链接(Dynamic Linking,指向运行时常量池的方法引用)
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示,在反汇编的class文件开头定义了符号引用的具体指向和含义。
常量池样式:
#1 = Methodref #2.#3
#2 = Class #3
#3 = Utf8 ()V
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
相对于对于方法调用而言
静态链接(发生于早期绑定): 当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接(发生于晚期绑定): 如果被调用的方法在编译期间无法被确定下来,只能在程序运行期将调用方法的符号引用转换为直接引用,称为动态链接
Java中任何一个方法都具有虚函数(需要晚期绑定,因为多态)的特征,如果不想晚期绑定,则可以使用final来修饰,注意:静态方法, 私有方法,final方法,实例构造器,父类方法都是非虚方法
class文件显示调用非虚方法有:invokestatic和invokespecial方式
调用虚方法有:invokevirtual(其中调用final方法实际也是非虚方法,唯一特例,其他均为虚方法),invokeinterface调用接口方法
为了提高性能,方法区建立了一个虚方法表,在链接的解析阶段中创建
java严格来讲是静态类型语言
String str = qwe //报错python中str = 12 //不会报错,12指定了str是整数型数据8.0引入lambda之后会调用invokedynamic,使得java一定程度具有动态类型语言的特点
-
方法返回地址(Return Address,方法正常退出或异常退出的定义)
存储调用该方法的PC寄存器的值,告诉执行引擎接下来执行指令的地址
但通过异常完成出口退出的不会给调用者产生任何的返回值
-
一些附加信息
还允许携带与Java虚拟机实现相关的一些附加信息,如对程序调试提供支持的信息
通过命令-Xss容量设置栈内存大小,栈会出现StackOverflowError,如果可以动态扩容的话,也可能出现OOM
位置在idea的run/debug configuration菜单的VM options选项可以设置
方法中定义的局部变量是否线程安全? 不一定,具体情况具体分析,如果局部变量在方法中是内部产生内部消亡的则是线程安全的
2. 程序计数器(线程私有)
也称为Program Counter Register。指向当前线程所执行的字节码指令的(地址)行号
3. 本地方法栈(线程私有)
用native修饰的方法,即本地方法,底层是C/C++实现的,java仅仅是通过JNI(Java Native Interface , Java本地接口书写程序)调用该方法。如Thread类的sleep(long)方法,java这种高级语言无法调用底层类库硬件实现功能。
JNI: Java调用非Java代码的接口,执行引擎中的解释器使用C语言编写
4. 堆(线程共享)
用于存储new出来的对象,95%的垃圾回收发生于此区
没有发生逃逸的对象,即方法执行结束后就不使用可弹栈的对象,可以定义为未逃逸对象,这种对象可以分配到栈上,无需分配到堆,称为栈上分配
7.0之后版本默认开启逃逸分析
结论: 开发中能使用局部变量的,就不要使用在方法外定义
-XX:+DoEscapeAnalysis显式开启逃逸分析
Java堆区在JVM启动时即被创建,大小也就确定
堆可以在物理上不连续的内 存空间中,但在逻辑上应该被视为连续的
所有线程共享Java堆,在此可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB),可解决线程安全问题,并发性好一点
几乎所有的对象和数组在运行时都分配在堆上,当然,有的对象及数组也分配在栈上,一般,栈中存储的是指向堆中对象实例的引用,当方法执行完,引用弹栈,但指向的对象实例等到垃圾回收才清除
堆内存细分:
7.0之前是永久代,8.0之后是元空间,并且一般情况下,虚线的属于方法区独立出来描述
-Xms 用于表示堆区(新生代+老年代)的起始内存,等价于-XX:InitialHeapSize
-Xmx 用于表示堆区(新生代+老年代)的最大内存,等价于-XX:MaxHeapSize
-Xmn 用于设置新生代最大内存
-XX:+PrintGCDetails可以打印GC细节信息
-XX:NewRatio = 2,表示新生代占1,老年代占2,即新生代占堆总大小的1/3,默认,一般不修改
可以修改-XX:NewRatio=4,令新生代占整个堆的1/5
默认堆空间,初始内存是电脑物理内存大小的1/64,最大内存是电脑物理内存大小的1/4
-XX:MaxTenuringThreshold= 进行设置年龄到达多少进入老年区
建议将堆初始内存和最大内存设置相同,避免 扩容后缩小到堆初始内存,不够了再扩容,扩容到最大内存再垃圾回收,降低程序执行效率
注:实际使用的是S1/S0+Eden+Old,如分配600m内存,查询到堆内存为575,其中
S1:25 S0:25 Eden:150 Old:400
默认的S0:S1:Eden = 1:1:6
官方声称默认是1:1:8,由于存在自适应机制AdaptiveSizePolicy,变成1:1:6
-XX:-UseAdaptiveSizePolicy:关闭自适应内存分配策略,也不够
还需要加上 -XX:SurvivorRatio = 8
几乎所有的Java对象都是在Eden区被创建的,如果对象大于Eden限制,则需要在老年区创建
绝大部分的Java对象的销毁都在新生代进行,有的说80%,有的说90%
-
new的对象先放在Eden区,此区有大小限制,大对象(超过Eden大小)直接分配到老年区
-
当Eden区填满之后,程序又需要创建对象,JVM的GC将对Eden进行垃圾回收(Minor GC) ,将Eden中不被使用的对象销毁,顺带也销毁S0及S1区中不被使用的对象,但S0或S1区如果填满,不会触发YGC(即Minor GC),如果对象仍在使用,可能被送到老年区
-
将Eden区的剩余对象移动到S0区
-
如果再次触发YGC,S0中没有被回收的,以及Eden区中没有被回收的,将被放到S1区(S0中的对象是复制算法填进来的,解决碎片化问题)
-
如果再次YGC,未被回收的对象会重新方法S0区,之后又去S1区,循环往复,动态对象年龄判断:如果S区中相同年龄的所有对象的大小总和大于S区空间的一半,年龄大于或等于该年龄的对象可直接进入老年代
-
当对象的年龄(每个对象会分配年龄计数器),超过15,将被送往老年区
-XX:MaxTenuringThreshold= 进行设置年龄到达多少进入老年区
-
在老年区,被清除几率小,当老年区内存不足时,会触发Major GC,进行垃圾回收
-
当Major GC之后仍然无法进行对象的保存,就会产生OOM异常
-XX:HandlerPromotionFailure 空间分配担保
默认为true:检查老年代最大可用的连续空间是否大于新生代所有对象的总空间,尝试进行Mionr GC,否则进行Full GC
GC线程与用户线程并发执行,当GC时,会触发STW(Stop the World),用户线程会暂停,因此调优的目的在于减少GC,Major GC导致的暂停时间约为Minor GC的10倍以上
一些特殊情况
JVM在进行GC时,并非每次都对上面的三个内存(新生代、老年代、方法区)区域一起回收,大部分回收都发生在新生代
根据回收区域,分为部分收集(Partial GC)和整堆回收(Full GC)
部分收集:Minor GC 和 Major GC
目前只有CMS GC会有单独收集老年代的行为
很多时候,Major GC和Full GC混淆使用, 实际上需要具体分辨是老年代回收还是整堆回收
混合收集:收集整个新生代以及部分老年代的垃圾收集,目前,只有G1 GC会有这种行为
出现了Major GC,经常会伴随至少一次Minor GC,但非绝对(在parallel Scavenge收集器的收集策略就有直接进行Major GC的策略选择过程)
整堆收集:收集整个java堆和方法区的垃圾收集
Full GC触发机制:
-
调用System.gc()时,系统建议执行Full GC,但不必然执行
-
老年代空间不足
-
方法区空间不足
-
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
-
由Eden、from向to区复制时,对象大小大于to可用内存,且老年代的可用内存小于该对象大小
TLAB:
为避免多个线程操作同一地址,需要加锁,影响效率,因此出现TLAB
默认仅占Eden区的1%
-XX:UseTLAB :查看是否开启TLAB,默认开启
-XX:TLABWasteTargetPercent设置TLAB空间占用Eden空间的百分比
对Eden区进行划分,JVM为每个线程分配一个私有缓存区域,这种内存分配方式称为快速分配策略
如果对象在TLAB空间分配内存失败,JVM就会尝试通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存
5. 方法区(线程共享)
具体实现使用元空间技术,是本地的一块内存,存储类的模板信息的class文件的信息,常量池,方法元信息。5%的垃圾回收发生于此区
《Java虚拟机规范》说明:逻辑上方法区属于堆,但HotSpotJVM中方法区的别名叫做Non-Heap(非堆)
7.0之前
-XX:PermSize 来设置永久代初始分配空间,默认值是20.75M
-XX:MaxPermSize 来设置永久代最大可分配空间,32bits机器默认是64M,64bits机器默认是82M
8.0之后
windows下:
-XX:MetaspaceSize = ?m 默认21m
-XX:MaxMetaspaceSize 值是-1,无限制
存储** 类型信息(包括方法信息,域信息)、运行时常量池(其中的字符串常量池后续版本有变化)、静态变量(后续版本有变化)、即时编译器编译后的代码缓存**
经典结构:
字节码文件中的常量池通过类加载器加载到方法区之后,则称为运行时常量池
常量池:
使用常量池,可以节省内存,例如没必要存储多个"字符串",直接存储符号引用即可
常量池,可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型
运行时常量池:
字节码中存放的常量池经过类加载器存放到方法区中的常量池,即运行时常量池
运行时常量池,相对于常量池有一个重要特征:动态性
方法区演进;
永久代被元空间替代的原因是JRockit和HotSpot融合,具体原因是永久代空间大小不好确定,容易OOM;对永久代调优很困难。
String Table字符串常量池为什么放到堆中,因为方法区中回收效率低,导致永久代内存不足,就转而放到堆里
对于静态变量,三个版本的实例对象都是放在堆中的,6.0版本引用放在永久代,7.0版本及8.0版本引用也放在堆中
方法区的垃圾回收难做,主要回收两部分内容:常量池中废弃的常量和不再使用的类型
不再使用的类型的判定,三条满足了也仅是被允许
- 该类所有实例已经被回收
- 该类的类加载器已经被回收,除非有可替换类加载器的场景,否则很难达成
- 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
-Xnoclassgc参数进行控制
在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景,通常需要JVM具备类型卸载的能力
6. 代码缓存区
部分人认为应该独立。部分人可能因为8.0之后的方法区使用元空间实现,即本地的一块内存,而代码缓存区(JIT编译的产物)也存在于本 地内存,故认为应该放进方法区
===============================
7. Runtime
一个JVM实例对应一个Runtime实例对象,即Runtime包含一个运行时数据区,对于一个运行时数据区,包含线程为n,则有n个程序计数器,n个栈,n个本地方法栈和1个堆,1个方法区
8. 线程
在HotSpot JVM中,每个线程都与操作系统的本地线程直接映射
-
当一个Java线程准备好执行以后,此时操作系统的一个本地线程也同时创建,Java线程执行终止后,本地线程也会回收
9. 代码优化
VM开启Server(64bits windows默认)模式才可开启逃逸分析
-
栈上分配内存
-
同步省略(锁消除)
经过逃逸分析,一个同步方法的锁对象不会被发布到其他线程,则可以进行锁消除,提升程序执行效率,因为同步的代价较大
-
分离对象或标量替换
有的对象可能不需要作为一个连续的内存结构存在也能被访问到,那么对象的部分或全部可以不存储在内存,而存储在CPU寄存器中
标量是一个无法再分解成更小的数据的描述,聚合量还可以分解
经过逃逸分析,一个聚合量只在方法中使用,则可以分解为标量,进行替换,减少堆内存占用
-XX:+EliminateAllocations:开启了标量替换,默认是打开的,允许将对象打散分配在栈上
实质就是new的对象可不可以不new 整个对象,而是new这个对象的属性
public static class Point{
int x;
int y;
}
public static void draw(){
//Point的标量为两个int,可使用标量替换,下面的代码变为int x;int y,就不会在堆中分配内存了
Point point= new Point();
point.x = 10 ;
point.y = 20;
}
10. 内存泄漏
堆中的对象始终与GC Roots存在关联,导致垃圾回收器无法进行自动回收,可通过工具查看泄露对象到GC Roots的引用链
11. 直接内存(本地内存)
IO 和 NIO的区别
如果程序报OOM异常,dump文件又比较小,可能是NIO使用出现异常
MaxDirectMemorySize设置直接内存大小,如果不指定,默认与堆的最大值-Xmx参数值一致
java进程内存空间近似= java堆空间+本地直接内存空间,栈的内存太小了
13. String在8.0和9.0版本的更改
8.0使用Character[]存储
9.0使用byte[]存储
因为发现大多数string用来存储拉丁文或者iso标准数据,因此为了节省内存,改用新的内存结构,对于仍然需要两个字节编码的数据,新版本添加了一个标志位用于甄别
字符串常量池可以通过-XX:StringTableSize 设置StringTable的长度
6.0可以更改
7.0默认为60013
8.0之后1009为最小值
data.intern()
如果字符串常量池中没有对应data的字符串的话,则在常量池中生成,jdk6的话,在永久代中字符串常量池中生成字符串字面量,jdk7及之后,在堆中字符串常量池中生成字符串字面量,与6不同的是,如果堆中字符串常量池外,存在该字符串字面量,则只在字符串常量池中生成指向字符串常量池外的已存在的该字符串字面量的引用
intern()方法返回的是一个string 如果字符串常量池中,有这个对象,则指向字符串常量池中这个字面量,具有节省内存空间的功能
字符串拼接中,如果其中有一个是变量,则会在堆中的字符串常量池外创建对象,变量拼接原理是StringBuilder
new String("str")创建的对象,7.0之后的版本会先在堆中字符串常量池中生成字面量"str",然后在堆中字符串常量池 外 生成一个指向该字面量的引用,即创建了字符串常量池中字面量的副本,创建了两个对象
使用拼接得到的字符串,默认创建了一个StringBuilder对象,调用其append()方法进行拼接,其返回值为toString()。toString()的源码为return new String(value,begin,offset) 该String构造函数不会在字符串常量池中生成字面量,即不会如同new String("str")创建字面量的副本,只在堆中字符串常量池外创建了字符串对象
如果变量被final修饰,就不是变量而变成常量了,也不是使用StringBuilder拼接,所以对于可以使用final的关键字,建议加上
final String s1 = "a";
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
//true
System.out.println(s3 == s4);
来一道题目
public static void main(String[] args){
String s = new String("1");
s.intern();
String s2 = "1";
//false:s指向堆中字符串常量池外的引用(指向字符串常量池),s2指向字符串常量池
System.out.println(s == s2);
//1.创建了StringBuilder对象
2.创建了"1"和两个new的共三个对象
3. StringBuilder对象拼接了两个"1",生成了表示"11"新的字符串对象(字符串常量池中未创建),StringBuilder指向该对象
共创建了五个对象,s3指向堆中字符串常量池外字符串对象
String s3 = new String("1")+new String("1");
//jdk6中,在永久代字符串常量池中,创建"11"字面量,jdk7及之后,在堆中字符串常量池中创建指向堆中字符串常量池外存在的"11"字符串对象的引用,即字符串常量池中创建的"11"引用和栈中指向堆中字符串常量池外的字符串引用是同一个
s3.intern();
//jdk6,s4指向永久代字符串常量池中的"11"字面量,输出false
//jdk7及之后s4指向堆中字符串常量池"11"对象引用(指向的是堆中字符串常量池外字符串对象),输出true
String s4 = "11";
System.out.println(s3 == s4);
}
再来一道题目
八、对象的实例化
指针碰撞:就是内存规则的情况下,指针左侧为使用的空间,右侧为未使用的空间,则指针碰撞就是在未使用的空间开始给新对象分配空间,然后指针右移到未使用空间的位置,和垃圾收集器相关,像Serial和ParNew使用标记压缩算法,内存比较规整,因此使用指针碰撞
空闲列表:针对内存不规整的情况,CMS垃圾收集器使用的是标记清除算法,内存不规整,使用空闲列表分配,虚拟机需要维护一个列表
对象内存结构
句柄访问
直接访问(HotSpot采用)
九、执行引擎
字节码文件机器也读不懂,所以需要执行引擎进行解释执行, 或者编译执行
高级语言(编译过程)--> 汇编语言(汇编过程)--> 机器指令
字节码在机器码上层
机器在热机状态可承受的负载要大于冷机状态:可理解为程序跑了一段时间后,JIT对于热点代码的检测和编译都已经完成一些。
程序执行一定时间,都可能达到阈值,因此出现热度衰减
java -Xint -version 完全采用解释器模式执行程序
java-Xcomp -version 完全采用JIT编译器模式执行程序,如果JIT出现问题,解释器会介入执行
java -Xmixed -version 采用解释器+JIT混合模式共同执行程序
对于JIT,64bits就是执行servlet complier模式(C2),优化更激进
所以如果不开启性能监测,实际上可能JIT开启C1和C2混合编译模式
十、 垃圾回收机制
1. 垃圾回收相关算法
1. 垃圾
在运行程序中没有任何指针指向的对象
2. 标记阶段
1). 引用计数算法
实现简单,便于辨识,判定效率高,回收没延迟,但增加时间和空间开销,无法处理循环引用的情况
python使用了引用计数,java没使用这个方法,然后python解决循环引用的方法就是手动接触或使用弱引用weakref
2). 根搜索算法(可达性分析算法)
这种类型的方法也叫做追踪性垃圾收集,解决了引用计数算法中循环引用的问题
当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法(Object类中),如果finalize()没有被重写或者**已经被执行一次(整个程序中只会执行一次)**了,则不会再被调用,否则由虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行
finalize()方法允许在子类中被重写,用于在此对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等,不建议主动调用,可能导致对象复活;执行时间没有保证;可能会影响GC性能
由于finalize()方法的存在,虚拟机中的对象存在三种情况
不可达的对象实行"最终审判",一个不可达的对象可能在某一条件下"复活"自己,如此,则对他的回收就是不合理的
可触及的:
从根节点开始,可以到达这个对象
可复活的:
对象的所有引用都被释放,但是对象有可能在finalize()中复活
不可触及的:
finalize()被调用,并且没有复活,那么就会进入不可触及状态,不可能复活,因为finalize()只会被调用一次
以上三种状态中,由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收
3. 清除阶段
1). 标记-清除算法
标记:标记的是不被清除的对象,非垃圾对象
清除:并不是删除,而是把对象地址放在地址列表,下次创建对象时,用新引用覆盖原引用
比如硬盘格式化后,如果没再写入新数据(覆盖原空间),是可以恢复的
缺点:
- 效率不高
- 标记时需要STW
- 会产生内存碎片,需要维护一个空闲列表
2). 复制算法
双内存
缺点:
-
费内存
-
G1这种拆分为大量region的GC,复制而非移动,意味GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小
-
复制的对象数量少的话比较好
新生代朝生夕死,适合用复制算法,老年代不适合
3). 标记-压缩(整理)算法
基于老年代, 标记-清除会产生碎片,不利于处理大对象,针对此问题优化,使用标记-压缩算法
标记: 从根节点开始标记所有被引用对象
压缩:将所有存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间
缺点:
-
效率低于复制算法
-
移动对象同时,如果该对象被其他对象引用,还需要调整引用的地址
-
移动过程中,需要STW
目前几乎所有GC都是采用分代收集算法执行垃圾回收
在HotSpot中,新生代使用复制算法,老年代使用标记-清除和标记-整理的混合实现
HotSpot 的老年代使用CMS回收器
4).增量收集算法
缺点:
-
线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降
5). 分区算法
十一、知识点补充
1. System.gc()
通过System.gc()或者Runtime.getRuntime().gc(), 会显示触发Full GC,同时对老年代和新生代进行回收
但该方法附带免责声明,无法保证对垃圾收集器的调用的执行时间
但调用System.runFinalization()强制调用使用引用对象的finalize方法
编写性能基准,可以在运行之间调用该方法
2. 内存溢出和内存泄漏
内存溢出:
没有空闲内存,垃圾收集器也无法提供更多内存
内存泄漏:
3. 并发与并行
并发: CMS、G1
4. 安全点
程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才可以,这些位置称为"安全点",选择在让程序长时间执行的特征,如方法调用、循环跳转和异常跳转等
安全点太少可能导致GC等待时间太长,太频繁可能导致运行时性能问题
如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?
- 抢先式中断(目前没有虚拟机采用):
首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点
- 主动式中断:
设置一个中断标志,各个线程运行到Safe Point的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起
5. 安全区域
Safepoint机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。但是,程序"不执行"的时候,比如线程处于Sleep状态或Blocked状态,这是线程无法响应JVM的中断请求,"走"到安全点去中断挂起,JVM也不太可能等待线程被唤醒,对于这种情况,就需要安全区域(Safe Region)来解决
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的,我们也可以把Safe Region看作是被扩展了的Safepoint
实际执行时:
-
当线程运行到Safe Region的代码时,首先标识已经进入了Safe Region 如果这段时间内发生GC,JVM会忽略标识为Safe Region状态的线程
-
当线程即将离开Safe Region时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开Safe Region的信号为止
6. 强引用
类似"Object obj = new Object();",无论何时,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象
7. 软引用
不可触及对象触发一次GC,当要发生OOM之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出OOM
空间不够才回收
高速缓存用到了软引用
Mybatis中用到了
代码
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);//软引用
obj = null;//销毁强引用
8. 弱引用
被弱引用关联的对象只能生存到下一次垃圾回收之前,当GC工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
发现即回收
也适用于缓存中
WeakHashMap内部有WeakReference,相较于HashMap降低OOM的几率
9. 虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知
对象回收跟踪
创建一个虚引用对象,拿不到对象
由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录
10. 终结器引用
用以实现对象的finalize()方法
十二、GC分类与性能指标
如果想要最小化使用内存和并行开销,请选Serial GC
如果想要最大化应用程序的吞吐量,请选Parallel GC
如果想要最小化GC的中断或停顿时间,请选CMS GC
- 按照垃圾回收的线程数
分为串行垃圾回收器(只允许有一个CPU用于执行垃圾回收操作,默认被应用在客户端的Client模式下的JVM)和并行垃圾回收器(在并发能力比较强的CPU上,并行回收器产生的停顿时间要短于串行回收器)
和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量,不过并行回收仍然与串行回收一样,采用独占式,使用了STW机制
- 按照工作模式
可以分为并发式垃圾回收器(用户线程和垃圾回收线程可以同时执行)和独占式垃圾回收器(只能执行一个线程)
- 性能指标
吞吐量:
运行用户代码的时间占总运行时间(程序的运行时间+内存回收的时间)的比例
暂停时间:
执行垃圾收集时,程序的工作线程被暂停的时间
内存占用:
Java堆区所占的内存大小
以上三者共同构成了一个“不可能三角”。一款优秀的收集器通常最多同时满足其中的两项
主要抓住吞吐量和暂停时间两点
1. CMS回收器:低延迟
使用场景:强交互应用,这款收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
CMS(Concurrent-Mark-Sweep,并发标记清除)
关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短(低延迟)就越适合与用户交互
目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,但因为使用标记清除,也会STW
在1.初始标记GC Roots的时候STW,之后2.并发标记(这一阶段与用户线程并发)3.重新标记(因为第2阶段所以需要修正第1阶段,为了标记,也会STW,停顿时间稍长)4. 并发清除(执行清除,也是并发,虽然标记-压缩不会产生碎片,但更适合于STW的情境下使用,只能选择并发清除)
2. Serial回收器
串行回收,新生代使用复制算法,老年代使用标记-压缩算法,适用于Client模式桌面应用场景中,可用内存一般不大。-XX +UseSerialGC 参数可指定该GC
3. ParNew回收器
并行回收,new代表只能处理新生代。采用复制算法,适用于Server模式下新生代默认GC,可以在多CPU的环境下,多核心硬件条件下指定,提升程序的吞吐量
对于新生代,回收次数多,使用并行方式更高效。对于老年代,回收次数少。使用串行方式节省资源
4. Parallel回收器
-
Parallel Scavenge收集器
并行回收,新生代使用复制算法,目标是达到一个可控制的吞吐量,主要适合在后台运算而不需要交互的任务,常在Server中使用,例如:执行批量处理、订单处理、工资支付、科学计算的应用程序
- Parallel Old收集器
并行执行,采用标记-压缩算法,java8中,默认使用此收集器
5. CMS
清除过程
-
初始标记:标记GC Roots,STW时间短
-
并发标记:标记其他引用链上的对象
-
重新标记:2上还会产生新的引用,STW
-
并发清除:还会产生浮动垃圾
十三、调优
1. java为什么需要性能调优
如图所示,达到回收内存临界点,进行垃圾回收操作