学习java以来,jvm的原理已经看过好多遍了,可是很多知识点都串不起来。
今天我把jvm相关知识整理了一下,看完之后肯定会对JVM很的清楚。
JVM是虚拟机,也是一种规范,他遵循着冯·诺依曼体系结构的设计原理。冯·诺依曼体系结构中,指出计算机处理的数据和指令都是二进制数。採用存储程序方式不加区分的存储在同一个存储器里,而且顺序执行,指令由操作码和地址码组成,操作码决定了操作类型和所操作的数的数字类型。地址码则指出地址码和操作数。从dos到window8。从unix到ubuntu和CentOS。还有MAC OS等等。不同的操作系统指令集以及数据结构都有着差异,而JVM通过在操作系统上建立虚拟机。自定义出来的一套统一的数据结构和操作指令,把同一套语言翻译给各大主流的操作系统。实现了跨平台执行,能够说JVM是java的核心,是java能够一次编译到处执行的本质所在。
我研究学习了JVM的组成和执行原理,JVM的统一数据格式规范、字节码文件结构,JVM关于内存的管理。
一、JVM的组成和执行原理 。
JVM的毕竟是个虚拟机,是一种规范,虽说符合冯诺依曼的计算机设计理念,可是他并非实体计算机,所以他的组成也不是什么存储器,控制器,运算器,输入输出设备。
在我看来,JVM执行在真实的操作系统中表现的更像应用或者说是进程,他的组成能够理解为JVM这个进程有哪些功能模块。而这些功能模块的运作能够看做是JVM的执行原理。JVM有多种实现,比如Oracle的JVM。HP的JVM和IBM的JVM等。而在本文中研究学习的则是使用最广泛的Oracle的HotSpot JVM。
1.JVM在JDK中的位置。
JDK是java开发的必备工具箱,JDK当中有一部分是JRE,JRE是JAVA执行环境。JVM则是JRE最核心的部分。我从oracle.com截取了一张关于JDK Standard Edtion的组成图,
从最底层的位置能够看出来JVM有多重要。而实际项目中JAVA应用的性能优化,OOM等异常的处理终于都得从JVM这儿来解决。
HotSpot是Oracle关于JVM的商标。差别于IBM。HP等厂商开发的JVM。Java HotSpot Client VM和Java HotSpot Server VM是JDK关于JVM的两种不同的实现,前者能够降低启动时间和内存占用,而后者则提供更加优秀的程序执行速度(參考自:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/index.html ,该文档有关于各个版本号的JVM的介绍)。
在命令行。通过java -version能够查看关于当前机器JVM的信息,以下是我在Win8系统上运行命令的截图。
能够看出我装的是build 20.13-b02版本号,HotSpot 类型Server模式的JVM。
2.JVM的组成
JVM由4大部分组成:ClassLoader,Runtime Data Area,Execution Engine,Native Interface。
我从CSDN找了一张描写叙述JVM大致结构的图:
2.1. ClassLoader 是负责载入class文件,class文件在文件开头有特定的文件标示。而且ClassLoader仅仅负责class文件的载入。至于它能否够执行,则由Execution Engine决定。
2.2.Native Interface 是负责调用本地接口的。
他的作用是调用不同语言的接口给JAVA用。他会在Native Method Stack中记录相应的本地方法,然后调用该方法时就通过Execution Engine载入相应的本地lib。原本多于用一些专业领域,如JAVA驱动。地图制作引擎等。如今关于这样的本地方法接口的调用已经被类似于Socket通信,WebService等方式代替。
2.3.Execution Engine 是运行引擎,也叫Interpreter。Class文件被载入后,会把指令和数据信息放入内存中,Execution Engine则负责把这些命令解释给操作系统。
2.4.Runtime Data Area 则是存放数据的,分为五部分:Stack,Heap。Method Area。PC Register,Native Method Stack。差点儿全部的关于java内存方面的问题。都是集中在这块。下图是javapapers.com上关于Run-time Data Areas的描写叙述:
能够看出它把Method Area化为了Heap的一部分。javapapers.com中觉得Method Area是Heap的逻辑区域,但这取决于JVM的实现者。而HotSpot JVM中把Method Area划分为非堆内存,显然是不包括在Heap中的。
下图是javacodegeeks.com中,2014年9月刊出的一片博文中关于Runtime Data Area的划分,当中指出,NonHeap包括PermGen和Code
Cache,PermGen包括Method Area,并且PermGen在JAVA SE
8中已经不再用了。查阅资料(https://abhirockzz.wordpress.com/2014/03/18/java-se-8-is-knocking-are-you-there/)得知。java8中PermGen已经从JVM中移除并被MetaSpace代替,java8中也不会见到OOM:PermGen
Space的异常。眼下Runtime Data Area能够用下图描写叙述它的组成:
2.4.1. Stack 是java栈内存,它等价于C语言中的栈。栈的内存地址是不连续的,每一个线程都拥有自己的栈。 栈里面存储着的是StackFrame,在《JVM Specification》中文版中被译作java虚拟机框架,也叫做栈帧。StackFrame包括三类信息:局部变量。运行环境,操作数栈。局部变量用来存储一个类的方法中所用到的局部变量。运行环境用于保存解析器对于java字节码进行解释过程中须要的信息,包括:上次调用的方法、局部变量指针和操作数栈的栈顶和栈底指针。操作数栈用于存储运算所须要的操作数和结果。
StackFrame在方法被调用时创建,在某个线程中,某个时间点上,仅仅有一个框架是活跃的,该框架被称为Current Frame,而框架中的方法被称为Current Method,当中定义的类为Current Class。局部变量和操作数栈上的操作总是引用当前框架。当Stack Frame中方法被运行完之后,或者调用别的StackFrame中的方法时,则当前栈变为另外一个StackFrame。
Stack的大小是由两种类型,固定和动态的,动态类型的栈能够依照线程的须要分配。
以下两张图是关于栈之间关系以及栈和非堆内存的关系基本描写叙述(来自 http://www.programering.com/a/MzM3QzNwATA.html ):
2.4.2. Heap 是用来存放对象信息的,和Stack不同。Stack代表着一种执行时的状态。换句话说,栈是执行时单位,解决程序该怎样执行的问题,而堆是存储的单位。解决数据存储的问题。Heap是伴随着JVM的启动而创建。负责存储全部对象实例和数组的。堆的存储空间和栈一样是不须要连续的,它分为Young Generation和Old Generation(也叫Tenured Generation)两大部分。
Young Generation分为Eden和Survivor,Survivor又分为From Space和 ToSpace。
和Heap常常一起提及的概念是PermanentSpace,它是用来载入类对象的专门的内存区,是非堆内存,和Heap一起组成JAVA内存,它包括MethodArea区(在没有Code Cache的HotSpotJVM实现里,则MethodArea就相当于GenerationSpace)。
在JVM初始化的时候,我们能够通过參数来分别指定,PermanentSpace的大小、堆的大小、以及Young Generation和Old Generation的比值、Eden区和From Space的比值。从而来细粒度的适应不同JAVA应用的内存需求。
2.4.3. PC Register 是程序计数寄存器,每一个JAVA线程都有一个单独的PC Register。他是一个指针,由Execution Engine读取下一条指令。假设该线程正在运行java方法,则PC Register存储的是 正在被运行的指令的地址。假设是本地方法。PC Register的值未定义。PC寄存器很小。仅仅占用一个字宽。能够持有一个returnAdress或者特定平台的一个指针。
2.4.4. Method Area 在HotSpot JVM的实现中属于非堆区,非堆区包含两部分:Permanet Generation和Code Cache,而Method Area属于Permanert Generation的一部分。
Permanent Generation用来存储类信息。比方说:class definitions,structures,methods。 field, method (data and code) 和 constants。
Code Cache用来存储Compiled Code。即编译好的本地代码,在HotSpot JVM中通过JIT(Just In Time) Compiler生成,JIT是即时编译器,他是为了提高指令的运行效率,把字节码文件编译成本地机器代码。例如以下图:
引用一个经典的案例来理解Stack,Heap和Method Area的划分,就是Sring a="xx";Stirng b="xx",问是否a==b?
首先==符号是用来推断两个对象的引用地址是否同样,而在上面的题目中,a和b按理来说申请的是Stack中不同的地址,可是他们指向Method Area中Runtime Constant Pool的同一个地址。依照网上的解释,在a赋值为“xx”时。会在Runtime Contant Pool中生成一个String Constant。当b也赋值为“xx”时,那么会在常量池中查看是否存在值为“xx”的常量。存在的话。则把b的指针也指向“xx”的地址,而不是新生成一个String Constant。我查阅了网络上大家关于String Constant的存储的说说法,存在稍微区别的是,它存储在哪里。有人说Heap中会分配出一个常量池,用来存储常量,全部线程共享它。而有人说常量池是Method Area的一部分,而Method Area属于非堆内存。那怎么能说常量池存在于堆中?
我觉得,事实上两种理解都没错。Method Area的确从逻辑上讲能够是Heap的一部分,在某些JVM实现里从堆上开辟一块存储空间来记录常量是符合JVM常量池设计目的的,所曾经一种说法没问题。
对于后一种说法,HotSpot JVM的实现中的确是把方法区划分为了非堆内存。意思就是它不在堆上。我在HotSpot JVM做了个简单的实验,定义多个常量之后。程序抛出OOM:PermGen Space异常,印证了JVM实现中常量池是在Permanent Space中的说法。可是。我的JDK版本号是1.6的。
查阅资料。JDK1.7中InternedStrings已经不再存储在PermanentSpace中,而是放到了Heap中;JDK8中PermanentSpace已经被全然移除,InternedStrings也被放到了MetaSpace中(假设出现内存溢出,会报OOM:MetaSpace。这里有个关于两者性能对照的文章: 。 所以。仁者见仁,智者见智,一个馒头足以引发血案,就算是同一个商家的JVM。毕竟JDK版本号在更新,也许正如StackOverFlow上大神们所说。对于理解JVM Runtime Data Area这一部分的划分逻辑。还是去看相应版本号的JDK源代码比較靠谱,或者是參考不同的版本号JVM Specification。
2.4.5. Native Method Stack 是供本地方法(非java)使用的栈。每一个线程持有一个Native Method Stack。
3.JVM的执行原理简单介绍
Java 程序被javac工具编译为.class字节码文件之后,我们执行java命令。该class文件便被JVM的Class Loader载入。能够看出JVM的启动是通过JAVA Path下的java.exe或者java进行的。
JVM的初始化、执行到结束大概包含这么几步:
调用操作系统API推断系统的CPU架构,依据相应CPU类型寻找位于JRE文件夹下的/lib/jvm.cfg文件,然后通过该配置文件找到相应的jvm.dll文件(假设我们參数中有-server或者-client。 则载入相应參数所指定的jvm.dll。启动指定类型的JVM),初始化jvm.dll而且挂接到JNIENV结构的实例上。之后就能够通过JNIENV实例装载而且处理class文件了。class文件是字节码文件,它依照JVM的规范,定义了变量,方法等的具体信息,JVM管理而且分配相应的内存来运行程序,同一时候管理垃圾回收。
直到程序结束。一种情况是JVM的全部非守护线程停止。一种情况是程序调用System.exit(),JVM的生命周期也结束。
关于JVM怎样管理分配内存,我通过class文件和垃圾回收两部分进行了学习。
二、JVM的内存管理和垃圾回收
JVM中的内存管理主要是指JVM对于Heap的管理,这是由于Stack,PC Register和Native Method Stack都是和线程一样的生命周期,在线程结束时自然能够被再次使用。
尽管说,Stack的管理不是重点,可是也不是全然不讲究的。
1.栈的管理
JVM同意栈的大小是固定的或者是动态变化的。在Oracle的关于參数设置的官方文档中有关于Stack的设置(http://docs.oracle.com/cd/E13150_01/jrockit_jvm/jrockit/jrdocs/refman/optionX.html#wp1024112)。是通过-Xss来设置其大小。关于Stack的默认大小对于不同机器有不同的大小。而且不同厂商或者版本的jvm的实现其大小也不同,例如以下表是HotSpot的默认大小:
Platform | Default |
---|---|
Windows IA32 | 64 KB |
Linux IA32 | 128 KB |
Windows x86_64 | 128 KB |
Linux x86_64 | 256 KB |
Windows IA64 | 320 KB |
Linux IA64 | 1024 KB (1 MB) |
Solaris Sparc | 512 KB |
我们一般通过降低常量。參数的个数来降低栈的增长,在程序设计时,我们把一些常量定义到一个对象中,然后来引用他们能够体现这一点。另外,少用递归调用也能够降低栈的占用。
栈是不须要垃圾回收的,虽然说垃圾回收是java内存管理的一个非常热的话题。栈中的对象假设用垃圾回收的观点来看,他永远是live状态,是能够reachable的。所以也不须要回收。他占有的空间随着Thread的结束而释放。
(參考自:http://stackoverflow.com/questions/20030120/java-default-stack-size)
关于栈通常会发生下面两种异常:
1.当线程中的计算所须要的栈超过所同意大小时。会抛出StackOverflowError。
2.当Java栈试图扩展时,没有足够的存储器来实现扩展,JVM会报OutOfMemoryError。
我针对栈进行了实验,因为递归的调用能够致使栈的引用添加,导致溢出,所以设计代码例如以下:
我的机器是x86_64系统,所以Stack的默认大小是128KB,上述程序在执行时会报错:
而当我在eclipse中调整了-Xss參数到3M之后。该异常消失
另外栈上有一点得注意的是,对于本地代码调用。可能会在栈中申请内存,比方C调用malloc(),而这样的情况下,GC是管不着的,须要我们在程序中,手动管理栈内存,使用free()方法释放内存。
2.堆的管理
堆的管理要比栈管理复杂的多。我通过堆的各部分的作用、设置。以及各部分可能发生的异常,以及怎样避免各部分异常进行了学习。
上图是 Heap和PermanentSapce的组合图。当中 Eden区里面存着是新生的对象,From Space和To Space中存放着是每次垃圾回收后存活下来的对象 ,所以每次垃圾回收后,Eden区会被清空。 存活下来的对象先是放到From Space,当From Space满了之后移动到To Space。当To Space满了之后移动到Old Space。Survivor的两个区是对称的,没先后关系。所以同一个区中可能同一时候存在从Eden复制过来 对象,和从前一个Survivor复制过来的对象,而拷贝到年老区的仅仅有从第一个Survivor复制过来的对象。并且,Survivor区总有一个是空的。同一时候,依据程序须要,Survivor区是能够配置为多个的(多于两个),这样能够添加对象在年轻代中的存在时间,降低被放到年老代的可能。
Old Space中则存放生命周期比較长的对象,并且有些比較大的新生对象也放在Old Space中。
堆的大小通过-Xms和-Xmx来指定最小值和最大值。通过-Xmn来指定Young Generation的大小(一些老版本号也用-XX:NewSize指定)。 即上图中的Eden加FromSpace和ToSpace的总大小。然后通过-XX:NewRatio来指定Eden区的大小,在Xms和Xmx相等的情况下,该參数不须要设置。通过-XX:SurvivorRatio来设置Eden和一个Survivor区的比值。(參考自博文:)
堆异常分为两种。一种是Out of Memory(OOM),一种是Memory Leak(ML)。Memory Leak终于将导致OOM。实际应用中表现为:从Console看,内存监控曲线一直在顶部。程序响应慢。从线程看,大部分的线程在进行GC,占用比較多的CPU,终于程序异常终止,报OOM。
OOM发生的时间不定,有短的一个小时,有长的10天一个月的。
关于异常的处理,确定OOM/ML异常后,一定要注意保护现场。能够dump heap,假设没有现场则开启GCFlag收集垃圾回收日志,然后进行分析,确定问题所在。假设问题不是ML的话,一般通过添加Heap,添加物理内存来解决这个问题,是的话。就改动程序逻辑。
3.垃圾回收
JVM中会在下面情况触发回收:对象没有被引用,作用域发生未捕捉异常,程序正常运行完成。程序运行了System.exit(),程序发生意外终止。
JVM中标记垃圾使用的算法是一种根搜索算法。简单的说,就是从一个叫GC Roots的对象開始(GC ROOT节点主要在全局性的引用(比如常量或静态属性)与运行上下文(比如栈帧中的本地变量表)中)。向下搜索,假设一个对象不能达到GC Roots对象的时候,说明它能够被回收了。
这样的算法比一种叫做引用计数法的垃圾标记算法要好,由于它避免了当两个对象啊互相引用时无法被回收的现象。
注意:1. 假设在节点搜索中从ROOT不能到达这个对象。并不是一定会被回收,由于JVM给了这些对象第二次机会,这些对象会被第一次标记(“缓刑”)而且会进行一次筛选。筛选条件就是此对象是否有必要运行finalize()方法,(当对象覆盖finalized方法或者已经被运行过一次。都视为不是必需运行finalize),通过筛选的对象放入F-Queue队列,低优先级的finalize线程会运行这种方法。这里的“运行”是说虚拟机会触发这种方法,但并不承诺会等到这种方法运行完毕。由于finalize方法中可能有死循环,假设在这次运行中。能将自己解救(将自身(this)与引用链上的不论什么一个对象关联就可以(比方吧自己的this赋值给某个类变量或者成员变量)),那么JVM在进行第二次标记的时候就会将他移除即将回收的集合。
2. Elden没有足够的内存时会MInor GC,能够通过 -XX:PreteureSizeThreshold參数设置当对象 >=这个值时,会直接放入老年代。
JVM中对于被标记为垃圾的对象进行回收时又分为了一下3种算法:
1.标记清除算法,该算法是从根集合扫描整个空间,标记存活的对象,然后在扫描整个空间对没有被标记的对象进行回收。这样的算法在存活对象较多时比較高效。但会产生内存碎片。
2.复制算法,该算法是从根集合扫描。并将存活的对象拷贝到新的空间,这样的算法在存活对象少时比較高效。(适合新生代每次生存的对象非常少)
3.标记整理算法。标记整理算法和标记清除算法一样都会扫描并标记存活对象,在回收未标记对象的同一时候会整理被标记的对象。攻克了内存碎片的问题。(适合老年代:没有过多内存)。
4.分代收集。
JVM中,不同的 内存区域作用和性质不一样,使用的垃圾回收算法也不一样。所以JVM中又定义了几种不同的垃圾回收器(图中连线代表两个回收器能够同一时候使用):
1.Serial GC。从名字上看,串行GC意味着是一种单线程的。所以它要求收集的时候全部的线程暂停。这对于高性能的应用是不合理的。所以串行GC一般用于Client模式的JVM中。
2.ParNew GC。是在SerialGC的基础上,添加了多线程机制。可是假设机器是单CPU的,这样的收集器是比SerialGC效率低的。
3.Parrallel Scavenge GC。
这样的收集器又叫吞吐量优先收集器。而吞吐量=程序执行时间/(JVM执行回收的时间+程序执行时间),如果程序执行了100分钟,JVM的垃圾回收占用1分钟。那么吞吐量就是99%。
Parallel Scavenge GC因为能够提供比較不错的吞吐量,所以被作为了server模式JVM的默认配置。
4.ParallelOld是老生代并行收集器的一种。使用了标记整理算法,是JDK1.6中引进的。在之前老生代仅仅能使用串行回收收集器。
5.Serial Old是老生代client模式下的默认收集器,单线程运行,同一时候也作为CMS收集器失败后的备用收集器。
6.CMS又称响应时间优先回收器,使用标记清除算法。他的回收线程数为(CPU核心数+3)/4。所以当CPU核心数为2时比較高效些。
CMS分为4个过程:初始标记、并发标记、又一次标记、并发清除。
7.GarbageFirst(G1)。比較特殊的是G1回收器既能够回收Young Generation,也能够回收Tenured Generation。
它是在JDK6的某个版本号中才引入的,性能比較高,同一时候注意了吞吐量和响应时间。
对于垃圾收集器的组合使用能够通过下表中的參数指定:
默认的GC种类能够通过jvm.cfg或者通过jmap dump出heap来查看,一般我们通过jstat -gcutil [pid] 1000能够查看每秒gc的大体情况,或者能够在启动參数中增加:-verbose:gc -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:./gc.log来记录GC日志。
GC中有一种情况叫做Full GC,下面几种情况会触发Full GC:
1.Tenured Space空间不足以创建打的对象或者数组,会运行FullGC,而且当FullGC之后空间假设还不够。那么会OOM:java heap space。
2.Permanet Generation的大小不足,存放了太多的类信息,在非CMS情况下回触发FullGC。
假设之后空间还不够,会OOM:PermGen space。
3.CMS GC时出现promotion failed和concurrent mode failure时,也会触发FullGC。promotion failed是在进行Minor GC时,survivor space放不下、对象仅仅能放入旧生代。而此时旧生代也放不下造成的。concurrent mode failure是在运行CMS GC的过程中同一时候有对象要放入旧生代,而此时旧生代空间不足造成的。
4.推断MinorGC后,要晋升到TenuredSpace的对象大小大于TenuredSpace的大小,也会触发FullGC。
能够看出。当FullGC频繁发生时。一定是内存出问题了。
三、JVM的数据格式规范和Class文件
1.数据类型规范
根据冯诺依曼的计算机理论,计算机最后处理的都是二进制的数。而JVM是怎么把java文件最后转化成了各个平台都能够识别的二进制呢?JVM自定义了一个抽象的存储数据单位。叫做Word。
一个字足够大以持有byte、char、short、int、float、reference或者returnAdress的一个值,两个字则足够持有更大的类型long、double。
它一般是主机平台一个指针的大小。如32位的平台上。字是32位。
同一时候JVM中定义了它所支持的基本数据类型,包含两部分:数值类型和returnAddress类型。数值类型分为整形和浮点型。
整形:
byte | 值是8位的有符号二进制补码整数 |
short |
值是16位的有符号二进制补码整数 |
int |
值是32位的有符号二进制补码整数 |
long |
值是64位的有符号二进制补码整数 |
char |
值是表示Unicode字符的16位无符号整数 ,注意Java中 是Unicode字符,占两个字节 。ASCii占8位可是他没有中文字符 |
浮点:
float | 值是32位IEEE754浮点数 |
double | 值是64位IEEE754浮点数 |
returnAddress类型的值是Java虚拟机指令的操作码的指针。
对照java的基本数据类型,jvm的规范中没有boolean类型。这是由于jvm中对boolean的操作是通过int类型来进行处理的。而boolean数组则是通过byte数组来进行处理。
至于String,我们知道它存储在常量池中,但他不是基本数据类型。之所以能够存在常量池中,是由于这是JVM的一种规定。
假设查看String源代码,我们就会发现。String事实上就是一个基于基本数据类型char的数组。如图:
2.字节码文件
通过字节码文件的格式我们能够看出jvm是怎样规范数据类型的。以下是ClassFile的结构:
关于各个字段的定义(參考自JVM Specification 和 博文:http://www.cnblogs.com/zhuYears/archive/2012/02/07/2340347.html),
magic:
魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的Class文件。
魔数值固定为0xCAFEBABE。不会改变。
minor_version、major_version:
分别为Class文件的副版本号和主版本号。它们共同构成了Class文件的格式版本号号。不同版本号的虚拟机实现支持的Class文件版本号号也对应不同,高版本号号的虚拟机能够支持低版本号的Class文件。反之则不成立。
constant_pool_count:
常量池计数器,constant_pool_count的值等于constant_pool表中的成员数加1。
constant_pool[]:
常量池。constant_pool是一种表结构,它包括Class文件结构及其子结构中引用的全部字符串常量、类或接口名、字段名和其他常量。常量池不同于其他,索引从1開始到constant_pool_count -1。
access_flags:
訪问标志,access_flags是一种掩码标志,用于表示某个类或者接口的訪问权限及基础属性。access_flags的取值范围和对应含义见下表:
this_class:
类索引,this_class的值必须是对constant_pool表中项目的一个有效索引值。constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类或接口。
super_class:
父类索引。对于类来说。super_class的值必须为0或者是对constant_pool表中项目的一个有效索引值。假设它的值不为0。那constant_pool表在这个索引处的项必须为CONSTANT_Class_info类型常量,表示这个Class文件所定义的类的直接父类。
当然,假设某个类super_class的值是0,那么它必然是java.lang.Object类,由于仅仅有它是没有父类的。
interfaces_count:
接口计数器。interfaces_count的值表示当前类或接口的直接父接口数量。
interfaces[]:
接口表,interfaces[]数组中的每一个成员的值必须是一个对constant_pool表中项目的一个有效索引值,它的长度为interfaces_count。每一个成员interfaces[i] 必须为CONSTANT_Class_info类型常量。
fields_count:
字段计数器。fields_count的值表示当前Class文件fields[]数组的成员个数。
fields[]:
字段表,fields[]数组中的每一个成员都必须是一个fields_info结构的数据项。用于表示当前类或接口中某个字段的完整描写叙述。
methods_count:
方法计数器。methods_count的值表示当前Class文件methods[]数组的成员个数。
methods[]:
方法表,methods[]数组中的每一个成员都必须是一个method_info结构的数据项,用于表示当前类或接口中某个方法的完整描写叙述。
attributes_count:
属性计数器。attributes_count的值表示当前Class文件attributes表的成员个数。
attributes[]:
属性表。attributes表的每一个项的值必须是attribute_info结构。
四、一个java类的实例分析
为了了解JVM的数据类型规范和内存分配的大体情况。我新建了MemeryTest.java:
编译为MemeryTest.class后,通过WinHex查看该文件,相应字节码文件各个部分不同的定义,我了解了以下16进制数值的详细含义,虽然不清楚ClassLoader的详细实现逻辑。可是能够想象这样一个严谨格式的文件给JVM对于内存管理和运行程序提供了多大的帮助。
执行程序后。我在windows资源管理器中找到相应的进程ID.
而且在控制台通过jmap
-heap 10016查看堆内存的使用情况:
输出结果中表示当前java进程启动的JVM是通过4个线程进行Parallel GC,堆的最小FreeRatio是40%,堆的最大FreeRatio是70%。堆的大小是4090M。新对象占用1.5M。Young Generation能够扩展到最大是1363M。 Tenured Generation的大小是254.5M,以及NewRadio和SurvivorRadio中,以下更是详细给出了眼下Young Generation中1.5M的划分情况,Eden占用1.0M,使用了5.4%,Space占了0.5M,使用了93%,To Space占了0.5M,使用了0%。
以下我们通过jmap dump把heap的内容打印打文件里:
使用Eclipse的MAT插件打开相应的文件:
选择第一项内存泄露分析报告打开test.bin文件。展示出来的是MAT关于内存可能泄露的分析。
从结果来看,有3个地方可能存在内存泄露,他们占领了Heap的22.10%,13.78%,14.69%,假设内存泄露。这里通常会有一个比值很高的对象。打开第一个Probem
Suspect,结果例如以下:
ShallowHeap是对象本身占用的堆大小。不包括引用,RetainedHeap是对象所持有的Shallowheap的大小,包括自己ShallowHeap和能够引用的对象的ShallowHeap。
垃圾回收的时候,假设一个对象不再引用后被回收。那么他的RetainedHeap是能回收的内存总和。通过上图能够看出程序中并没有什么内存泄露,能够放心了。
假设还有什么不太确定的对象。则能够通过多个时间点的HeapDumpFile来研究某个对象的变化情况。
五、小结
以上便是我近期几天对JVM相关资料的整理,主要环绕他的基本组成和执行原理等,内存管理,节本数据类型和字节码文件。JVM是一个很优秀的JAVA程序,也是个不错的规范,这次整理学习让我对他有了更加清晰的认知,对Java语言的理解也更加加深。
这里补充一点 :java的重载与多态事实上是与虚拟机相关的,重载是静态分派(编译时决定执行哪个方法),多态是动态分派(执行时决定执行哪个方法)。以下给出重载代码:
public class JVM { static abstract class A{ } static class B extends A{ } static class C extends A{ }//去掉此方法 会编译出错 public void say(A a){ System.out.println("a"); } public void say(B b){ System.out.println("b"); } public void say(C c){ System.out.println("c"); } public static void main(String args[]){ JVM jvm = new JVM(); A b = new B(); A c = new C(); jvm.say(b); jvm.say(c); }输出:
a a当上面改动为:
jvm.say((B)b);时 则输出结果为b,调用哪个方法是在编译时就确定的。
对于基本类型的重载级别:
char->int->long->float->double char和byte short是同一级别,类型转换不安全。
比方char的重载 会先看1.char的參数 2.int 3.long 4.float 5.double 6.包装器类型(character) 7.Serializable或者Comparable接口,他俩优先级一样,当同一时候存在时会提示类型模糊。拒绝编译。
8.Object 9. char.. 可变參数类型