JVM 不仅仅可以用作运行Java,JVM自己定义了一套字节码规范,只要你的源代码可以编译成指定的字节码那么都可以被JVM运行。
JVM运行在操作系统之上,JVM负责
指令集的架构一般分成两成,一种基于寄存器,一种基于栈。
经典的寄存器指令架构x86,比如传统的PC,像是汇编。寄存器指令,高效,性能好,但移植性差。
mov ax,2
mov bx,3
add ax,bx
java编译器输入的指令流基本上是一种基于栈的指令架构。相同的功能,基于栈的指令生成了更多代码。
int i = 2;
int j = 3;
int k = i + j;
---------------
0: iconst_2 //定义常量2
1: istore_1 //存入栈1位置
2: iconst_3 //定义常量3
3: istore_2 //存入栈2位置
4: iload_1 //加载1位置
5: iload_2 //加载2位置
6: iadd //相加
7: istore_3 //存入栈3位置
java虚拟机的启动是通过引导类加载器(bootstrap loader)创建一个初始类(initial class)来完成,这个类由虚拟机的具体实现指定。加载所有运行程序的基础类的支持。
执行所谓的java程序(我们写的),真真正正在执行的是一个虚拟机的进程,进程解析字节码执行。
程序正常执行结束,遇到异常或错误,或由于操作系统错误引发退出。或调用Rumtime,System的 exit方法
类加载子系统负责从文件系统或网络中加载Class文件,class文件在文件头有特殊标识。
ClassLoader只负责class文件的加载,至于它是否能够运行,则有ExecutionEngine决定。
加载的类信息被存放在方法区。除了类信息外,方法区中还会存放运行时常量池信息,还保存字符串字面量和数字常量。
确保Class文件的字节流中包含信息符合当前虚拟机的要求
文件格式验证,元数据验证,字节码验证,符号引用验证。
public final static int i = 1; // final static在编译时已经分配,准备阶段显示赋值
public static int j = 2; // prepare 阶段 j 赋值为0
符号引用就是一组符号来描述引用的目标。符号引用的字面量形式明确定义在《java虚拟机规范》的Class文件格式中。直接引用就是将目标的指针、相对偏移量或一个简介定位目标的句柄。
public class ClassInit {
public static int i = 520;
public int j = 250;
public ClassInit() {
}
}
使用idea插件jclasslib查看ClassInit类的字节码。(字节码都是有规则的,jclasslib就当作字节码的格式化解释器)
其中init方法代表的类实例构造器。也可以看出init中的指令调用了父类构造器,初始化了 j 的值。
clinit方法代表这类的构造器,其中初始化类变量,将520赋值给了类变量 i 。
public class ClinitLock {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println(Thread.currentThread().getName() + "开始");
DemoThread demoThread = new DemoThread();
System.out.println(Thread.currentThread().getName() + "结束");
};
Thread thread1 = new Thread(r,"线程1");
Thread thread2 = new Thread(r,"线程2");
thread1.start();
thread2.start();
}
}
class DemoThread {
static {
System.out.println(Thread.currentThread().getName() + "正在调用clinit方法");
if (true) {
while (true) {
}
}
}
}
JVM支持两种类加载器引导类加载器、自定义类加载器,从概念上来说,自定义类加载指的是由开发人员自行定义的一类加载器,但java虚拟机规范却没有这么定义,而是将所用派生自抽象类ClassLoader的类加载器都定义为自定义类加载器
不论规范如何划分类加载器,程序开发中常见的就是三中:启动类加载器、扩展类加载器、引导类加载器。类加载器之间并不是继承,而是通过包含“父加载器”引用,保证逻辑结构
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
// 应用类加载器 sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(contextClassLoader);
// 扩展类加载器 sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println(contextClassLoader.getParent());
// 获取不到启动类加载器 null
System.out.println(contextClassLoader.getParent().getParent());
// 应用类加载器加载自定义类 sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(ClassLoaderTest.class.getClassLoader());
// 扩展类加载器加载扩展类 sun.misc.Launcher$ExtClassLoader@1b6d3586
System.out.println(ZipFileStore.class.getClassLoader());
// 启动类加载器加载核心类库
System.out.println(String.class.getClassLoader());
}
}
引导类加载器使用c/c++ 实现,嵌套在JVM内部
用来加载核心类库(JAVA_HOME/jre/lib/rt.jar,resource.jar或sun.boot.class.path路径下内容)用于提供JVM自身需要的类
并不继承自ClassLoader(用c写的)
加载扩展类和应用类加载器,并指定他们的父类加载器
出于安全考虑,bootStrapClassLoader只会加载包名为java、javax、sun等开头的类
java语言编写,由sun.misc.Launcher$ExtClassLoader实现
派生于ClassLoader
父类加载器为启动类加载器
从java.ext.dirs 系统属性指定的路径中加载类库,或从JDK安装目录的jre/lib/ext 目录下加载类库。如果用户自己的jar放在这个文件目录下也将有扩展类加载器加载
java语言编写,由sun.misc.Launcher$AppClassLoader实现
派生于ClassLoader
父类加载器为启动类加载器
复制加载环境变量classpath或系统属性 java.class.path 指定路径下的类库
是默认类加载器,一般来说,java应用的类都是由他来完成加载
可通过ClassLoader.getSystemClassLoader()获取
public class ClassLoaderTest {
public static void main(String[] args) {
// 引导类加载器目录
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Stream.of(urLs).forEach(System.out::println);
System.out.println("----------------------------------------");
// 扩展类加载器加载目录
String dirs = System.getProperty("java.ext.dirs");
Stream.of(dirs.split(";")).forEach(System.out::println);
System.out.println("----------------------------------------");
// 应用类加载器加载目录
String path = System.getProperty("java.class.path");
Stream.of(path.split(";")).forEach(System.out::println);
}
}
/**
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_251/jre/classes
----------------------------------------
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext
C:\Windows\Sun\Java\lib\ext
----------------------------------------
C:\Program Files\Java\jdk1.8.0_251\jre\lib\charsets.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\deploy.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\access-bridge-64.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\cldrdata.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\dnsns.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jaccess.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jfxrt.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\localedata.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\nashorn.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunec.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunjce_provider.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunmscapi.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunpkcs11.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\zipfs.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\javaws.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\jce.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfr.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfxswt.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\jsse.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\management-agent.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\plugin.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\resources.jar
C:\Program Files\Java\jdk1.8.0_251\jre\lib\rt.jar
F:\javaSource\jvm-demo\target\classes
D:\IDEA\lib\idea_rt.jar
**/
程序的运行(执行引擎的解析执行)依赖于运行时数据区,运行时数据区包括方法区、堆、栈、程序计数器、本地方法栈。方法区,堆是线程共享的。虚拟机栈,本地方法栈,程序计数器每个线程都有一份
1.8之前使用方法区存储类元信息,常量池等信息,1.8之后使用元空间存储这些信息。(元空间使用堆外内存)
java.lang.Runtime 的实例代表着运行时数据区(每个Java应用都对应这一个单例的Runtime,Runtime可以通过Runtime.getRuntime()获取当前应用实例)
(图为java8)
CPU只有把程序状态到寄存器中才可以执行。JVM的程序计数器类似与对物理寄存器的抽象(地址寄存器),每个线程的程序计数器存储线程下一条指令的地址。执行引擎根据计数器的指示执行。
由于跨平台的设计,java的指令使用栈结构来设计。不同于cpu架构采用寄存器指令架构。(使用寄存器指令架构更依赖于cpu)
java虚拟机栈是什么?
java虚拟机栈(java virtual machine stack),每个线程创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(stack frame),一个个栈帧对应这一次次方法调用。
主要作用
主管java程序运行,它保存方法的局部变量、部分结果、并参与方法的调用和返回。
每个线程都有自己的栈,栈中的数据都是一**栈帧(stack frame)**的方式存储,线程正在执行的每个方法(层级调用)都对应各自的几个栈帧。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中各种数据信息
在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即当前正在执行的方法的栈帧(栈顶栈帧),当前栈帧对应的就是当前方法,定义当前方法的类也就是当前类
执行引擎运行的所有字节码指令只针对当前栈帧操作。
如果当前栈帧中调用了其他方法,那么新的栈帧将被创建放入栈顶,成为新的当前栈帧。
java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常(未处理)。不管那种方式都会导致栈帧被弹出
局部变量表也被成为Local Variable,也被成为局部变量数组
使用javap -v 查看类字节码,主要关注main方法编译后的字节码指令
7 public static void main(String[] args) {
8 int i = 10;
9 String str = "abc";
10 Object obj = new Object();
11 }
如下(//表示注释)
public static void main(java.lang.String[]);
// 参数与返回值类型,V表示void
descriptor: ([Ljava/lang/String;)V
// 访问修饰
flags: ACC_PUBLIC, ACC_STATIC
// code字节码指令
Code:
// locals4 就表示局部变量表长度,注意#2,#2,#1这都是常量池引用
stack=2, locals=4, args_size=1
0: bipush 10
2: istore_1
3: ldc #2 // String abc
5: astore_2
6: new #3 // class java/lang/Object
9: dup
10: invokespecial #1 // Method java/lang/Object."":()V
13: astore_3
14: return
// lineNumber表示程序行号,行号后面的数字表示,对应行的程序对应的字节码指令。如line 8:0 再看后面的 line 9:3可知 0到3行的字节码指令对应的是第8行的程序 int i = 10;
LineNumberTable:
line 8: 0
line 9: 3
line 10: 6
line 11: 14
LocalVariableTable:
// slot 变量槽
// signature中的L表示引用类型
// Name表示变量名
// Start与Length表示变量的作用域,Code字节码共15行
Start Length Slot Name Signature
0 15 0 args [Ljava/lang/String;
3 12 1 i I
6 9 2 str Ljava/lang/String;
14 1 3 obj Ljava/lang/Object;
变量槽也就局部变量表中占的位置(索引),32位类型占一个槽,64位类型long,double占两个槽,64位类型通过起始索引访问,如果方法是构造方法,或实例方法,对象引用this将会放在索引为0的槽中(jclasslib插件查看)
slot是可以重复利用的如果一个变量过了他的作用域那么之后声明的变量会占用他的位置,变量b过了自己的生命周期,下个变量占用他的位置
public void test2() {
int a = 10;
{
int b = 20;
b = b + a;
}
// 下面是访问不到b的
int c = a * 2;
}
注意看变量b的StartPc与Length描述的作用域防范是小于15的(15整个指令长度)
每个独立的栈帧中除了包含局部变量表外,还包含一个后进先出的操作数栈,也称为表达式栈 (使用数组结构)
操作数栈,在方法执行中,根据字节码指令,往栈中写入数据或提取数据,即入栈、出栈
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是JVM执行引擎一个工作区(JVM基于栈指令架构),当一个方法刚开始执行的时候,一个新的栈帧随之创建,这时操作数栈是空的
每个操作数栈都会拥有一个明确的栈深用户存储,其所需的最大深度在编译期已经定义好了,保存在方法的Code属性中的max_Stack值中
栈中的可以存储任意的Java数据类型,32bit占一个栈深,64bit占两个
操作数栈只能通过 push,pop操作来完成一次数据访问
如果方法调用有返回值,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中的下一条需要执行的字节码指令
操作数栈实例
源码
public void test(){
byte a = 10;
int b = 20;
int c = a + b;
}
字节码
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
// 操作数栈2
stack=2, locals=4, args_size=1
// 入栈10
0: bipush 10
// 存入局部变量表 1 位置
2: istore_1
// 入栈20
3: bipush 20
// 存入局部变量表 2 位置
5: istore_2
// 入栈局部变量表 1 位置
6: iload_1
// 入栈局部变量表 2 位置
7: iload_2
// 栈顶两个位置相加
8: iadd
// 存入局部变量表 3 位置
9: istore_3
10: return
LineNumberTable:
line 6: 0
line 7: 3
line 8: 6
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/xxxx/OperandStackTest;
3 8 1 a B
6 5 2 b I
10 1 3 c I
执行流程图
字节码层面解释i++与++i的区别
5 public void test() {
6 int i = 10;
7 int k;
8 k = i++;
9 k = ++i;
10 }
前++的本质就是执行iload操作是在 iinc 之前还是在之后
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=3, args_size=1
0: bipush 10
2: istore_1
3: iload_1 // 加载局部变量表 1 号位置入栈
4: iinc 1, 1 // 局部变量表 1 号位置 自增1
7: istore_2 // 操作数栈顶存入局部变量表 2 号位置 (栈顶还是之前i的值,但此时变量表中i已经自增了)
8: iinc 1, 1 // 局部变量表 1 号位置 自增1
11: iload_1 // 加载局部变量表 1 号位置入栈(注意这时i已经自增完成)
12: istore_2 // 操作数栈顶存入局部变量表 2 号位置
13: return
LineNumberTable:
line 6: 0
line 8: 3 // 第8行的代码是从3行字节码开始的(k=i++)
line 9: 8 // 第9行的代码是从8行字节码开始的 (k=++i)
line 10: 13
LocalVariableTable:
Start Length Slot Name Signature
0 14 0 this Lcom/tttiger/OperandStackTest;
3 11 1 i I
8 6 2 k I
再来看一个有趣的例子,j的最终 结果是8
public void test() {
int i = 3;
int j = i++ + ++i;
}
Code:
stack=2, locals=3, args_size=1
0: iconst_3
1: istore_1
2: iload_1 // 先iload 后 iinc 后++完成
3: iinc 1, 1
6: iinc 1, 1 // 先iinc后 iload 后++
9: iload_1 // 此时 iload 局部变量表中的i已经变成5
10: iadd // 操作数栈的 3+5
11: istore_2 // 存储最终相加结果到局部变量表2位置
12: return
指向运行时常量池的方法引用
public void testA(){
try{
testB();
}catch (Exception e){
e.printStackTrace();
}
}
如果处理了异常,回去异常表中找到匹配的异常进行处理
public void testA();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=1
0: aload_0
1: invokevirtual #2 // Method testB:()V
4: goto 12
7: astore_1
8: aload_1
9: invokevirtual #4 // Method java/lang/Exception.printStackTrace:()V
12: return
Exception table:
from to target type
0 4 7 Class java/lang/Exception // 0到4行发生异常,跳转到7行处理
LineNumberTable:
line 7: 0
line 10: 4
line 8: 7
line 9: 8
line 11: 12
LocalVariableTable:
Start Length Slot Name Signature
8 4 1 e Ljava/lang/Exception;
0 13 0 this Lcom/tttiger/DynamicLinkTest;
栈帧中还允许携带与java虚拟机相关的一些附加信息。例如对程序调试提供支持的信息。
一个Native Method就是java调用非java代码的接口,一个native method的实现并非由java实现,例如可能是c实现,在定义个native method 并不需要提供实现体,因为其实现体是由非java程序实现的。本地接口的作用是融合不同的编程语言为java所用,他的初衷是融合c/c++程序。
java对底层的支持很有局限性,而c/c++可以很好的解决这个问题。
现在的垃圾收集器大部分基于分代收集理论设计。
-XX:+PrintFlagsInital 查看所有参数设置(默认值)
-XX:+PrintFlagsFinal 查看所有参数的最终值(可能会存在修改,不再是初始值)
堆内存的大小在启动时已经设定好了,可以同过 -Xms 和 -Xmx 设置
-Xms 堆内存起始大小(-Xms1024m)
-Xmx 堆内存最大内存
通常将 -Xms 与 -Xmx 设置为相同的值,其目的是为了能够在垃圾回收清理完堆后不需要重新分割计算堆区的大小
默认情况下,起始内存大小为: 物理内存/64,最大内存为:物理内存/4
默认 -XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整个堆的1/3
若修改为 -XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整个堆的1/5
(还可以使用 -Xmn100m 直接指定新生代的大小,直接指定后比例就不起作用了,平常不用)
Eden与另外两个Survivor空间的占比默认是8:1:1(默认情况下其实不是,因为还有一个参数-XX:+UseAdaptiveSizePolicy,开启了自动分配,可以-XX:-UseAdaptiveSizePolicy 关闭) 需要指定 -XX:SurvisorRatio=8 显示指定Eden区占比为8
-XX:UseTLAB 设置是否开启TLAB
-XX:TLABWasteTargetPercent 设置TLAB占比
-XX:MaxTenuringThreshold 对象年龄超过指定值放入老年代(默认15)
存储在JVM中的java对象可以划分为两类:1. 生命周期较短的瞬时对象,这类对象创建和消亡都非常迅速。 2. 生命周期非常长,在某些极端情况下可能与JVM的生命周期相同
java堆进一步划分,可分为年轻代(youngGen)和老年代(oldGen)
年轻代又可以划分为Eden空间,Survivor0区,Survivor1区(可叫做From区,To区,From,To是不停交换的,复制算法回收)
为对象分配内存是一件非常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配,分配在哪里,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。
对象分配的特殊情况
JVM再进行GC时,并非每次都对三个内存(新生代,老年代,方法区)区域一起回收。
针对HotSpot VM的实现,它里面的GC按照回收区域可分为两大类:1. 部分收集 2. 整堆收集
部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为
整堆收集:FullGC 收集整个java堆和方法区
指老年代空间不足时发生在老年代的GC,对象从老年代消失时,会说Major GC 或 Full GC发生了
出现MajorGC,经常会伴随至少一次MinorGC(但非绝对的,在Parallel Scavenge收集器的策略里就有直接进行Major的策略选择过程,也就是在老年代空间不足时,指定先触发minorGC,如果之后空间还不足触发Major GC)
MajorGC的速度一般会比MinorGC满10倍以上,STW的时间更长
TLAB(Thread Local Allocation Buffer)
堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据
由于对象实例的创建在JVM中非常频繁,因此在并发情况下从堆区划分内存空间是线程不安全
为避免多个线程操作同一地址,需要使用加锁等机制,影响内存分配速度
从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有的缓存区域(只有在分配的时候是私有的,加快分配速度),它包含在Eden中
多线程同时分配时,使用TLAB可以避免一系列的线程安全问题,同时还能够提升内存分配的吞吐量,因此可称之为快速分配策略
尽管不是所有对象实例都能够成功的在TLAB中成功分配,但在TLAB中分配是首选的(TLAB空间有限)
默认情况下,TLAB空间的内存非常小,仅占整个Eden空间的1%,当然可以通过“-XX:TLABWasteTargetPercent” 设置TLAB占用Eden的比值
一旦对象在TLAB空间分配内存失败,JVM会尝试通过加锁(CAS)直接在Eden中配分
堆是对象分配的唯一选择么?随着JIT编译器的发展与逃逸分析技术的成熟,栈上分配,标量替换优化技术使得对象在堆中分配变得不是那么绝对了
栈上分配测试
// JVM 参数 -Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
public class EscapeTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("用时:"+(end-start));
}
public static void alloc(){
// 没有发生逃逸
EscapeTest test = new EscapeTest();
}
}
// JVM 参数 -Xms1G -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
用时:78
Heap
PSYoungGen total 305664K, used 173015K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 66% used [0x00000000eab00000,0x00000000f53f5f00,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3236K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
同样的代码开始栈上分配后
// JVM 参数 -Xms1G -Xmx1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
用时:4
Heap
PSYoungGen total 305664K, used 15729K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
eden space 262144K, 6% used [0x00000000eab00000,0x00000000eba5c420,0x00000000fab00000)
from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
to space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
ParOldGen total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
Metaspace used 3236K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
// 内存较小时,关闭栈上分配,进行了垃圾回收
// JVM 参数 -Xms256m -Xmx256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
[GC (Allocation Failure) [PSYoungGen: 65536K->776K(76288K)] 65536K->784K(251392K), 0.0015262 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66312K->776K(76288K)] 66320K->792K(251392K), 0.0007224 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
用时:51
Heap
PSYoungGen total 76288K, used 32046K [0x00000000fab00000, 0x0000000100000000, 0x0000000100000000)
eden space 65536K, 47% used [0x00000000fab00000,0x00000000fc989890,0x00000000feb00000)
from space 10752K, 7% used [0x00000000ff580000,0x00000000ff642020,0x0000000100000000)
to space 10752K, 0% used [0x00000000feb00000,0x00000000feb00000,0x00000000ff580000)
ParOldGen total 175104K, used 16K [0x00000000f0000000, 0x00000000fab00000, 0x00000000fab00000)
object space 175104K, 0% used [0x00000000f0000000,0x00000000f0004000,0x00000000fab00000)
Metaspace used 3236K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
// 开启栈上分配后没有进行垃圾回收
// JVM 参数 -Xms256m -Xmx256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
用时:4
Heap
PSYoungGen total 76288K, used 6554K [0x00000000fab00000, 0x0000000100000000, 0x0000000100000000)
eden space 65536K, 10% used [0x00000000fab00000,0x00000000fb166858,0x00000000feb00000)
from space 10752K, 0% used [0x00000000ff580000,0x00000000ff580000,0x0000000100000000)
to space 10752K, 0% used [0x00000000feb00000,0x00000000feb00000,0x00000000ff580000)
ParOldGen total 175104K, used 0K [0x00000000f0000000, 0x00000000fab00000, 0x00000000fab00000)
object space 175104K, 0% used [0x00000000f0000000,0x00000000f0000000,0x00000000fab00000)
Metaspace used 3231K, capacity 4500K, committed 4864K, reserved 1056768K
class space used 349K, capacity 388K, committed 512K, reserved 1048576K
在动态编译同步代码块的时候,JIT编译器可以借助逃逸分析来判断同步代码块所使用的锁对象是否只能被一个线程访问而没有被发布到其他线程。如果没有JIT编译器在编译这个同步代码块的时候就会取消这部分代码的同步。这样大大提高并发性和性能。这个取消同步的过程叫同步省略。这个取消同步的过程也叫锁消除
// 使用的锁对象只能被一个线程访问
public void test(){
// 每个线程调用都是一个新的实例
Object obj = new Object();
synchronized (obj){
System.out.println(obj);
}
}
// 在运行期间将会编译优化为(在字节码层面加锁还是存在的)
public void test() {
Object obj = new Object();
System.out.println(obj);
}
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存在cpu寄存器中
标量 Scalar是只一个无法在分解成更小的数据的数据。Java中的原始数据类型就是标量。相对的还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为它可以分解为其他聚合量和标量
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过编译器优化,就会把这个对象替换成其中包含的若干个成员变量来替代。这个过程就是标量替换
public static void main(String[] args){
alloc();
}
private static void alloc(){
Point point = new Point(1,2);
System.out.print(point.x+"-"+point.y);
}
class Point{
private int x;
private int y;
}
经过标量替换后,就会变成
private static void alloc(){
int x = 1;
int y = 2;
System.out.print(x+"-"+y);
}
Point 这个聚合变量经过逃逸分析后,发现它并没有逃逸,就被替换成两个标量了。一旦替换为标量就不需要创建对象,那么就不需要分配堆内存了。标量替换为栈上分配提供了很好的基础
JAVA虚拟及规范中明确说明,尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾回收或进行压缩。但对于HotSpotJVM 方法区也称做非堆。
在JDK1.7之前,习惯上把方法区,称为永久代。jdk8开始,使用元空间取代了永久代。
本质上,方法区和永久代并不等价。JVM虚拟机规范对如何实现方法区,不做统一要求。例如:EBA JRokit/IBM j9都不存在永久代的概念(现在看来,当年使用永久代,不是好的idea。导致Java程序更容易OOM,因为使用的是JVM内存(超过 -XX:MaxPermSize 上限))
而到了JDK8 ,终于完全废弃了永久代的概念,改用与JRokit,J9一样使用本地内存来实现元空间
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代使用的最大区别在于元空间直接使用本地内存。
jdk7及之前:
通过-XX:PermSize来设置永久代出是分配空间,默认为20.75mb。-XX:MaxPermSize来设定永久代最大可分配空间。32位机器默认64M,64位机器模式是82M
jdk8之后:
元数据区大小可以使用参数 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize指定,替代上述原因两个参数。
默认值依赖与平台。windows下,-XX:MetaspaceSize是21M, -XX:MaxMatespaceSize的值是-1,没有上限
与永久代不同,如果不指定元空间的大小,默认情况下,虚拟机会耗尽所有系统内存,最后抛出 OutOfMemoryError
-XX:MetaspaceSize: 设置初始的元空间大小。对于一个64位的服务器端来说,其默认的-XX:MetaspaceSize=21M。默认的方法区初始大小。在方法区空间不足时,会触发FullGC尝试卸载无用的类,还是无可用内存的话,将会触发Full GC。如果释放的空间不足,那么在不超过MaxMetaspaceSize 时,适当提高该值。(如果MetaspaceSize 初始设置过小可能频繁触发FullGC)
jdk版本 | 方法区变化 |
---|---|
jdk1.6及以前 | 有永久代(permanent generation),静态变量存放在永久代上 |
jdk1.7 | 有永久代,但已经逐步去永久代,字符串常量池,静态变量移除保存在堆中 |
jdk.18 | 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池,静态变量任然在堆中 |
方法区为什么要调整为使用本地内存?
对方法区进行调优是很困难的类的回收条件非常苛刻,一些项目会动态加载很多类,很可能会内存溢出倒不如直接使用本地内存
StringTable为什么要调整?
永久代的回收效率低,只有在老年代不足或方法区不足触发FullGC的时候在进行回收,而开发过程中有大量的字符串被创建,回收效率低,导致永久代空间不足。放到堆里,能及时回收内存。
方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即使编译器编译后的代码缓存、域信息、方法信息(域信息,方法信息也可看做包含在类型信息中)。
对每个加载的类型(类Class、接口Interface、枚举Enum、注解Annotation),JVM必须在方法区中存储一下类型信息:
这个类的完整有效包名(全类名)
这个类型的直接父类的完整有效名(对于interface或java.lang.Object 没有父类)
这个类型的修饰符(public ,abstract,final的某个子集)
这个类型直接接口的一个有序列表
JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序(属性)
域相关信息包括:域名称、域类型、域修饰符(public、private、protected、static、final、volatile、transient的某个子集)
JVM必须保存所有方法的一下信息,同域信息一样包括生命顺序
Classfile /E:/javaSource/jvm-demo/target/classes/com/tttiger/MethodAreaTest.class
Last modified 2020-8-22; size 1300 bytes
MD5 checksum 2a7cbd3d6b78efd7fda7e3bdea57b838
Compiled from "MethodAreaTest.java"
// 类型全类名,父类,接口,泛型全类名
public class com.tttiger.MethodAreaTest extends java.lang.Object implements java.lang.Comparable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
#1 = Methodref #12.#45 // java/lang/Object."":()V
#2 = Fieldref #46.#47 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #48.#49 // java/io/PrintStream.println:(I)V
#4 = Class #50 // java/lang/Exception
#5 = Methodref #4.#51 // java/lang/Exception.printStackTrace:()V
#6 = Class #52 // java/lang/String
#7 = Methodref #11.#53 // com/tttiger/MethodAreaTest.compareTo:(Ljava/lang/String;)I
#8 = Fieldref #11.#54 // com/tttiger/MethodAreaTest.num:I
#9 = String #55 // 测试方法区
#10 = Fieldref #11.#56 // com/tttiger/MethodAreaTest.str:Ljava/lang/String;
#11 = Class #57 // com/tttiger/MethodAreaTest
#12 = Class #58 // java/lang/Object
#13 = Class #59 // java/lang/Comparable
#14 = Utf8 num
#15 = Utf8 I
#16 = Utf8 str
#17 = Utf8 Ljava/lang/String;
#18 = Utf8
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/tttiger/MethodAreaTest;
#25 = Utf8 test1
#26 = Utf8 count
#27 = Utf8 test2
#28 = Utf8 (I)I
#29 = Utf8 value
#30 = Utf8 e
#31 = Utf8 Ljava/lang/Exception;
#32 = Utf8 cal
#33 = Utf8 result
#34 = Utf8 StackMapTable
#35 = Class #50 // java/lang/Exception
#36 = Utf8 compareTo
#37 = Utf8 (Ljava/lang/String;)I
#38 = Utf8 o
#39 = Utf8 (Ljava/lang/Object;)I
#40 = Utf8
#41 = Utf8 Signature
#42 = Utf8 Ljava/lang/Object;Ljava/lang/Comparable;
#43 = Utf8 SourceFile
#44 = Utf8 MethodAreaTest.java
#45 = NameAndType #18:#19 // "":()V
#46 = Class #60 // java/lang/System
#47 = NameAndType #61:#62 // out:Ljava/io/PrintStream;
#48 = Class #63 // java/io/PrintStream
#49 = NameAndType #64:#65 // println:(I)V
#50 = Utf8 java/lang/Exception
#51 = NameAndType #66:#19 // printStackTrace:()V
#52 = Utf8 java/lang/String
#53 = NameAndType #36:#37 // compareTo:(Ljava/lang/String;)I
#54 = NameAndType #14:#15 // num:I
#55 = Utf8 测试方法区
#56 = NameAndType #16:#17 // str:Ljava/lang/String;
#57 = Utf8 com/tttiger/MethodAreaTest
#58 = Utf8 java/lang/Object
#59 = Utf8 java/lang/Comparable
#60 = Utf8 java/lang/System
#61 = Utf8 out
#62 = Utf8 Ljava/io/PrintStream;
#63 = Utf8 java/io/PrintStream
#64 = Utf8 println
#65 = Utf8 (I)V
#66 = Utf8 printStackTrace
{
// 域信息
// 属性类型描述访问修饰符
public static int num;
// 类型描述
descriptor: I
// 访问修饰
flags: ACC_PUBLIC, ACC_STATIC
private static java.lang.String str;
descriptor: Ljava/lang/String;
flags: ACC_PRIVATE, ACC_STATIC
// 默认构造器
public com.tttiger.MethodAreaTest();
// 返回信息
descriptor: ()V
// 访问修饰
flags: ACC_PUBLIC
// 字节码
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 6: 0
// 局部变量表信息
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/tttiger/MethodAreaTest;
// 方法信息
public void test1();
// 返回类型
descriptor: ()V
// 访问修饰
flags: ACC_PUBLIC
// 字节码
Code:
stack=2, locals=2, args_size=1
0: bipush 20
2: istore_1
3: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
6: iload_1
7: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
10: return
// 行号对应指令的表
LineNumberTable:
line 13: 0
line 14: 3
line 15: 10
// 局部变量表
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/tttiger/MethodAreaTest;
3 8 1 count I
public static int test2(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 30
4: istore_2
5: iload_2
6: iload_0
7: idiv
8: istore_1
9: goto 17
12: astore_2
13: aload_2
14: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
17: iload_1
18: ireturn
// 异常表
Exception table:
from to target type
2 9 12 Class java/lang/Exception
LineNumberTable:
line 18: 0
line 21: 2
line 22: 5
line 25: 9
line 23: 12
line 24: 13
line 26: 17
LocalVariableTable:
Start Length Slot Name Signature
5 4 2 value I
13 4 2 e Ljava/lang/Exception;
0 19 0 cal I
2 17 1 result I
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ int, int ]
stack = [ class java/lang/Exception ]
frame_type = 4 /* same */
public int compareTo(java.lang.String);
descriptor: (Ljava/lang/String;)I
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iconst_0
1: ireturn
LineNumberTable:
line 31: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this Lcom/tttiger/MethodAreaTest;
0 2 1 o Ljava/lang/String;
public int compareTo(java.lang.Object);
descriptor: (Ljava/lang/Object;)I
flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: checkcast #6 // class java/lang/String
5: invokevirtual #7 // Method compareTo:(Ljava/lang/String;)I
8: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/tttiger/MethodAreaTest;
// 静态初始化clint方法,初始化赋值static属性整合static代码块
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #8 // Field num:I
5: ldc #9 // String 测试方法区
7: putstatic #10 // Field str:Ljava/lang/String;
10: return
LineNumberTable:
line 8: 0
line 9: 5
}
Signature: #42 // Ljava/lang/Object;Ljava/lang/Comparable;
SourceFile: "MethodAreaTest.java"
static 与 static final 区别
// 普通static在链接准备时进行初始化,赋值默认0值,然后再初始化阶段赋予实际值(clint方法中赋值)
public static int num;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
// static final 方法在编译器就指定了值
private static final int num2;
descriptor: I
flags: ACC_PRIVATE, ACC_STATIC, ACC_FINAL
// 直接指定值
ConstantValue: int 20
// clint 方法中对num进行实际赋值
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #8 // Field num:I
5: return
LineNumberTable:
line 8: 0
#3 = Methodref #48.#49 // java/io/PrintStream.println:(I)V
invokevirtual #3
class文件中方法调用,使用#3 符号殷弘标识常量池中的调用,(这都只是字面量,符号用于描述程序调用)虚拟机加载时将会替换成真是的符号所代表的描述的类或方法或字面量的真实地址(类似符号表)
方法区的回收效率不高,尤其是类型的卸载,条件很苛刻。但这个区域的回收有是有却也是必要的。(JDK 11 ZGC收集器不再支持类卸载)
方法区的垃圾回收主要回收两部分内容:常量池中废弃的常量和不再使用的类型
常量回收
方法区的常量池主要存放两大类信息:字面量和符号引用。字面量比较接近java语言层次的常量概念,如文本字符串,被声明为final的常量值等。而符号引用属于编译原理的概念,包括下面三类常量
类和接口的全限定名
字段的名称和表述符
方法的名称和描述符
HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收
类回收
判断一个class是否可以被回收条件是很苛刻的,需要同时满足三个条件
java虚拟机允许满足了上面的三个条件的类被回收,被允许不是必然,是否可以回收java虚拟机提供了 -Xnclassgc 控制
类的实例化的几种方式
// Object o = new Object();
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."":()V
7: astore_1
new一个对象对应到字节码层面就是,
使用new关键字 #2 (Object的标识符),Object类型加载,分配堆空间(类实例的占用空间是可以确定的),进行“零”值初始化。new字节码指令的作用是创建指定类型的对象实例、对其进行默认初始化,并且将指向该实例的一个引用压入操作数栈顶;
使用dup指令将new时分配的对象引用在复制一份,因为在下面调用invokespecical调用构造器会pop栈中的对象引用作为this传递,复制一份是为了astore 存储到局部变量表
创建对象的执行步骤
判断对象的类是否加载。加载、链接、初始化。(当虚拟机遇到一条new指令,首先去检查这个指令的参数能否在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载、解析和初始化。如果没有,那么在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为key进行查找对应的.class文件。如果没有找到文件,则抛出ClassNotFoundException异常,如果找到,则进行类加载,并生成对应的Class类对象
为对象分配内存。(首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。如果实例成员变量是引用变量,仅分配引用空间即可,即4字节)
初始化分配到的内存。(为属性设置默认值,保证对象实例属性在不赋值的情况下可用
设置对象头。(将对象的所属类(类元信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象头中、这个过程取决与JVM的实现)
执行init方法进行初始化。(初始化成员变量,执行实例代码块,调用构造方法,并将堆内对象的首地址赋值为引用变量)init方法就是将成员变量的直接赋值,实例代码块,构造器合并。
public class Consumer {
public int id = 888;
public String name;
public Account acct;
{
this.name = "匿名用户";
}
public Consumer(){
this.acct = new Account();
}
}
class Account{
}
public com.tttiger.Consumer();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: sipush 888
8: putfield #2 // Field id:I
11: aload_0
12: ldc #3 // String 匿名用户
14: putfield #4 // Field name:Ljava/lang/String;
17: aload_0
18: new #5 // class com/tttiger/Account
21: dup
22: invokespecial #6 // Method com/tttiger/Account."":()V
25: putfield #7 // Field acct:Lcom/tttiger/Account;
28: return
LineNumberTable:
line 11: 0
line 4: 4
line 9: 11
line 12: 17
line 13: 28
LocalVariableTable:
Start Length Slot Name Signature
0 29 0 this Lcom/tttiger/Consumer;
public class Consumer {
public int id = 888;
public String name;
public Account acct;
{
this.name = "匿名用户";
}
public Consumer(){
this.acct = new Account();
}
public static void main(String[] args) {
Consumer con = new Consumer();
}
}
class Account{
}
“虚拟机” 是一个相对于“物理机”的概念,这两种机器都有代码执行能力,区别在于物理机的执行引擎是直接建立在、处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎是由软件实现的,因此可以不受物理条件制约的定制指令集与执行引擎,能够执行那些不被硬件直接支持的指令
字节码文件并不能直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令,符号表,以及其他辅助信息。如果想要运行Java程序,执行引擎(Execution Engine)的任务就说将字节码指令/编译为平台上的本地机器指令运行
当java虚拟机启动时会根据规范定义对字节码逐行解释执行,将字节码翻译为平台对应的本地机器指令
将源代码(字节码)直接编译和本地机器相关的机器语言。
java语言为半编译半解释型语言主要原因就在于在执行代码有两种方式字节码解释器和JIT编译器
当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即响应执行。但解释执行的效率较低。
编译器想要发挥作用,把代码编译为本地机器码,需要一定的执行时间。但编译为本地代码后执行速度快。
当虚拟机启动的时候,解释器可以首先发挥作用,不必等待编译器全部编译完成,这样可以省去编译的时间快速启动程序。随着程序的运行,编译器逐渐发挥作用,查找热点代码,将有价值的代码编译为本地机器指令。
JIT编译器在运行时针对那些频繁被调用的“热点代码”做出深度优化。
一个被多次调用的方法,或是一个方法内部循环次数较多的循环体都可以被称之为热点代码,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生再方法执行过程中,因此也被称之为栈上替换(和栈上分配是两个东西),或简称为OSR(on stack replacement) 参考
热点探测采用计数器方法对方法调用次数、循环体执行次数进行技术,分别为方法调用计数器(invocation counter) 和回边计数器(back edge counter 统计循环次数)(JVM Clinet模式1500次,Server模式下10000次。超过次数会触发JIT)
-XX:CompileThreshold来认为设定
当一个方法被调用时,会检查该方法是否存在JIT编译后的代码,如果存在,则优先使用编译后的本地代码来执行。如果不存在编译后的版本,则将此方法的调用计数器+1,然后判断方法调用计数器和回边计数器之和是否超过方法调用计数器的阈值。如果超过JIT进行异步编译,编译完成后,栈上替换技术支持当前方法从解释执行切换为执行编译后的代码
热度衰减
方法调用计数并不是一直累加的。当超过一定时间限度,如果方法的调用次数还没有超过阈值,那么这个方法的计数就会衰减一半,这个过程称为热度衰减。-XX:-UseCounterDecay 关闭热度衰减这样程序运行时间长后都可以编译为本地代码。-XX:CounterHalfLifeTime 设置半衰时间单位秒。(热度衰减的动作是在虚拟机进行垃圾回收时顺便进行的)
-Xint 完全采用解释执行模式
-Xcomp 完全采用即时编译器执行程序。(有问题会再尝试解释器)
-Xmixed 混合模式
JVM有实际上由两种即时编译器分别对应JVM的Client模式与Server模式(64位虚拟机默认使用Server模式)简称C1,C2
java -client 、java -server 运行程序指定是使用C1编译器还是C2编辑器。C1编译器会对字节码简单和可靠的优化,耗时短。可以更快的编译。C2会进行耗时较长的深度的优化,但编译后机器码执行效率高。
String info = "abc";
// 与 String abc = "abc"; 是相同的
String abc = "a"+"b"+"c";
public void newString
// 常量池中
String a = "a";
// 常量池中
String b = "b";
// 堆中
String abc = a+b+"c";
}
生成的字节码
// 加载常量池中a的引用入栈
0 ldc #2
// 存储在局部变量表1的位置(0的位置是this)
2 astore_1
// 加载常量池中b的引用入栈
3 ldc #3
// 存储在局部变量表2的位置(变量b对应的位置)
5 astore_2
// 开始拼接字符串,首先new了一个StringBuilder,分配内存,默认初始化
6 new #4
// 赋值栈顶引用(invokespecial会用掉一个引用,dup后栈顶又两个对象的引用)
9 dup
// 调用构造方法
10 invokespecial #5 >
// 加载局部变量1位置,也就是a
13 aload_1
// 调用StringBuilder的append拼接参数
14 invokevirtual #6
// 加载局部变量2位置,也就是b
17 aload_2
// 调用StringBuilder的append拼接参数
18 invokevirtual #6
// 加载常量池中的c
21 ldc #7
// 调用StringBuilder的append拼接参数
23 invokevirtual #6
// 调用StringBuilder的toString方法,返回String类型
26 invokevirtual #8
// 存储在局部变量表3的位置(abc)
29 astore_3
30 return
// StringBuilder的toString
public String toString() {
// 动态拼接的变量存储在堆中
return new String(this.value, 0, this.count);
}
// 所以 ab == "ab" 结果是false,一个在堆中一个在常量池中
public void testString(){
String a = "a";
String b = "b";
String ab = a+b;
System.out.println(ab == "ab");
}
// 如果变量都被声明为final那么编译时也会进行优化,会把a+b的结果给常量池放一份直接赋值给变量ab
public void newString(){
final String a = "a";
final String b = "b";
String ab = a+b;
System.out.println(ab == "ab");
}
String abc = "abc";
String abc2 = new String("abc");
System.out.println(abc == abc2);
System.out.println(abc == abc2.intern());
---sout
false
true
---字节码
0 ldc #12 <abc>
2 astore_1
3 new #13 <java/lang/String>
6 dup
7 ldc #12 <abc>
9 invokespecial #14 <java/lang/String.<init>>
12 astore_2
// 一个是直接加载常量池引用,一个是new Stirng 构造器传入常量池引用,abc2在堆中,abc在常量池中
深入理解intern方法
intern方法在1.6之后机制有所不同,在jdk1.6之后字符串常量池从方法区放在了堆中,字符串的intern也有所不同
String a = new String("哈")+new String("哈");
a.intern();
String b = "哈哈";
System.out.println(a == b);
// 1.6 版本 false
// 1.6 之后版本 true
直接来看下字节码
0 new #8
3 dup
4 invokespecial #9 >
7 new #2
10 dup
11 ldc #10 <哈>
13 invokespecial #4 >
16 invokevirtual #11
19 new #2
22 dup
23 ldc #10 <哈>
25 invokespecial #4 >
28 invokevirtual #11
31 invokevirtual #12
34 astore_1 // 字符串拼接完成,此时字符串常量池中只有 “哈”
35 aload_1
36 invokevirtual #5 // 将拼接结果 “哈哈” 放入常量池
39 pop
40 ldc #13 <哈哈> // 从常量池找 “哈哈”
42 astore_2 // 赋值给 b 变量
43 getstatic #6
46 aload_1
47 aload_2
48 if_acmpne 55 (+7)
51 iconst_1
52 goto 56 (+4)
55 iconst_0
56 invokevirtual #7
59 return
再看这段代码
String a = new String("哈")+new String("哈");
// 返回常量池中的引用
String c = a.intern();
String b = "哈哈"; // 去常量池找"哈哈",没有常量池放入"哈哈”,但是a.intern()已经放入"哈哈"
System.out.println(a == b);
System.out.println(a == c);
// 1.6 false false
// 1.6 之后版本 true true
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a6cAQzJH-1618211668116)(https://gitee.com/diaosinanshen/picgo/raw/master/img/StringTable (1)].jpg)
在jdk6之后使用String的intern方法,如果字符串常量池中有则返回常量池引用,如果没有在常量池中直接保存调用对象的引用。
在 JDK1.6下 a.intern() 时字符串常量池中还没有“哈哈”,a.intern() 调用后给常量池放入了“哈哈”,返回常量池中“哈哈”的引用,这与之前堆中的a指向的堆中的对象没有任何关系。
在 JDK 1.7/8 下 a.intern() 时字符串常量池中还没有“哈哈”,a.intern() 将自身的引用放入字符串常量池,返回常量池中“哈哈”的引用,这与a指向的是堆中的同一个对象。
总结
jdk 1.6中,调用String的intern方法
jdk 1.7/8中,调用String的intern方法
垃圾回收机制是Java的招牌能力,极大提高开发效率,如今垃圾回收几乎成为现代语言的标配。
垃圾是指在程序运行期间没有任何指针指向的对象,这个对象占用的内存就需要被回收。如果不即时堆内存中的垃圾进行清理,那么,这些垃圾占用的内存会一直保留到应用进程结束,垃圾占用的内存无法被其他内存使用,甚至因此导致内存溢出。
在GC执行垃圾回收之前,首先需要确定内存中那些对象“存活”,那么对象“死亡”。已有被标记为已"死亡"的对象,GC才会对其进行会后。这个确定对象是否存活的阶段称为标记阶段。(当一个对象已经不再被任何存活的对象继续引用时,就被当作“死亡”对象)
引用计数算法就说为每一个对象保存一个计数属性,用来记录对象的引用次数,只要有任何对象引用了此对象引用次数加一,当引用失效时计数器减一。这种方法虽然实现简单,对象辨识度高,回收效率告,但它有着致命的缺点,即无法解决循环引用的问题。
相对于引用计数算法,可达性分析算法不仅同样具备实现简单和执行效率高等特点,更重要的是该算法可以有效的解决循环引用的问题,防止内存泄漏。Java垃圾的标记就使用可达性分析算法 。
可以作为GCRoots的引用
在执行可达性分析时,分析工作必须在一个保证一致性的内存快照中,这点如果不满足那么分析结果就不是准确的,这点也是导致GC必须进行Stop the world 的原因,枚举根节点必须停止
当垃圾回收器发现没有引用指向一个对象,这个对象就是垃圾,在回收它之前,总会先调用这个对象的finalize()方法。
finalize()方法允许在子类中重写,常用于在对象回收时进行一些资源释放。GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义如下:
unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
finalized: 表示GC已经对该对象执行过finalize方法
reachable: 表示GC Roots引用可达
finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
unreachable:对象不可通过上面两种途径可达
当成功区分出内存中存活对象与死亡对象时,GC接下来的任务就是执行垃圾回收,释放掉死亡对象占用的空间,以便于接下来生成的对象内存分配。
目前在JVM中常见的垃圾清除算法可分为三种
缺点:
1. 效率不高
2. 在进行GC的时候,需要停止整个应用程序
3. 清理会产生内存碎片,需要维护空闲列表
注意,这里的清除不是真的置空,而是将需要清除的对象占用的内存地址保存到空闲列表中
复制算法将内存分为两个区域AB使用(A大小=B大小),每次只使用其中一个区域,若当前使用A区域,在进行垃圾回收时就是将存活的对象复制到B区域,然后清空A区域,接下来使用B区域,如此往复。
优点
缺点
背景:复制算法的高效是建立在存活对象少,垃圾对象多的前提下。这种情况在新生代经常发生,但是在老年代,大部分都是存活的对象,如果继续使用复制算法,由于存活对象过多,复制的成本也将很高。
优点
缺点
每种算法都有自己的优缺点,分代收集算法是基于这样一个事实:不用的对象生命周期是不一样的,因此不同生命周期的对象可以采用不同的收集方式。一般Java中将堆内存分为新生代和老年代,这个可以根据不同的年龄代的特点使用不同的回收算法,以提高垃圾回收的效率。
年轻代,老年代 1:2;年轻代eden,s0,s1,8:1:1 (JVM默认分代比例)
如果一次垃圾回收的时间过长,那么可以让垃圾回收线程与用户线程交替执行。每次垃圾回收器只收集一小片区域的内存空间,然后切换到用户线程。如此反复直到垃圾回收完成。增量收集本质上还是标记清除,标记整理算法,他是通过对线程冲突的妥善处理,允许垃圾回收期分阶段的完成标记、清除、整理或复制工作。(例如CMS回收器)
一般来说,相同条件下,堆空间越大,一次GC所需要的时间就越长,有关GC产生的停顿也越长。为了更好的控制GC的停顿时间,将一块大的内存分为成多个小块,根据需要的停顿时间,每次合理的回收若干个小区间,而不是回收整个内存,从而减少GC停顿时间。(G1回收器)
StopTheWorld,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用线程被暂停,没有任何相应,这个停顿被称为StopTheWorld。
可达性分析算法中枚举根节点的过程会导致Java执行线程停顿。
oop (ordinary object pointer) 普通对象指针,oopmap就是存放这些指针的map,OopMap 用于枚举 GC Roots,记录栈中引用数据类型的位置。迄今为止,所有收集器在根节点枚举这一步骤都是必须暂停用户线程的,
收集线程会对栈上的内存进行扫描,看看哪些位置存储了Reference类型。如果发现某个位置确实存的是Reference类型,它所引用的对象这一次不能被回收。问题是,栈上的本地变量表里面只有一部分数据是Reference类型的,那些非Reference类型的数据对我们而言毫无用途,但我们还是不得不堆整个栈全部扫描一遍,这是对时间和资源的一种浪费。
一个很自然的想法时,能不能用空间换时间,把栈上代表的引用的位置全部记录下来,这样到真正gc的时候就可以直接读取,而不用再一点一点的扫描了,Hotspot就是实现的。它使用一种叫做OopMak的数据结构来记录这类信息。
一个线程为一个栈,一个栈由多个栈桢组成,一个栈桢对应一个方法,一个方法有多个安全点。GC发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的OopMap,记录栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈桢的OopMap ,通过栈中记录的被引用的对象内存地址,即可找到这些对象(GC Roots)
总结oopMap的作用
可以避免全栈扫描,加快枚举根节点的速度
可以帮助HotSpot实现准确式GC
程序并不是在所有地方都会停下来进行GC,只有在特定的位置才会停下来GC,这些位置就称为安全点。
安全点的选择很重要,如果太少可能导致等待GC时间太长,如果太多可能导致GC太频繁。大部分的执行时间都非常短暂,通常会根据是否语句让程序长时间执行的特征为标准,比如选择一个执行时间较长的指令作为SafePoint,如方法调用,循环跳转和异常跳转。
SafePoint机制保证了程序执行时,在不太长时间就会遇到可进入GC的SafePoint。但是如果程序处于Sleep状态或Blocked状态,线程无法走到安全点去中断挂起,JVM也不可能等待线程被唤醒。这种情况就需要安全区域来解决。
安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的
吞吐量
吞吐量代表着用户代码运行时间占总运行时间的占比。(总运行时间= 程序运行时间 + 垃圾回收时间)
暂停时间
执行垃圾收集时,程序被暂停的时间。(垃圾回收的频率与暂停时间存在相关关系,通常来说垃圾回收频率越低,程序暂停时间越长)
这两个指标也表示这垃圾回收器的两个发展方向。吞吐量优先,意味着在单位时间中,STW的时间最短,STW在总运行时间中的占比最少。暂停时间优先,意味着每次垃圾回收时暂停用户线程的时间最短,低延时。(暂停时间优先往往意味着回收频率的提高,总的停顿时间可能更长)
(基于JDK14最新的搭配使用方案)
7款经典的垃圾收集器
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:Serial Old、Parallel Old、CMS
整堆收集器:CMS
Serial 是最基本的,历史最悠久的垃圾收集器。JDK1.3之前新生代的唯一选择。
Serial收集器采用复制算法、串行回收(单线程)和STW机制的方式执行内存回收。
除了年轻代之外 ,Serial还提供老年代的版本Serial Old。Serial Old收集器采用标记压缩算法,串行回收和STW机制方式执行内存回收。
Serial 系列收集器在进行垃圾回收时,只会使用一个线程进行垃圾回收,且在垃圾回收期间必须暂停其他工作线程,Serial收集器实现简单在单CPU下回收效率也不错
-XX:+UseSerialGC 参数可以指定年轻代和老年代都使用串行收集器。Serial 与 Serial Old搭配使用
-XX:+UseParNewGC 指定新生代使用ParNewGC
-XX:ParallelGCThreads 执行回收线程数量,默认开启和CPU数量相同的线程数
-XX:+UseParallelGC 手动指定年轻代使用Parallel收集器
-XX:+UseParallelOldGC 手动指定老年代使用Parallel收集器,只要指定一个UseParallelGC或UseParallelOldGC另一个参数都会被激活配合使用
-XX:ParallelGCThreads 设置并行收集线程数,默认是CPU数量(当CPU小于8时),CPU大于8时线程数=3+[5*cpuCount/8]
-XX:MaxGCPauseMillis 设置垃圾收集器最大停顿时间(即STW时间,此参数的设置侧重暂停时间优先)。
-XX:GCTimeRatio 垃圾收集时间占比,侧重吞吐量。取值范围(1-100),默认为99,也就是垃圾回收时间占比1%
-XX:+UseAdaptiveSizePolicy 设置开启Parallel 自适应调节策略,在这种模式下,年轻代,Eden和Survivor的比值、晋升老年代的对象年龄等参数会被自动调节,以达到在堆大小、吞吐量和停顿时间之间的平衡。默认是开启状态
在JDK1.5时期,HotSpot推出了一款在强交互应用中具有跨时代意义的垃圾收集器,CMS(Concurrent-Mark-Sweep)收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作
CMS收集器的关注点是尽可能的缩短垃圾回收时暂停用户线程的时间。CMS采用标记清除算法,并且也会STW
初始标记:初始标记阶段,需要STW,这个阶段仅仅只是标记出GC Roots能直接关联的对象,所以标记非常快
并发标记:在这个阶段直接从GC Roots开始遍历整个对象图,耗时时间较长,不需要停止用户线程
重新标记:由于并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段要长一些
在并发标记过程成会中产生两种问题需要在重新标记阶段解决,1. 本来可达的对象不可达了(浮动垃圾)2. 本来不可达的内存可达了
浮动垃圾是可以容忍的,等待下一次回收
CMS采用标记清除算法,如果在并发标记阶段new了一个对象,但并发标记并没有从GC Roots找到该对象标记为可达,那么在清理过程中就会清理到可达对象,重新标记阶段就是修正这种错误。在并发清理阶段保证所有被标记为不可达的地址,都是真正不死亡对象。建议阅读
并发清理:清理删除掉标记为死亡的对象,是否内存空间。由于不需要移动存活对象,所以在这个阶段可以并发完成
由于最耗时的两个阶段并发标记与并发清除阶段都不需要停止用户线程,所以整体的回收是低停顿的。由于在垃圾收集阶段用户线程没有中断,所以在CMS的回收过程中,还要确保用户线程有足够的内存可用。因此CMS收集器不能等到老年代满了再回收,在达到设置阈值时就要提前进行垃圾回收。如果CMS运行期间内存不够用了,这是就会出现”Concurrent Mode Failure“这时虚拟机将会启动后备方案:临时启动 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就更长了。CMS由于使用标记清除,会产生内存碎片,当空闲空间不足时,不得不提前触发Full GC进行碎片整理
-XX:+UseConcMarkSweepGC 手动指定使用CMS 收集器执行内存回收任务。开启该参数后自动加 -XX:UseParNewGC打开。也就是说将会使用ParNew(Yound区)+ CMS(Old区)+Serial Old 后备的组合。
-XX:CMSInitiatingOcuupanyFraction 设置堆内存使用率的阈值,当达到这个阈值就触发垃圾回收。JDK5之前默认是68%,JDK6即以上版本默认是92%,此阈值的设置要根据程序使用内存的增长速度来适当设置,如果程序使用内存增长快而阈值又大很可能频繁触发FullGC
-XX:+UseCMSCompactAtFullCollection 用于指定执行完FullGC后对内存空间进行压缩整理,以此避免内存碎片。内存整理无法并发完成所以停顿时间将会加长。
-XX:CMSFullGCsBeforeCompaction 设置执行多少次FullGC后进行内存整理
-XX:ParallelCMSThreads 设置CMS 并发执行时线程数量,默认启动的线程数是(ParallelGCThreads+3)/4
在JDK9中CMS垃圾收集器已经被标记为废弃。在JDK14中已经删除CMS收集器,如强制指定会使用默认收集器
官方给G1设定的目标是在延迟可控的情况下获得尽可能高的吞吐量,希望它担当起”全功能收集器“的重任。
G1是一个并行收集器,它把堆内存分成很多不相关的区域(Region)。使用不同的Region来表示Eden,幸存者0区,幸存者1区,老年代。G1跟踪各个Region里面垃圾堆积的价值大小(回收可以释放的空间大小和所需要的时间的经验)在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,Garbage First。在JDK7中正式启用,是JDK9以后的默认收集器,(JDK9移除了CMS)
并行与并发
分代收集
空间整合
可预测的停顿模型
相较于CMS,G1还不具备全范围,压倒性的优势。比如在用户程序允许过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(OverLoad)都要比CMS要高。通常来说在小内存上CMS表现大概率会由于G1,而G1在大内存应用上会更有优势。CMS与G1的平衡点大概在6-8GB内存之间。
虽然G1还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的,它们都是一部分Region的集合。通过Region的动态分配方式实现逻辑上的连续。
一个空闲的Region可能被分配为Eden,Survivor,Old,Humongous,4中不同的角色,每块区域回收后都会重新分配角色。humongous用户存储大对象,如果一个超过1.5个Region大小,就会被放到humongous角色的region中,如果一个humongous也放不下就会找连续的humongous region进行分配。在Region中,内存的分配使用指针碰撞完成内存分配,TLAB机制在Region中也是存在的
无论G1 还是其他分代收集器,JVM都采用RememberSet来避免全局扫描(一个年轻代的对象可能被老年代对象引用,那么只扫描年轻代的GCRoots那么不能确定这个对象是否存活)。
G1的垃圾回收:
G1 中提供了三种垃圾回收模式:YoungGC、MixedGC、和 Full GC,分别在不同条件下触发。(Full GC 单线程,独占式,高强度的Full GC作为失败保护机制任然存在)
年轻代GC
混合回收
当内存使用达到一定值(默认45%)时,开始老年代并发标记过程。当标记完成后马上开始混合回收,G1 GC 从老年代区间移动存活对象到空闲空间,这些空闲也将称为老年代的一部分,和年轻代不同,G1的老年代回收并不需要回收整个老年代,一次只扫描回收一部分(满足暂停事件,回收价值高的region),老年代的Region和年轻代Region一起回收。
-XX:+UseG1GC 手动指定使用G1收集器执行内存回收任务
-XX:G1HeapRegionSize 设置每个Region的大小。值必须是2的幂,范围是1MB到32MB之间。使用G1时Java堆会被分为大小统一的的区(region)。此参数可以指定每个heap区的大小. 默认值将根据 heap size 算出最优解
-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间指标。G1会尽量满足期望,默认事件是200ms
-XX:ParallelGCThread 设置垃圾收集器在并行阶段使用的线程数。STW是多线程回收垃圾,最多设置为8
-XX:ConcGCThreads 设置并发标记的线程数。
-XX:InitiatingHeapOccupancyPercent 设置触发GC周期的Java堆占用阈值。超过此值触发GC,默认值是45。