JVM 体系结构
一.Java虚拟机结构:
一.JVM 体系结构:
JVM内存结构
上图展示了JVM 体系结构!下面我们分别说下他们的作用:
方法区
一个JVM只有一个方法区,是所有线程共享的
存放Class的线性二进制流
类信息,该类型的常量池,字段信息,方法的字节码,操作数栈和该方法的栈帧中的局部变量区的大小,异常表,到类ClassLoader的引用,到Class类的应用
方法区大小不固定,可以动态调整
方法区也可以被GC
堆
一个JVM只有一个堆,所有线程共享
存放所有类实例和数组
PC寄存器
JVM会为每一个创建的线程分配一个PC寄存器
大小为一个字节
内容是下一条将执行指令的地址
java方法栈
线程启动时,JVM会为其分配一个Java栈
JVM对java方法栈只有“压栈”,“出栈”的操作,操作的单位是栈帧
栈帧由三部分组成“局部变量区”,“操作数栈“,“栈帧数据区”
二.JVM栈帧结构:
public class Test2 { void spin() { int a = 1; int b = 2; int c = (a + b) * 5; } }
编译后代码如下:
PC3GD000516:~/develop/jdk1.7.0_07/bin$ javap -c Test2.class Compiled from "Test2.java" public class Test2 { public Test2(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return void spin(); Code: 0: iconst_1 1: istore_1 2: iconst_2 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: iconst_5 8: imul 9: istore_3 10: return }
(图中数字均以十六进制表示。其中字节码的一列表示的是字节码指令的实际数值,后面跟着的助记符则是其对应的文字形式。标记为红色的值是相对上一条指令的执行状态有所更新的值。下同)
说明:Java字节码以1字节为单元。上面代码中有11条指令,每条都只占1单元,共11单元==11字节。
程序计数器是用于记录程序当前执行的位置用的。对Java程序来说,每个线程都有自己的PC。PC以字节为单位记录当前运行位置里方法开头的偏移量。
每个线程都有一个Java栈,用于记录Java方法调用的“活动记录”(activation record)。Java栈以帧(frame)为单位线程的运行状态,每调用一个方法就会分配一个新的栈帧压入Java栈上,每从一个方法返回则弹出并撤销相应的栈帧。
每 个栈帧包括局部变量区、求值栈(JVM规范中将其称为“操作数栈”)和其它一些信息。局部变量区用于存储方法的参数与局部变量,其中参数按源码中从左到右 顺序保存在局部变量区开头的几个slot。求值栈用于保存求值的中间结果和调用别的方法的参数等。两者都以字长(32位的字)为单位,每个slot可以保 存byte、short、char、int、float、reference和returnAddress等长度小于或等于32位的类型的数据;相邻两项 可用于保存long和double类型的数据。每个方法所需要的局部变量区与求值栈大小都能够在编译时确定,并且记录在.class文件里。
在上 面的例子中,Demo.foo()方法所需要的局部变量区大小为3个slot,需要的求值栈大小为2个slot。Java源码的a、b、c分别被分配到局 部变量区的slot 0、slot 1和slot 2。可以观察到Java字节码是如何指示JVM将数据压入或弹出栈,以及数据是如何在栈与局部变量区之 前流动的;可以看到数据移动的次数特别多。动画里可能不太明显,iadd和imul指令都是要从求值栈弹出两个值运算,再把结果压回到栈上的;光这样一条 指令就有3次概念上的数据移动了。
对了,想提醒一下:Java的局部变量区并不需要把某个局部变量固定分配在某个slot里;不仅如此,在一个方 法内某个slot甚至可能保存不同类型的数据。如何分配slot是编译器的自由。从类型安全的角度看,只要对某个slot的一次load的类型与最近一次 对它的store的类型匹配,JVM的字节码校验器就不会抱怨。以后再找时间写写这方面。
二 JVM内存结构
JVM堆结构
堆分为:新生代:Eden,S0,S1;年老代,其中s0,s1是完全对等的,在GC时数据互相拷贝。
例如:下面的列表中各代的比例。
S0 S1 E O P YGC YGCT FGC FGCT GCT
0.00 0.00 5.83 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.47 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.47 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.48 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.48 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.50 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.50 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.51 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.51 30.71 34.49 36 4.177 223 308.158 312.335
0.00 0.00 7.51 30.71 34.49 36 4.177 223 308.158 312.335
三 内存分配参数:
内存分配参数表
注意:图上的参数是正确的,但是图中说PermSize最大为64M是不对的。
堆分配
四,JVM 内存参数分析实例
环境:OS:Linux version 2.6.9-79.custome.ELxenU cpu: 4 * Intel(R) Xeon(R) CPU E5410 @ 2.33GHz (双核) memory:4G
1 | -server -Xmx2g -Xms2g -Xmn256m -XX:PermSize=192m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 |
-Xmx2g 最大堆内存2G
-Xms2g 最小内存2G
-Xmn256m 新生代内存256m 整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般是固定大小的(例如64m、96m),所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。
-XX:PermSize=192m 持久代 192m
-Xss256k 指定线程桟大小256K
-XX:LargePageSizeInBytes=128m 指定Java heap的分页页面大小为128M
-server 可以使得新生代采用并行GC,年老代采用串行
-XX:+DisableExplicitGC
-XX:+UseConcMarkSweepGC 指定在Old Generation使用concurrent gc ,启用CMS低停顿垃圾收集器。GC线程和应用线程并行
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
五. 内存回收
GC:垃圾回收。回收的是堆和方法区的内存。
基本原理:找到不被使用的对象,然后回收内存。使用收集器的方式实现GC。
A)怎么找到?从根集合出发,找出无引用的对象。
根集合对象: 当前运行线程栈上引用的对象,常量及静态变量,传到本地方法且没有被本地方法释放的对象引用。
B)收集器
按回收算法为两种: 引用计数收集器,跟踪收集器。
引用计数采用算法:原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数。垃圾回收时,只用收集计数为0的对象。
跟踪收集器采用算法:复制,标记-清除,标记-压缩。
按分区对待的方式分: 增量收集器(jdk5开始废弃),分代收集器。
增量收集器:就是通过一定的回收算法,把一个长时间的中断,划分为很多个小的中断,通过这种方式减少GC对用户程序的影响。
分代收集:对象存活的时间有长短,基于此将堆分为多个代,不同的代采用不同的GC方式。
按吞吐量和响应时间(暂停时间)分为: 串行收集器,并行收集器,并发收集器。
C)评估垃圾回收策略的两个重要度量
吞吐量:JVM花费在垃圾回收上的时间越长,则吞吐量越低
暂停时间:JVM垃圾回收过程当中有一个暂停期,在暂停期间,应用程序不能运行
串行收集器:单线程(单CPU)进行垃圾回收的工作。
–适用情况:数据量比较小;单处理器下并且对响应时间无要求的应用。
–缺点:只能用于小型应用
并行收集器:多个线程同时进行垃圾回收的工作。
–适用情况:”对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:科学计算。
–缺点:应用响应时间可能较长
并发收集器:传说中的CMS。垃圾回收器的一些工作与应用程序同时进行。
–适用情况:”对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器。
D)GC类型
GC有两种类型:Minor GC(Scavenge GC)和Full GC。
Minor GC:对新生代内存进行GC。
Full GC:对新生代,旧生代,持久代都进行GC。
Full GC可能的原因:
a)老年代或持久代空间满。
b)老年代采用CMS GC,GC日志出现prmotion failed和concurrent mode failure时可能触发。
prmotion failed:Minor GC是,S0(S1)放不下,放入旧生代时,仍然放不下造成的。
concurrent mode failure:CMS GC的过程中,有对象放入旧生代,此时旧生代空间不够。
c)统计得到Minor GC后存活对象放入旧生代的平均大小大于旧生代剩余空间。
d)System.gc(),只是”建议”JVM回收内存,不是强制。
六. 为何内存溢出:
既然都有GC,为什么还有内存被用尽(当然除了突然申请大空间)。这里更想说的是新生代和老年代被耗尽。
这是因为jvm有四种引用类型,不同的引用,GC的条件是不一样的。
A)四种引用
软引用:SoftReference,弱引用:WeakReference ,虚引用:PhantomReference。
软引用:内存不足,或软引用不经常使用时会被回收。适用于做缓存。
弱引用:使用弱引用创建的对象本身没有强引用,GC时一定会被回收。
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
除此之外都是强引用,我们一般创建一个对象时的引用就是强引用。对象被强引用,是不会不垃圾回收的。
B)内存溢出(泄露)
两种理解,
一是需要使用的对象在不断增加,直到需要分配的jvm内存超出了无法满足,于是产生溢出。
二是无用的对象在不断增加,但又无法回收,于是产生泄露。
泄露的对象有两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。这些对象不会被GC所回收,然而它却占用内存。