JVM运行时内存结构Word下载.docx
《JVM运行时内存结构Word下载.docx》由会员分享,可在线阅读,更多相关《JVM运行时内存结构Word下载.docx(20页珍藏版)》请在冰豆网上搜索。
![JVM运行时内存结构Word下载.docx](https://file1.bdocx.com/fileroot1/2023-1/31/def7ca85-774b-45be-bac9-92816fc42951/def7ca85-774b-45be-bac9-92816fc429511.gif)
例如:
?
1
2
3
4
5
6
7
public
static
int
runClassMethod(int
i,long
l,float
f,double
d,Object
o,byte
b)
{
return
0;
}
runInstanceMethod(char
c,double
d,short
s,boolean
}
对应的局域变量区是:
runInstanceMethod的局部变量区第一项是个reference(引用),它指定的就是对象本身的引用,也就是我们常用的this,但是在runClassMethod方法中,没这个引用,那是因为runClassMethod是个静态方法。
(2)操作数栈
操作数栈和局部变量区一样,也被组织成一个以字长为单位的数组。
但和前者不同的是,它不是通过索引来访问的,而是通过入栈和出栈来访问的。
操作数栈是临时数据的存储区域。
a=
100;
b
=5;
c
=
a+b;
对应的操作数栈变化为:
从图中可以得出:
操作数栈其实就是个临时数据存储区域,它是通过入栈和出栈来进行操作的。
PS:
JVM实现里,有一种基于栈的指令集(Hotspot,oracleJVM),还有一种基于寄存器的指令集(DalvikVM,安卓JVM),两者有什么区别的呢?
基于栈的指令集有接入简单、硬件无关性、代码紧凑、栈上分配无需考虑物理的空间分配等优势,但是由于相同的操作需要更多的出入栈操作,因此消耗的内存更大。
而基于寄存器的指令集最大的好处就是指令少,速度快,但是操作相对繁琐。
示例:
8
9
10
11
12
13
14
package
com.demo3;
class
Test
{
void
foo()
a
1;
2;
(a
+
*
5;
main(String[]
args)
foo();
基于栈的Hotspot的执行过程如下:
基于寄存器的DalvikVM执行过程如下所示:
上述两种方式最终通过JVM执行引擎,CPU接收到的汇编指令是:
(3)帧数据区
帧数据区存放了指向常量池的指针地址,当某些指令需要获得常量池的数据时,通过帧数据区中的指针地址来访问常量池的数据。
此外,帧数据区还存放方法正常返回和异常终止需要的一些数据。
2.垃圾回收机制
1)、为什么要垃圾回收
JVM自动检测和释放不再使用的内存,提高内存利用率。
Java运行时JVM会执行GC,这样程序员不再需要显式释放对象。
2)、回收哪些内存区域
因为线程内存区随着线程的产生和退出而分配和回收,所以垃圾回收主要集中在共享内存区,也就是持久带(PermanentSpace)和堆(Heap)。
3)、如何判断对象已死(对象标记)
(1)引用计数法
引用计数法就是通过一个计数器记录该对象被引用的次数,方法简单高效,但是解决不了循环引用的问题。
比如对象A包含指向对象B的引用,对象B也包含指向对象A的引用,但没有引用指向A和B,这时当前回收如果采用的是引用计数法,那么对象A和B的被引用次数都为1,都不会被回收。
JVM不是采用这种方法。
(2)
根搜索(可达性分析算法)
根搜索(可达性分析算法)可以解决对象循环引用的问题,基本原理是:
通过一个叫“GCROOT”根对象作为起点,然后根据关联关系,向下节点搜索,搜索路径叫引用链,也就是常说的引用。
从“GCROOT”根对象找不到任何一条路径与之相连的对象,就被判定可以回收,相当于这对象找不到家的感觉。
示例图:
GC会收集那些不是GCroot且没有被GCroot引用的对象。
一个对象可以属于多个GCroot。
GCroot有几下种:
∙虚拟机栈(栈帧中的本地变量表)中引用的对象
∙方法区中类静态属性引用的对象
∙方法区中常量引用的对象
∙本地方法栈中JNI(native方法)引用的对象
∙用于JVM特殊目的对象,例如系统类加载器等等
虽然有可达性分析算法来判定对象状态,但这并不是对象是否被回收的条件,对象回收的条件远远比这个复杂。
无法通过GCROOT关联到的对象,不都是立刻被回收。
如果这个对象没有被关联到,而且没有被mark2标记,那么会进入一个死缓的阶段,被第一次标记(mark1),然后被放入一个F-Queue队列;
如果这个对象被mark2标记了,那么这个对象将会被回收。
F-Queue队列由一个优先级较低的Finalizer线程去执行,其中的mark1对象等待执行自己的finalize()方法(JVM并不保证等待finalize()方法运行结束,因为finalize()方法或者执行慢,或者死循环,会影响该队列其他元素执行)。
执行mark1对象的finalize()方法,就会进行第二次标记(mark2)。
以后的GC都会按这个逻辑执行“搜索,标记1,标记2”。
这一“标记”过程是后续垃圾回收算法的基础。
∙如果在finalize()方法体内,再次对本对象进行引用,那么对象就复活了。
∙finalize()方法只会被执行一次,所以对象只有一次复活的机会。
3)垃圾回收算法
垃圾回收算法主要有三种:
∙标记-清除
∙标记-复制
∙标记-整理
这三种都有“标记”过程,这个标记过程就是上述的根搜索(可达性分析算法)。
后面的“清除”、“复制”和“整理”动作,是具体的对象被回收的实现方式。
(1)标记-清除
通过根搜索(可达性分析算法)标记完成后,直接将标记为垃圾的对象所占内存空间释放。
这种算法的缺点是内存碎片多。
虽然缺点明显,这种策略却是后两种策略的基础。
正因为它的缺点,所以促成了后两种策略的产生。
(2)标记-复制
通过根搜索(可达性分析算法)标记完成后,将内存分为两块,将一块内存中保留的对象全部复制到另
一块空闲内存中。
这种算法的缺点是,可用内存变成了一半。
怎么解决这个缺点呢?
JVM将堆(heap)分成young区和old区。
young区包括eden、s0、s1,并且三个区之间的大小有一定比例。
例如,按8:
1:
1分成一块Eden和两小块Survivor区,每次GC时,young区里,将Eden和S0中存活的对象复制到另一块空闲的S1中。
young区的垃圾回收是经常要发生的,被称为MinorGC(次要回收)。
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发MinorGC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。
然后整理Survivor的两个区。
这种方式的GC是对Youngspace的Eden区进行,不会影响到Oldspace。
因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。
因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
MinorGC主要过程:
a、新生成的对象在Eden区完成内存分配;
b、当Eden区满了,再创建对象,会因为申请不到空间,触发minorGC,进行young(eden+1survivor)区的垃圾回收。
(为什么是eden+1survivor:
两个survivor中始终有一个survivor是空的,空的那个被标记成ToSurvivor);
c、minorGC时,Eden不能被回收的对象被放入到空的survivor(也就是放到ToSurvivor,同时Eden肯定会被清空),另一个survivor(FromSurvivor)里不能被GC回收的对象也会被放入这个survivor(ToSurvivor),始终保证一个survivor是空的。
(MinorGC完成之后,ToSurvivor和FromSurvivor的标记互换);
d、当做第3步的时候,如果发现存放对象的那个survivor满了,则这些对象被copy到old区,或者survivor区没有满,但是有些对象已经足够Old(通过XX:
MaxTenuringThreshold参数来设置),也被放入Old区。
(对象在Survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会晋升到老年代中)
(3)标记-整理
oldspace也可以标记-复制策略吗?
当然不行!
youngspace中的对象大部分都是生命周期较短的对象,每次GC后,所剩下的活对象数量不是很大。
而oldspace中的对象大部分都是生命周期特别长的对象,即使GC后,仍然会剩下大量的活对象。
如果仍然采用复制动作,回收效率会变得非常低。
根据oldspace的特点,可以采用整理动作。
整理时,先清除掉应该清除的对象,然后把存活对象“压缩”到堆的一端,按顺序排放。
Oldspace(+PermanentSpace)的垃圾回收是偶尔发生的,被称为FullGC(主要回收)。
FullGC因为需要对整个堆进行回收,包括Young、Old和Perm,所以比MinorGC要慢,因此应该尽可能减少FullGC的次数。
在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。
有如下原因可能导致FullGC:
∙年老代(Tenured)被写满
∙持久代(Perm)被写满
∙System.gc()被显示调用
∙上一次GC之后Heap的各域分配策略动态变化
4)、垃圾收集器
垃圾收集算法是内存回收的理论基础,而垃圾收集器就是内存回收的具体实现。
堆(Heap)分代被目前大部分JVM所采用。
它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
一般情况下将堆区划分为oldspace和Youngspace,oldspace的特点是每次垃圾收集时只有少量对象需要被回收,而Youngspace的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
目前大部分垃圾收集器对于Youngspace都采取“标记-复制”算法。
而由于Oldspace的特点是每次回收都只回收少量对象,一般使用的是“标记-整理”算法。
(1)YoungSpace上的GC实现:
Serial(串行):
Serial收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。
Serial收集器是针对新生代的收集器,采用的是“标记-复制”算法。
它的优点是实现简单高效,但是缺点是会给用户带来停顿。
这个收集器类型仅应用于单核CPU桌面电脑。
使用serial收集器会显着降低应用程序的性能。
ParNew(并行):
ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。
ParallelScavenge(并行):
ParallelScavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是“标记-复制”算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。
(2)OldSpace上的GC实现:
SerialOld(串行):
Serial收集器的OldSpace版本,采用的是“标记-整理”算法。
ParallelOld(并行):
ParallelOld是ParallelScavenge收集器的OldSpace版本(并行收集器),使用多线程和“标记-整理”算法。
CMS(并发):
CMS(CurrentMarkSweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是"
标记-清除"
算法。
(3).G1
G1(GarbageFirst)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。
还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。
3.JVM参数
1).堆
-Xmx:
最大堆内存,如:
-Xmx512m
-Xms:
初始时堆内存,如:
-Xms256m
-XX:
MaxNewSize:
最大年轻区内存
NewSize:
初始时年轻区内存.通常为Xmx的1/3或1/4。
新生代=Eden+2个Survivor空间。
实际可用空间为=Eden+1个Survivor,即90%
MaxPermSize:
最大持久带内存
PermSize:
初始时持久带内存
+PrintGCDetails。
打印GC信息
NewRatio
新生代与老年代的比例,如–XX:
NewRatio=2,则新生代占整个堆空间的1/3,老年代占2/3
SurvivorRatio
新生代中Eden与Survivor的比值。
默认值为8。
即Eden占新生代空间的8/10,另外两个Survivor各占1/10
2).栈
-xss:
设置每个线程的堆栈大小.
JDK1.5+每个线程堆栈大小为1M,一般来说如果栈不是很深的话,1M是绝对够用了的。
3).垃圾回收
4).JVM
client模式和server模式
Java_home/bin/java命令有一个-server和-client参数,该参数标识了JVM以server模式或client模式启动。
JVMServer模式与client模式启动,最主要的差别在于:
-Server模式启动时,速度较慢,但是一旦运行起来后,性能将会有很大的提升。
当虚拟机运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器,而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器.C2比C1编译器编译的相对彻底,,服务起来之后,性能更高。
(1)查看当前JVM默认启动模式
java-version可以直接查看出默认使用的是client还是server。
(2)JVM默认启动模式自动侦测
从JDK5开始,如果没有显式地用-client或者-server参数,那么JVM启动时,会根据机器配置和JDK的版本,自动判断该用哪种模式。
∙thedefinitionofaserver-classmachineisonewithatleast2CPUsandatleast2GBofphysicalmemory.
∙windows平台,64位版本的JDK,没有提供-client模式,直接使用server模式。
(3).通过配置文件,改变JVM启动模式
两种模式的切换可以通过更改配置(jvm.cfg配置文件)来实现:
32位的JVM配置文件在JAVA_HOME/jre/lib/i386/jvm.cfg,
64位的JVM配置文件在JAVA_HOME/jre/lib/amd64/jvm.cfg,目前64位只支持server模式。
32位版本的JDK5的jvm.cfg文件内容:
-client
KNOWN
-server
-hotspot
ALIASED_TO
-client
-classic
WARN
-native
ERROR
-green
64位版本的JDK7的jvm.cfg文件内容:
IGNORE
-server
4.堆VS栈
JVM栈是运行时的单位,而JVM堆是存储的单位。
JVM栈代表了处理逻辑,而JVM堆代表了数据。
JVM堆中存的是对象。
JVM栈中存的是基本数据类型和JVM堆中对象的引用。
JVM堆是所有线程共享,JVM栈是线程独有。
Java中的参数传递是传值呢?
还是传址?
我们都知道:
C语言中函数参数的传递有:
值传递,地址传递,引用传递这三种形式。
但是在Java里,方法的参数传递方式只有一种:
值传递。
所谓值传递,就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到任何影响。
要说明这个问题,先要明确两点:
1.引用在Java中是一种数据类型,跟基本类型int等等同一地位。
2.程序运行永远都是在JVM栈中进行的,因而参数传递时,只存在传递基本类型和对象引用的问题。
不会直接传对象本身。
在运行JVM栈中,基本类型和引用的处理是一样的,都是传值。
如果是传引用的方法调用,可以理解为“传引用值”的传值调用,即“引用值”被做了一个复制品,然后赋值给参数,引用的处理跟基本类型是完全一样的。
但是当进入被调用方法时,被传递的这个引用值,被程序解释(或者查找)到JVM堆中的对象,这个时候才对应到真正的对象。
如果此时进行修改,修改的是引用对应的对象,而不是引用本身,即:
修改的是JVM堆中的数据。
所以这个修改是可以保持的了。
15
16
17
18
19
20
21
22
23
DataWrap
a;
b;
ReferenceTransferTest
swap(DataWrap
dw)
tmp
dw.a;
dw.a
dw.b;
dw.b
tmp;
dw
new
DataWrap();
6;
9;
swap(dw);
对应的内存图:
附: