深入理解JVM内存结构-垃圾回收-类加载&字节码技术-内存模型
Posted 编程小工匠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解JVM内存结构-垃圾回收-类加载&字节码技术-内存模型相关的知识,希望对你有一定的参考价值。
一、什么是 JVM ?
JVM(Java Virtual Machine)其实就类似于一台小电脑运行在windows或者linux这些操作系统环境下。它直接和操作系统进行交互,与硬件不直接交互,然后操作系统可以帮我们完成和硬件进行交互的工作。
JVM是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。Java语言最重要的特点就是跨平台运行。使用JVM就是为了支持与操作系统无关,实现跨平台。所以,JAVA虚拟机JVM是属于JRE的,而现在我们安装JDK时也附带安装了JRE(当然也可以单独安装JRE)。
JVM的用处比如:自动装箱、自动拆箱是怎么实现的,反射是怎么实现的,垃圾回收机制是怎么回事…
一次编译,处处执行 ,自动的内存管理,垃圾回收机制 , 数组下标越界检查…
Java程序具有跨平台特性主要是指字节码文件可以在任何具有Java虚拟机的计算机或者电子设备上运行,Java虚拟机中的Java解释器负责将字节码文件解释成为特定的机器码进行运行。因此在运行时,Java源程序需要通过编译器编译成为.class文件。众所周知java.exe是java class文件的执行程序,但实际上java.exe程序只是一个执行的外壳,它会装载jvm.dll(windows下,下皆以windows平台为例,linux下和solaris下其实类似,为:libjvm.so),这个动态连接库才是java虚拟机的实际操作处理所在。
下面我们一起走入JVM的世界!
Chapter 2. The Structure of the Java Virtual Machine (oracle.com)
1、HotSpot介绍
HotSpot Virtual Machine Garbage Collection Tuning Guide
Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide,(oracle.com)、
**HotSpot 的正式发布名称为" Java HotSpot Performance Engine ",**是 Java虚拟机 的一个实现,包含了服务器版和桌面应用程序版,现时由 Oracle 维护并发布。它利用 JIT 及自适应优化技术(自动查找性能热点并进行动态优化,这也是HotSpot名字的由来)来提高性能。
HotSpot VM,它是Sun JDK和OpenJDK中所带的虚拟机,也是目前使用范围最广的Java虚拟机。 其最初并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的; 甚至这个虚拟机最初并非是为Java语言而开发的,它来源于Strongtalk VM, 而这款虚拟机中相当多的技术又是来源于一款支持Self语言实现“达到C语言50%以上的执行效率”的目标而设计的虚拟机, Sun公司注意到了这款虚拟机在JIT编译上有许多优秀的理念和实际效果,在1997年收购了Longview Technologies公司,从而获得了HotSpot VM。HotSpot VM既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势, 如它名称中的HotSpot指的就是它的热点代码探测技术(其实两个VM基本上是同时期的独立产品,HotSpot还稍早一些,HotSpot一开始就是准确式GC, 而Exact VM之中也有与HotSpot几乎一样的热点探测。 为了Exact VM和HotSpot VM哪个成为Sun主要支持的VM产品,在Sun公司内部还有过争论,HotSpot打败Exact并不能算技术上的胜利)。
HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小,这样有助于引入更多的代码优化技术,输出质量更高的本地代码。
在2008年和2009年,Oracle公司分别收购了BEA公司和Sun公司,这样Oracle就同时拥有了两款优秀的Java虚拟机:JRockit VM和HotSpot VM。 Oracle公司宣布(大约应在发布JDK 8的时候)会完成这两款虚拟机的整合工作,使之优势互补。 整合的方式大致上是在HotSpot的基础上,移植JRockit的优秀特性,譬如使用JRockit的垃圾回收器与MissionControl服务, 使用HotSpot的JIT编译器与混合的运行时系统。
2、HosSpot中的概念
2.1解释执行与 JIT
**解释器:**Java 程序在运行的时候,主要就是执行字节码指令,一般这些指令会按照顺序解释执行,这种就是解释执行,解释执行的方式是非常低效的,它需要把字节码先翻译成机器码,才能往下执行。
编译器:字节码是 Java 编译器做的一次初级优化,许多代码可以满足语法分析,其实还有很大的优化空间。
所以,为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。完成这个任务的编译器,就称为即时编译器(Just In Time Compiler),简称 JIT 编译器。
-
**动态编译(dynamic compilation)**指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫*静态编译(static compilation)。
-
JIT编译(just-in-time compilation)**狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。
-
**自适应动态编译(adaptive dynamic compilation)**也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。
2.2热点代码
热点代码,就是那些被频繁调用的代码,比如调用次数很高或者在 for 循环里的那些代码。这些再次编译后的机器码会被缓存起来,以备下次使用,但对于那些执行次数很少的代码来说,JIT这种编译动作就纯属浪费。
JVM 提供了一个参数“-XX:ReservedCodeCacheSize”,用来限制 CodeCache 的大小。也就是说,JIT 编译后的代码都会放在 CodeCache 里。
如果这个空间不足,JIT 就无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,从而造成了 CPU占用上升。
2.3热点探测
**在 HotSpot 虚拟机中的热点探测是 JIT 优化的条件,热点探测是基于计数器的热点探测,采用这种方法的虚拟机会为每个方法建立计数器统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法” 。**虚拟机为每个方法准备了两类计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter)。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,会触发 JIT 编译。
-
方法调用计数器
用于统计方法被调用的次数,方法调用计数器的默认阈值在 C1 模式下是 1500 次,在 C2 模式下是 10000 次,可通过 -XX: CompileThreshold 来设定;
而在分层编译的情况下,-XX: CompileThreshold 指定的阈值将失效,此时将会根据当前待编译的方法数以及编译线程数来动态调整。当方法计数器和回边计数器之和超过方法计数器阈值时,就会触发 JIT 编译器。 -
回边计数器
用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge),**该值用于计算是否触发 C1 编译的阈值,在不开启分层编译的情况下,C1 默认为 13995,**C2 默认为 10700,可通过 -XX: OnStackReplacePercentage=N 来设置;而在分层编译的情况下,
-XX:OnStackReplacePercentage 指定的阈值同样会失效,此时将根据当前待编译的方法数以及编译线程数来动态调整。**建立回边计数器的主要目的是为了触发 OSR(On StackReplacement)编译,即栈上编译。**在一些循环周期比较长的代码段中,当循环达到回边计数器阈值时,JVM 会认为这段是热点代码,JIT 编译器就会将这段代码编译成机器语言并缓存,在该循环时间段内,会直接将执行代码替换,执行缓存的机器语言。
3、JVM、JRE、JDK 的关系
4、常见的 JVM
我们主要用的是 HotSpot 版本的虚拟机。
5、JAVA运行时环境逻辑图
6、JVM运行原理
- **ClassLoader:**Java 代码编译成二进制后,会经过类加载器,这样才能加载到 JVM 中运行。
- **Method Area:**类是放在方法区中。
- **Heap:**类的实例对象。
- 当类调用方法时,会用到 JVM Stack、PC Register、本地方法栈。
- 方法执行时的每行代码是有执行引擎中的解释器逐行执行,
- 方法中的热点代码频繁调用的方法,由 JIT 编译器优化后执行,
- GC 会对堆中不用的对象进行回收。
- 需要和操作系统打交道就需要使用到本地方法接口。
7、关于JVM的几个问题
7.1几个数据结构的概念
内存空间大致可以用下图表示:
函数在调用的时候都是在栈空间上开辟一段空间以供函数使用,所以下面来详细谈一谈函数的栈帧结构。如图示,栈是由高地址向地地址的方向生长的,而且栈有其栈顶和栈底,在x86系统的CPU中,寄存器ebp保存的是栈底地址,称为帧指针,寄存器esp保存的是栈顶地址,称为栈指针。而且还应该明确一点,栈指针和帧指针一次只能存储一个地址,所以,任何时候,这一对指针指向的是同一个函数的栈帧结构。并且ebp一般由系统改变它的值,而esp会随着数据的入栈和出栈而移动,也就是说esp始终指向栈顶。
【1】堆
堆: 堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。
- **堆的这一特性称之为堆序性。**因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆),如果根节点最大,称之为大顶堆(或大根堆)。堆的左右孩子没有大小的顺序。
- 堆的存储一般都用数组来存储堆,第0个结点左右子结点下标分别为1和2。
【2】栈
栈: 栈是一种运算受限的线性表,FILO先进后出的数据结构。
-
其限制是指只仅允许在表的一端进行插入和删除操作,这一端被称为栈顶(Top),相对地,把另一端称为栈底(Bottom)。把新元素放到栈顶元素的上面,使之成为新的栈顶元素称作进栈、入栈或压栈(Push);把栈顶元素删除,使其相邻的元素成为新的栈顶元素称作出栈或退栈(Pop)。这种受限的运算使栈拥有“先进后出”的特性(First In Last Out),简称FILO。
-
栈分顺序栈和链式栈两种。栈是一种线性结构,**所以可以使用数组或链表(单向链表、双向链表或循环链表)作为底层数据结构。使用数组实现的栈叫做顺序栈,使用链表实现的栈叫做链式栈,**二者的区别是顺序栈中的元素地址连续,链式栈中的元素地址不连续。
【3】栈帧
**栈帧: 栈帧是指为一个函数调用单独分配的那部分栈空间。**也叫过程[活动记录],是编译器用来实现过程[函数调用]的一种[数据结构]。
- 运行的程序从当前函数调用另外一个函数时,就会为下一个函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前帧。而原来的函数也有一个对应的栈帧,被称为调用帧。每一个栈帧里面都会存入当前函数的局部变量
- 当函数被调用时,就会被加入到调用栈顶部,执行结束之后,就会从调用栈顶部移除该函数。并将程序运行权利(帧指针)交给此时栈顶的栈帧。这种后进先出的结构也就是函数的调用栈。
栈帧的两个边界分别有FP(R11)和SP(R13)L来限定
1.栈帧:虚拟机用来进行方法调用和方法执行的数据结构
2.栈帧的组成 = 局部变量表 + 操作数栈 + 动态链接 + 方法返回地址 + 附加信息
3.局部变量表
(1)存放的内容 = 方法参数列表对应的值 + 方法内定义的局部变量的值
(2)局部变量表 = 变量槽 * n(即多个变量槽构成)
1)一个变量槽存放一个32位以内的数据类型:
char,int ,bit,boolean,float,short,reference,returnAddress
2)64位的数据结构就需要2个变量槽:long,double
3)变量槽的访问是根据索引定位来完成的
(3)局部变量表和类变量不同,类变量有一个初始化赋值的过程,局部变量表中的值如果不赋值,那就真的是没值
4.操作数栈
(1)数据结构 = 先入后出的栈结构
(2)操作数栈在编译的过程中最大深度就已经确定好了
(3)操作数栈中的数据类型必须严格遵照字节码指令规定的类型
(4)从概念模型上来看,每一个栈帧是独立的。但是实际上上一个栈帧的局部变量表会和下一个栈帧的操作数栈有一部分重合
(5).java虚拟机的解释执行引擎 = 基于栈的执行引擎
5.动态链接
每一个栈帧都包含一个指向运行时常量池的该栈帧多对应的方法,用于动态链接
6.方法返回地址
(1)方法返回的两种方式 = 执行引擎遇到方法返回的指令 + 遇到错误
(2)不管哪种方法返回,程序都会回到上一层继续执行,那么栈帧中需要保存一些方法返回的信息。最常见的信息就是保存上一层的计数器,好让程序能准确定位到上一层。
7.附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接,方法返回地址与其它附加信息全部归为一类,称为栈帧信息。
7.2为什么HotSpot虚拟机要使用解释器与编译器并存的架构?
尽管并不是所有的Java虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机(如HotSpot),都同时包含解释器和编译器。解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。
7.3为何HotSpot虚拟机要实现两个不同的即时编译器?
HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编译器,分别用在客户端和服务端。目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。程序使用哪个编译器,取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用“-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式。
用Client Complier获取更高的编译速度,用Server Complier 来获取更好的编译质量。为什么提供多个即时编译器与为什么提供多个垃圾收集器类似,都是为了适应不同的应用场景。
3)哪些程序代码会被编译为本地代码?如何编译为本地代码?
程序中的代码只有是热点代码时,才会编译为本地代码,那么什么是热点代码呢?
运行过程中会被即时编译器编译的“热点代码”有两类:
1、被多次调用的方法。 2、被多次执行的循环体。
两种情况,编译器都是以整个方法作为编译对象。 这种编译方法因为编译发生在方法执行过程之中,因此形象的称之为栈上替换(On Stack Replacement,OSR),即方法栈帧还在栈上,方法就被替换了。
7.4如何判断方法或一段代码或是不是热点代码呢?
要知道方法或一段代码是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测)。
目前主要的热点探测方式有以下两种:
(1)基于采样的热点探测
采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这个方法就是“热点方法”。这种探测方法的好处是实现简单高效,还可以很容易地获取方法调用关系(将调用堆栈展开即可),缺点是很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
(2)基于计数器的热点探测
采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。
7.5HotSpot虚拟机中使用的是哪种热点检测方式呢?
在HotSpot虚拟机中使用的是第二种——**基于计数器的热点探测方法,因此它为每个方法准备了两个计数器:方法调用计数器和回边计数器。**在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。
二、JVM 的内存结构
1、PC Register程序计数器
1)定义
Program Counter Register **程序计数器(寄存器)**作用:是记录下一条 jvm 指令的执行地址行号。
特点:
- 是线程私有的(每个线程都有自动的程序计数器)
- 不会存在内存溢出问题
2)作用
程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
0: getstatic #20 // PrintStream out = System.out;
3: astore_1 // --
4: aload_1 // out.println(1);
5: iconst_1 // --
6: invokevirtual #26 // --
9: aload_1 // out.println(2);
10: iconst_2 // --
11: invokevirtual #26 // --
14: aload_1 // out.println(3);
15: iconst_3 // --
16: invokevirtual #26 // --
19: aload_1 // out.println(4);
20: iconst_4 // --
21: invokevirtual #26 // --
24: aload_1 // out.println(5);
25: iconst_5 // --
26: invokevirtual #26 // --
29: return
**解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,**这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
**多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,**以便于接着往下执行。
2、JVM Stacks虚拟机栈
1)定义
每个线程运行需要的内存空间,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的方法
package cn.itcast.jvm.t1._01stack;
/**
* 演示栈帧
*/
public class Demo1_1 {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
问题辨析:
垃圾回收是否涉及栈内存?
- 不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。垃圾回收的是堆内存中的无用对象
栈内存分配越大越好吗?
- 不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
- if 物理内存=500M 一个线程1M 就可以500个线程 一个2M 250个线程(一般采用系统默认的栈内存大小)
方法呢的局部变量是否线程安全(私有的就不需要考虑,static修饰的公共资源要考虑)
- 如果方法内部的变量(基本变量/引用变量)没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
/**
* 局部变量的线程安全问题
*/
public class Demo1_2 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
// 方法内部的变量(基本变量/引用变量)没有逃离方法的作用访问,它是线程安全的
public static void m1() { // 不存在线程安全问题
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
// 局部变量引用了对象,并逃离了方法的访问,那就要考虑线程安全问题。
public static void m2(StringBuilder sb) { // 不存在线程安全问题 可以使用StringBuffer
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() { // 不存在线程安全问题 可以使用StringBuffer
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}
2)栈内存溢出
**栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError **
【1】-Xss256k
默认栈帧在1M左右使用 -Xss256k 指定栈内存大小!
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo1_3 {
private static int count; // 计数 打印调用栈帧的次数
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
// 递归调用
private static void method1() {
count++;
method1();
}
}
调用38313次栈溢出
设置栈帧大小
再次执行
【2】第三方类库操作
Emp和Dept类相互调用
/**
* json 数据转换
*/
public class Demo1_19 {
public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");
Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);
Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);
d.setEmps(Arrays.asList(e1, e2));
// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}
class Emp {
private String name;
@JsonIgnore // 作用:遇到部门属性就不转换json 变成单向关联
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dept getDept() {
return dept;
}
public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Emp> getEmps() {
return emps;
}
public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}
3)线程运行诊断
【1】案例一:cpu 占用过多
/**
* 演示 cpu 占用过高
*/
public class Demo1_16 {
public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("1...");
while(true) {
}
}, "thread1").start();
new Thread(null, () -> {
System.out.println("2...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();
new Thread(null, () -> {
System.out.println("3...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}
解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程
nohup java cn.itcast.jvm.t1.Demo1_16 >/dev/null & 运行
top 命令,查看是哪个进程占用 CPU 过高
ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
例如:ps H -eo pid,tid,%cpu | grep 32665
jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,
# 注意 jstack 查找出的线程 id 是 16 进制的,需要转换。
# 会详细定位到出现问题的源码行数
【2】案例二:死锁迟迟得不到结果
通过jstack排查出思索的问题
/**
* 演示线程死锁
*/
class A{};
class B{};
public class Demo1_3 {
static A a = new A();
static B b = new B();
public static void main(String[] args) throws InterruptedException {
new Thread(以上是关于深入理解JVM内存结构-垃圾回收-类加载&字节码技术-内存模型的主要内容,如果未能解决你的问题,请参考以下文章