JVM学习笔记(二):JVM基本结构
1 来源
- 来源:《Java虚拟机 JVM故障诊断与性能优化》——葛一鸣
- 章节:第二章
本文是第二章的一些笔记整理。
2 JVM
基本参数-Xmx
java
命令的一般形式如下:
java [-options] class [args..]
其中-options
表示JVM
启动参数,class
为带有main()
的Java
类,args
表示传递给main()
的参数,也就是main(String [] args)
中的参数。
一般设置参数在-optinos
处设置,先看一段简单的代码:
public class Main {
public static void main(String[] args) {
for(int i=0;i<args.length;++i) {
System.out.println("argument "+(i+1)+" : "+args[i]);
}
System.out.println("-Xmx "+Runtime.getRuntime().maxMemory()/1024/1024+" M");
}
}
设置应用程序参数以及JVM
参数:
输出:
可以看到-Xmx32m
传递给JVM
,使得最大可用堆空间为32MB
,参数a
作为应用程序参数,传递给main()
,此时args.length
的值为1。
3 JVM
基本结构
各部分介绍如下:
类加载子系统
:负责从文件系统或者网络中加载Class
信息,加载的类信息存放在一个叫方法区
的内存空间中方法区
:除了包含加载的类信息之外,还包含运行时常量池信息,包括字符串字面量以及数字常量Java堆
:在虚拟机启动时建立,是最主要的内存工作区域,几乎所有的Java
对象实例都存在于Java堆
中,堆空间是所有线程共享的直接内存
:是在Java堆
外的,直接向系统申请的内存区域。NIO
库允许Java
程序使用直接内存
,通常直接内存
的访问速度要优于Java堆
。另外由于直接内存
在堆外,大小不会受限于-Xmx
指定的堆大小,但是会受到操作系统总内存大小的限制垃圾回收系统
:可以对方法区
、Java堆
和直接内存
进行回收,Java堆
是垃圾收集器的工作重点。对于不再使用的垃圾对象,垃圾回收系统
会在后台默默工作、默默查找,标识并释放垃圾对象Java栈
:每个JVM
线程都有一个私有的Java栈
,一个线程的Java栈
在线程创建时被创建,保存着帧信息、局部变量、方法参数等本地方法栈
:与Java栈
类似,不同的是Java栈
用于Java
方法调用,本地方法栈
用于本地方法(native method
)调用,JVM
允许Java
直接调用本地方法PC寄存器
:每个线程私有的空间,JVM
会为每个线程创建PC寄存器
,在任意时刻一个Java
线程总是执行一个叫做当前方法
的方法,如果当前方法
不是本地方法,PC
寄存器就会指向当前正在被执行的指令,如果当前方法
是本地方法,那么PC寄存器
的值就是undefined
执行引擎
:负责执行JVM
的字节码,现代JVM
为了提高执行效率,会使用即时编译技术将方法编译成机器码后执行
下面重点说三部分:Java堆
、Java栈
以及方法区
。
4 Java堆
几乎所有的对象都存在Java堆
中,根据垃圾回收机制的不同,Java堆
可能拥有不同的结构,最常见的一种是将整个Java堆
分为新生代
和老年代
:
新生代
:存放新生对象或年龄不大的对象,有可能分为eden
、s0
、s1
,其中s0
和s1
分别被称为from
和to
区域,它们是两块大小相等、可以互换角色的内存空间老年代
:存放老年对象,绝大多数情况下,对象首先在eden
分配,在一次新生代回收后,如果对象还存活,会进入s0
或s1
,之后每经过一次新生代回收,如果对象存活则年龄加1。当对象年龄到达一定条件后,会被认为是老年对象,就会进入老年代
5 Java栈
5.1 简介
Java栈
是一块线程私有的内存空间,如果是Java堆
与程序数据密切相关,那么Java栈
和线程执行密切相关,线程执行的基本行为是函数调用,每次函数调用都是通过Java栈
传递的。
Java栈
与数据结构中的栈
类似,有FIFO
的特点,在Java
栈中保存的主要内容为栈帧,每次函数调用都会有一个对应的栈帧
入栈,每次调用结束就有一个对应的栈帧
出栈。栈顶总是当前的帧(当前执行的函数所对应的帧)。栈帧保存着局部变量表
、操作数栈
、帧数据
等。
这里说一下题外话,相信很多读者对StackOverflowError
不陌生,这是因为函数调用过多造成的,因为每次函数调用都会生成对应的栈帧,会占用一定的栈空间,如果栈空间不足,函数调用就无法进行,当请求栈深度大于最大可用栈深度时,就会抛出StackOverflowError
。
JVM
提供了-Xss
来指定线程的最大栈空间。
比如,下面这个递归调用的程序:
public class Main {
private static int count = 0;
public static void recursion(){
++count;
recursion();
}
public static void main(String[] args) {
try{
recursion();
}catch (StackOverflowError e){
System.out.println("Deep of calling = "+count);
}
}
}
指定-Xss1m
,结果:
指定-Xss2m
:
指定-Xss3m
:
可以看到调用深度随着-Xss
的增加而增加。
5.2 局部变量表
局部变量表是栈帧的重要组成部分之一,用于保存函数的参数及局部变量。局部变量表中的变量只在当前函数调用中有效,函数调用结束后,函数栈帧销毁,局部变量表也会随之销毁。
5.2.1 参数数量对局部变量表的影响
由于局部变量表在栈帧中,如果函数的参数和局部变量表较多,会使局部变量表膨胀,导致栈帧会占用更多的栈空间,最终减少了函数嵌套调用次数。
比如:
public class Main {
private static int count = 0;
public static void recursion(long a,long b,long c){
long e=1,f=2,g=3,h=4,i=5,k=6,q=7;
count++;
recursion(a,b,c);
}
public static void recursion(){
++count;
recursion();
}
public static void main(String[] args) {
try{
// recursion();
recursion(0L,1L,2L);
}catch (StackOverflowError e){
System.out.println("Deep of calling = "+count);
count = 0;
}
}
}
无参数的调用次数(-Xss1m
):
带参数的调用次数(-Xss1m
):
可以看到次数明显减少了,原因正是因为局部变量表变大,导致栈帧变大,从而次数减少。
下面使用jclasslib
进一步查看,先在IDEA
安装如下插件:
安装后使用插件查看情况:
第一个函数是带参数的,可以看到最大局部变量表的大小为20字
(注意不是字节),Long
在局部变量表中需要占用2字。而相比之下不带参数的函数最大局部变量表大小为0:
5.2.2 槽位复用
局部变量表中的槽位是可以复用的,如果一个局部变量超过了其作用域,则在其作用域之后的局部变量就有可能复用该变量的槽位,这样能够节省资源,比如:
public static void localVar1(){
int a = 0;
System.out.println(a);
int b = 0;
}
public static void localVar2(){
{
int a = 0;
System.out.println(a);
}
int b = 0;
}
同样使用jclasslib
查看:
可以看到少了localVar2
的最大局部变量大小为1字,相比localVar1
少了1字,继续分析,localVar1
第0个槽位为变量a,第1个槽位为变量b:
而localVar2
中的b复用了a的槽位,因此最大变量大小为1字,节约了空间。
5.2.3 对GC
的影响
下面再来看一下局部变量表对垃圾回收的影响,示例:
public class Main {
public static void localGC1(){
byte [] a = new byte[6*1024*1024];
System.gc();
}
public static void localGC2(){
byte [] a = new byte[6*1024*1024];
a = null;
System.gc();
}
public static void localGC3(){
{
byte [] a = new byte[6*1024*1024];
}
System.gc();
}
public static void localGC4(){
{
byte [] a = new byte[6*1024*1024];
}
int c = 10;
System.gc();
}
public static void localGC5(){
localGC1();
System.gc();
}
public static void main(String[] args) {
System.out.println("-------------localGC1------------");
localGC1();
System.out.println();
System.out.println("-------------localGC2------------");
localGC2();
System.out.println();
System.out.println("-------------localGC3------------");
localGC3();
System.out.println();
System.out.println("-------------localGC4------------");
localGC4();
System.out.println();
System.out.println("-------------localGC5------------");
localGC5();
System.out.println();
}
}
输出(请加上-Xlog:gc
参数):
[0.004s][info][gc] Using G1
-------------localGC1------------
[0.128s][info][gc] GC(0) Pause Full (System.gc()) 10M->8M(40M) 12.081ms
-------------localGC2------------
[0.128s][info][gc] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation) 9M->8M(40M) 0.264ms
[0.128s][info][gc] GC(2) Concurrent Cycle
[0.133s][info][gc] GC(3) Pause Full (System.gc()) 16M->0M(14M) 2.799ms
[0.133s][info][gc] GC(2) Concurrent Cycle 4.701ms
-------------localGC3------------
[0.133s][info][gc] GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 0M->0M(14M) 0.203ms
[0.133s][info][gc] GC(5) Concurrent Cycle
[0.135s][info][gc] GC(5) Pause Remark 8M->8M(22M) 0.499ms
[0.138s][info][gc] GC(6) Pause Full (System.gc()) 8M->8M(22M) 2.510ms
[0.138s][info][gc] GC(5) Concurrent Cycle 4.823ms
-------------localGC4------------
[0.138s][info][gc] GC(7) Pause Young (Concurrent Start) (G1 Humongous Allocation) 8M->8M(22M) 0.202ms
[0.138s][info][gc] GC(8) Concurrent Cycle
[0.142s][info][gc] GC(9) Pause Full (System.gc()) 16M->0M(8M) 2.861ms
[0.142s][info][gc] GC(8) Concurrent Cycle 3.953ms
-------------localGC5------------
[0.143s][info][gc] GC(10) Pause Young (Concurrent Start) (G1 Humongous Allocation) 0M->0M(8M) 0.324ms
[0.143s][info][gc] GC(11) Concurrent Cycle
[0.145s][info][gc] GC(11) Pause Remark 8M->8M(16M) 0.316ms
[0.147s][info][gc] GC(12) Pause Full (System.gc()) 8M->8M(18M) 2.402ms
[0.149s][info][gc] GC(13) Pause Full (System.gc()) 8M->0M(8M) 2.462ms
[0.149s][info][gc] GC(11) Concurrent Cycle 6.843ms
首行输出表示使用G1
,下面逐个进行分析:
localGC1
:并没有回收内存,因为此时byte
数组被变量a
引用,因此无法回收localGC2
:回收了内存,因为a
被设置为了null
,byte
数组失去强引用localGC3
:没有回收内存,虽然此时a
变量已经失效,但是仍然存在于局部变量表中,并且指向byte
数组,因此无法回收localGC4
:回收了内存,因为声明了变量c
,复用了a
的槽位,导致byte
数组失去引用,顺利回收localGC5
:回收了内存,虽然localGC1
中没有释放内存,但是返回到localGC5
后,localGC1
的栈帧被销毁,也包括其中的byte
数组失去了引用,因此在localGC5
中被回收
5.3 操作数栈与帧数据区
操作数栈也是栈帧的重要内容之一,主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间,也是一个FIFO
的数据结构。
而帧数据区则保存着常量池指针,方便程序访问常量池,此外,帧数据区也保存着异常处理表,以便在出现异常后,找到处理异常的代码。
5.4 栈上分配
栈上分配是JVM
提供的一项优化技术,基本思想是,将线程私有的对象打散分配到栈上,好处是函数调用结束后可以自动销毁,而不需要垃圾回收器的介入,从而提高系统性能。
栈上分配的一个技术基础是逃逸分析,逃逸分析目的是判断对象的作用域是否会逃逸出函数体,例子如下:
public class Main {
private static int count = 0;
public static class User{
public int id = 0;
public String name = "";
}
public static void alloc(){
User user = new User();
user.id = 5;
user.name = "test";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
}
启动参数:
-server # 开启Server模式,此模式下才能开启逃逸分析
-Xmx10m # 最大堆内存
-Xms10m # 初始化堆内存
-XX:+DoEscapeAnalysis # 开启逃逸分析
-Xlog:gc # GC日志
-XX:-UseTLAB # 关闭TLAB
-XX:+EliminateAllocations # 开启标量替换,默认打开,允许将对象打散分配在栈上
输出如下,没有GC
日志:
而如果关闭了标量替换,也就是添加-XX:-EliminateAllocations
,就可以看到会频繁触发GC
,因为这时候对象存放在堆上而不是栈上,堆只有10m空间,会频繁进行GC
:
6 方法区
与Java堆
一样,方法区
是所有线程共享的内存区域,用于保存系统的类信息,比如类字段、方法、常量池等,方法区
的大小决定了系统可以保存多少个类,如果定义了过多的类,会导致方法区
溢出,会直接OOM
。
在JDK6/7
中方法区
可以理解成永久区
,JDK8
后,永久区
被移除,取而代之的是元数据区
,可以使用-XX:MaxMetaspaceSize
指定,这是一块堆外的直接内存,如果不指定大小,默认情况下JVM
会耗尽所有可用的系统内存。
如果元数据区
发生溢出,JVM
会抛出OOM
。
7 Java堆
、Java栈
以及方法区
的关系
看完了Java堆
、Java栈
以及方法区
,最后来一段代码来简单分析一下它们的关系:
class SimpleHeap{
private int id;
public SimpleHeap(int id){
this.id = id;
}
public void show(){
System.out.println("id is "+id);
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}
main
中创建了两个局部变量s1
、s2
,则这两个局部变量存放在Java栈
中。同时这两个局部变量是SimpleHeap
的实例,这两个实例存放在Java堆
中,而其中的show
方法,则存放与方法区
中,图示如下:
8 小结
本文主要讲述了JVM
的基本结构以及一些基础参数,基本结构可以分成三部分:
- 第一部分:
类加载子系统
、Java堆
、方法区
、直接内存
- 第二部分:
Java栈
、本地方法栈
、PC寄存器
- 第三部分:执行引擎
而重点讲了三部分:
Java堆
:常见的结构为新生代
+老年代
结构,其中新生代可分为edsn
、s0
、s1
Java栈
:包括局部变量表、操作数栈与帧数据区,还提到了一个JVM
优化技术栈上分配,可以通过-XX:+EliminateAllocation
开启(默认开启)方法区
:所有线程共享区域,用于保存类信息,比如类字段、方法、常量等# 1 来源来源:《Java虚拟机 JVM故障诊断与性能优化》——葛一鸣
章节:第二章
本文是第二章的一些笔记整理。
2 JVM
基本参数-Xmx
java
命令的一般形式如下:
java [-options] class [args..]
其中-options
表示JVM
启动参数,class
为带有main()
的Java
类,args
表示传递给main()
的参数,也就是main(String [] args)
中的参数。
一般设置参数在-optinos
处设置,先看一段简单的代码:
public class Main {
public static void main(String[] args) {
for(int i=0;i<args.length;++i) {
System.out.println("argument "+(i+1)+" : "+args[i]);
}
System.out.println("-Xmx "+Runtime.getRuntime().maxMemory()/1024/1024+" M");
}
}
设置应用程序参数以及JVM
参数:
输出:
可以看到-Xmx32m
传递给JVM
,使得最大可用堆空间为32MB
,参数a
作为应用程序参数,传递给main()
,此时args.length
的值为1。
3 JVM
基本结构
各部分介绍如下:
类加载子系统
:负责从文件系统或者网络中加载Class
信息,加载的类信息存放在一个叫方法区
的内存空间中方法区
:除了包含加载的类信息之外,还包含运行时常量池信息,包括字符串字面量以及数字常量Java堆
:在虚拟机启动时建立,是最主要的内存工作区域,几乎所有的Java
对象实例都存在于Java堆
中,堆空间是所有线程共享的直接内存
:是在Java堆
外的,直接向系统申请的内存区域。NIO
库允许Java
程序使用直接内存
,通常直接内存
的访问速度要优于Java堆
。另外由于直接内存
在堆外,大小不会受限于-Xmx
指定的堆大小,但是会受到操作系统总内存大小的限制垃圾回收系统
:可以对方法区
、Java堆
和直接内存
进行回收,Java堆
是垃圾收集器的工作重点。对于不再使用的垃圾对象,垃圾回收系统
会在后台默默工作、默默查找,标识并释放垃圾对象Java栈
:每个JVM
线程都有一个私有的Java栈
,一个线程的Java栈
在线程创建时被创建,保存着帧信息、局部变量、方法参数等本地方法栈
:与Java栈
类似,不同的是Java栈
用于Java
方法调用,本地方法栈
用于本地方法(native method
)调用,JVM
允许Java
直接调用本地方法PC寄存器
:每个线程私有的空间,JVM
会为每个线程创建PC寄存器
,在任意时刻一个Java
线程总是执行一个叫做当前方法
的方法,如果当前方法
不是本地方法,PC
寄存器就会指向当前正在被执行的指令,如果当前方法
是本地方法,那么PC寄存器
的值就是undefined
执行引擎
:负责执行JVM
的字节码,现代JVM
为了提高执行效率,会使用即时编译技术将方法编译成机器码后执行
下面重点说三部分:Java堆
、Java栈
以及``
4 Java堆
几乎所有的对象都存在Java堆
中,根据垃圾回收机制的不同,Java堆
可能拥有不同的结构,最常见的一种是将整个Java堆
分为新生代
和老年代
:
新生代
:存放新生对象或年龄不大的对象,有可能分为eden
、s0
、s1
,其中s0
和s1
分别被称为from
和to
区域,它们是两块大小相等、可以互换角色的内存空间老年代
:存放老年对象,绝大多数情况下,对象首先在eden
分配,在一次新生代回收后,如果对象还存活,会进入s0
或s1
,之后每经过一次新生代回收,如果对象存活则年龄加1。当对象年龄到达一定条件后,会被认为是老年对象,就会进入老年代
5 Java栈
5.1 简介
Java栈
是一块线程私有的内存空间,如果是Java堆
与程序数据密切相关,那么Java栈
和线程执行密切相关,线程执行的基本行为是函数调用,每次函数调用都是通过Java栈
传递的。
Java栈
与数据结构中的栈
类似,有FIFO
的特点,在Java
栈中保存的主要内容为栈帧,每次函数调用都会有一个对应的栈帧
入栈,每次调用结束就有一个对应的栈帧
出栈。栈顶总是当前的帧(当前执行的函数所对应的帧)。栈帧保存着局部变量表
、操作数栈
、帧数据
等。
这里说一下题外话,相信很多读者对StackOverflowError
不陌生,这是因为函数调用过多造成的,因为每次函数调用都会生成对应的栈帧,会占用一定的栈空间,如果栈空间不足,函数调用就无法进行,当请求栈深度大于最大可用栈深度时,就会抛出StackOverflowError
。
JVM
提供了-Xss
来指定线程的最大栈空间。
比如,下面这个递归调用的程序:
public class Main {
private static int count = 0;
public static void recursion(){
++count;
recursion();
}
public static void main(String[] args) {
try{
recursion();
}catch (StackOverflowError e){
System.out.println("Deep of calling = "+count);
}
}
}
指定-Xss1m
,结果:
指定-Xss2m
:
指定-Xss3m
:
可以看到调用深度随着-Xss
的增加而增加。
5.2 局部变量表
局部变量表是栈帧的重要组成部分之一,用于保存函数的参数及局部变量。局部变量表中的变量只在当前函数调用中有效,函数调用结束后,函数栈帧销毁,局部变量表也会随之销毁。
5.2.1 参数数量对局部变量表的影响
由于局部变量表在栈帧中,如果函数的参数和局部变量表较多,会使局部变量表膨胀,导致栈帧会占用更多的栈空间,最终减少了函数嵌套调用次数。
比如:
public class Main {
private static int count = 0;
public static void recursion(long a,long b,long c){
long e=1,f=2,g=3,h=4,i=5,k=6,q=7;
count++;
recursion(a,b,c);
}
public static void recursion(){
++count;
recursion();
}
public static void main(String[] args) {
try{
// recursion();
recursion(0L,1L,2L);
}catch (StackOverflowError e){
System.out.println("Deep of calling = "+count);
count = 0;
}
}
}
无参数的调用次数(-Xss1m
):
带参数的调用次数(-Xss1m
):
可以看到次数明显减少了,原因正是因为局部变量表变大,导致栈帧变大,从而次数减少。
下面使用jclasslib
进一步查看,先在IDEA
安装如下插件:
安装后使用插件查看情况:
第一个函数是带参数的,可以看到最大局部变量表的大小为20字
(注意不是字节),Long
在局部变量表中需要占用2字。而相比之下不带参数的函数最大局部变量表大小为0:
5.2.2 槽位复用
局部变量表中的槽位是可以复用的,如果一个局部变量超过了其作用域,则在其作用域之后的局部变量就有可能复用该变量的槽位,这样能够节省资源,比如:
public static void localVar1(){
int a = 0;
System.out.println(a);
int b = 0;
}
public static void localVar2(){
{
int a = 0;
System.out.println(a);
}
int b = 0;
}
同样使用jclasslib
查看:
可以看到少了localVar2
的最大局部变量大小为1字,相比localVar1
少了1字,继续分析,localVar1
第0个槽位为变量a,第1个槽位为变量b:
而localVar2
中的b复用了a的槽位,因此最大变量大小为1字,节约了空间。
5.2.3 对GC
的影响
下面再来看一下局部变量表对垃圾回收的影响,示例:
public class Main {
public static void localGC1(){
byte [] a = new byte[6*1024*1024];
System.gc();
}
public static void localGC2(){
byte [] a = new byte[6*1024*1024];
a = null;
System.gc();
}
public static void localGC3(){
{
byte [] a = new byte[6*1024*1024];
}
System.gc();
}
public static void localGC4(){
{
byte [] a = new byte[6*1024*1024];
}
int c = 10;
System.gc();
}
public static void localGC5(){
localGC1();
System.gc();
}
public static void main(String[] args) {
System.out.println("-------------localGC1------------");
localGC1();
System.out.println();
System.out.println("-------------localGC2------------");
localGC2();
System.out.println();
System.out.println("-------------localGC3------------");
localGC3();
System.out.println();
System.out.println("-------------localGC4------------");
localGC4();
System.out.println();
System.out.println("-------------localGC5------------");
localGC5();
System.out.println();
}
}
输出(请加上-Xlog:gc
参数):
[0.004s][info][gc] Using G1
-------------localGC1------------
[0.128s][info][gc] GC(0) Pause Full (System.gc()) 10M->8M(40M) 12.081ms
-------------localGC2------------
[0.128s][info][gc] GC(1) Pause Young (Concurrent Start) (G1 Humongous Allocation) 9M->8M(40M) 0.264ms
[0.128s][info][gc] GC(2) Concurrent Cycle
[0.133s][info][gc] GC(3) Pause Full (System.gc()) 16M->0M(14M) 2.799ms
[0.133s][info][gc] GC(2) Concurrent Cycle 4.701ms
-------------localGC3------------
[0.133s][info][gc] GC(4) Pause Young (Concurrent Start) (G1 Humongous Allocation) 0M->0M(14M) 0.203ms
[0.133s][info][gc] GC(5) Concurrent Cycle
[0.135s][info][gc] GC(5) Pause Remark 8M->8M(22M) 0.499ms
[0.138s][info][gc] GC(6) Pause Full (System.gc()) 8M->8M(22M) 2.510ms
[0.138s][info][gc] GC(5) Concurrent Cycle 4.823ms
-------------localGC4------------
[0.138s][info][gc] GC(7) Pause Young (Concurrent Start) (G1 Humongous Allocation) 8M->8M(22M) 0.202ms
[0.138s][info][gc] GC(8) Concurrent Cycle
[0.142s][info][gc] GC(9) Pause Full (System.gc()) 16M->0M(8M) 2.861ms
[0.142s][info][gc] GC(8) Concurrent Cycle 3.953ms
-------------localGC5------------
[0.143s][info][gc] GC(10) Pause Young (Concurrent Start) (G1 Humongous Allocation) 0M->0M(8M) 0.324ms
[0.143s][info][gc] GC(11) Concurrent Cycle
[0.145s][info][gc] GC(11) Pause Remark 8M->8M(16M) 0.316ms
[0.147s][info][gc] GC(12) Pause Full (System.gc()) 8M->8M(18M) 2.402ms
[0.149s][info][gc] GC(13) Pause Full (System.gc()) 8M->0M(8M) 2.462ms
[0.149s][info][gc] GC(11) Concurrent Cycle 6.843ms
首行输出表示使用G1
,下面逐个进行分析:
localGC1
:并没有回收内存,因为此时byte
数组被变量a
引用,因此无法回收localGC2
:回收了内存,因为a
被设置为了null
,byte
数组失去强引用localGC3
:没有回收内存,虽然此时a
变量已经失效,但是仍然存在于局部变量表中,并且指向byte
数组,因此无法回收localGC4
:回收了内存,因为声明了变量c
,复用了a
的槽位,导致byte
数组失去引用,顺利回收localGC5
:回收了内存,虽然localGC1
中没有释放内存,但是返回到localGC5
后,localGC1
的栈帧被销毁,也包括其中的byte
数组失去了引用,因此在localGC5
中被回收
5.3 操作数栈与帧数据区
操作数栈也是栈帧的重要内容之一,主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间,也是一个FIFO
的数据结构。
而帧数据区则保存着常量池指针,方便程序访问常量池,此外,帧数据区也保存着异常处理表,以便在出现异常后,找到处理异常的代码。
5.4 栈上分配
栈上分配是JVM
提供的一项优化技术,基本思想是,将线程私有的对象打散分配到栈上,好处是函数调用结束后可以自动销毁,而不需要垃圾回收器的介入,从而提高系统性能。
栈上分配的一个技术基础是逃逸分析,逃逸分析目的是判断对象的作用域是否会逃逸出函数体,例子如下:
public class Main {
private static int count = 0;
public static class User{
public int id = 0;
public String name = "";
}
public static void alloc(){
User user = new User();
user.id = 5;
user.name = "test";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
}
启动参数:
-server # 开启Server模式,此模式下才能开启逃逸分析
-Xmx10m # 最大堆内存
-Xms10m # 初始化堆内存
-XX:+DoEscapeAnalysis # 开启逃逸分析
-Xlog:gc # GC日志
-XX:-UseTLAB # 关闭TLAB
-XX:+EliminateAllocations # 开启标量替换,默认打开,允许将对象打散分配在栈上
输出如下,没有GC
日志:
而如果关闭了标量替换,也就是添加-XX:-EliminateAllocations
,就可以看到会频繁触发GC
,因为这时候对象存放在堆上而不是栈上,堆只有10m空间,会频繁进行GC
:
6 方法区
与Java堆
一样,方法区
是所有线程共享的内存区域,用于保存系统的类信息,比如类字段、方法、常量池等,方法区
的大小决定了系统可以保存多少个类,如果定义了过多的类,会导致方法区
溢出,会直接OOM
。
在JDK6/7
中方法区
可以理解成永久区
,JDK8
后,永久区
被移除,取而代之的是元数据区
,可以使用-XX:MaxMetaspaceSize
指定,这是一块堆外的直接内存,如果不指定大小,默认情况下JVM
会耗尽所有可用的系统内存。
如果元数据区
发生溢出,JVM
会抛出OOM
。
7 Java堆
、Java栈
以及方法区
的关系
看完了Java堆
、Java栈
以及方法区
,最后来一段代码来简单分析一下它们的关系:
class SimpleHeap{
private int id;
public SimpleHeap(int id){
this.id = id;
}
public void show(){
System.out.println("id is "+id);
}
public static void main(String[] args) {
SimpleHeap s1 = new SimpleHeap(1);
SimpleHeap s2 = new SimpleHeap(2);
s1.show();
s2.show();
}
}
main
中创建了两个局部变量s1
、s2
,则这两个局部变量存放在Java栈
中。同时这两个局部变量是SimpleHeap
的实例,这两个实例存放在Java堆
中,而其中的show
方法,则存放在方法区
中,图示如下:
8 小结
本文主要讲述了JVM
的基本结构以及一些基础参数,基本结构可以分成三部分:
- 第一部分:
类加载子系统
、Java堆
、方法区
、直接内存
- 第二部分:
Java栈
、本地方法栈
、PC寄存器
- 第三部分:执行引擎
而重点讲了三部分:
Java堆
:常见的结构为新生代
+老年代
结构,其中新生代可分为edsn
、s0
、s1
Java栈
:包括局部变量表、操作数栈与帧数据区,还提到了一个JVM
优化技术栈上分配,可以通过-XX:+EliminateAllocation
开启(默认开启)方法区
:所有线程共享区域,用于保存类信息,比如类字段、方法、常量等
JVM学习笔记(二):JVM基本结构的更多相关文章
- java之jvm学习笔记二(类装载器的体系结构)
java的class只在需要的时候才内转载入内存,并由java虚拟机的执行引擎来执行,而执行引擎从总的来说主要的执行方式分为四种, 第一种,一次性解释代码,也就是当字节码转载到内存后,每次需要都会重新 ...
- java之jvm学习笔记十三(jvm基本结构)
java之jvm学习笔记十三(jvm基本结构) 这一节,主要来学习jvm的基本结构,也就是概述.说是概述,内容很多,而且概念量也很大,不过关于概念方面,你不用担心,我完全有信心,让概念在你的脑子里变成 ...
- muduo学习笔记(二)Reactor关键结构
目录 muduo学习笔记(二)Reactor关键结构 Reactor简述 什么是Reactor Reactor模型的优缺点 poll简述 poll使用样例 muduo Reactor关键结构 Chan ...
- java jvm学习笔记二(类装载器的体系结构)
欢迎装载请说明出处:http://blog.csdn.net/yfqnihao 在了解java虚拟机的类装载器之前,有一个概念我们是必须先知道的,就是java的沙箱,什 ...
- JVM学习笔记:JVM的体系结构与JVM的生命周期
1 JVM在java平台中的位置 1.1 Java平台组成 Java平台主要由Java虚拟机和Java API这两部分组成.参考Oracle官网. 1.2 java平台结构图 JDK1.2开始,迫于J ...
- jvm学习笔记二(减少GC开销的建议)
一:触发主GC(Garbage Collector)的条件 JVM进行次GC的频率很高,但因为这种GC占用时间极短,所以对系统产生的影响不大.更值得关注的是主GC的触发条件,因为它对系统影响很明显.总 ...
- JVM学习笔记二:垃圾收集算法
垃圾回收要解决的问题: 哪些内存需要回收? 线程私有区域不需要回收,如PC.Stack.Native Stack:Java 堆和方法区需要 什么时候回收? 以后的文章解答 如何回收? 首先进行对象存活 ...
- JVM学习笔记(二):垃圾收集
程序计数器. 虚拟机栈. 本地方法栈3个区域随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操作. 每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这 ...
- JVM 学习笔记二 :JVM内存区域
一.内存分配概述
- 【JVM学习笔记二】垃圾收集器与内存分配策略
1. 概述 1) GC的历史比Java久远 2) GC需要完成的三件事: | 哪些内存需要回收 | 什么时候回收 | 如何回收 3) Java内存运行时区域各个部分: | Java虚拟机栈.计数器.本 ...
随机推荐
- glibc内存管理那些事儿
本文转载自glibc内存管理那些事儿 Linux内存空间简介 32位Linux平台下进程虚拟地址空间分布如下图: 进程虚拟地址空间分布 图中,0xC0000000开始的最高1G空间是内核地址空间,剩下 ...
- Nginx之Location匹配规则
概述 经过多年发展,nginx凭借其优异的性能征服了互联网界,成为了各个互联网公司架构设计中不可获取的要素.Nginx是一门大学问,但是对于Web开发者来说,最重要的是需要能捋的清楚Nginx的请求路 ...
- js---it笔记
typeof a返回的是字符串 vscode scss安装的easy scss中的配置settingjson文件中的css编译生成路径是根目录下的
- short i=1;i=i+1;为什么报错?
先测试,看结果: 提示我们说不能将short类型的转化为int类型! 先不急着下结论,我们继续测试,用i+=1; 我们发现并没有报错,为什么同样是加1,会出现这样两种不同的结果呢? 查阅了一些资料,大 ...
- 前端axios传递一个包含数组的对象到后台,后台可以用String接收,也可以用List集合接收
前端代码: data() { return { listQuery: { date: [], } }}, //查询列表信息getList() { if (this.listQuery.date == ...
- 图像分割 | Context Prior CPNet | CVPR2020
文章转自微信公众号:「机器学习炼丹术」 文章作者:炼丹兄(已授权) 作者联系方式:cyx645016617 论文名称:"Context Prior for Scene Segmentatio ...
- 第48天学习打卡(HTML 行内元素和块元素 列表 表格 视频和音频 页面结构分析 iframe内联框架 表单语法 )
行内元素和块元素 块元素 无论内容多少,该元素独占一行 (p.h1-h6) 行内元素 内容撑开宽度,左右都是行内元素的可以排在一行 (a.strong.em...) 列表 什么是列表 ...
- 通过webhost扩展方式初始化EFCore数据库
通过webhost扩展方式初始化EFCore数据库 EFCore数据库初始化 1.定义WebHostMigrationExtensions类 public static class WebHostM ...
- token、cookie和session区别以及django中的cookie,csrf
参考:https://my.oschina.net/xianggao/blog/395675?fromerr=GC9KVenE [前言]登录时需要post的表单信息. 先跳过具体案例,讲解基础知识: ...
- python进阶(12)闭包
闭包 首先了解一下:如果在一个函数的内部定义了另一个函数,外部的我们叫他外函数,内部的我们叫他内函数. 在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用 ...