JVM虚拟机的内存结构如下图所示
方法区和堆由所有线程共享,其他区域都是线程私有的
程序计数器(Program Counter Register)
通过程序计数器来记录线程执行的jvm字节码的行数
虚拟机栈(Java Stack)
虚拟机栈也是线程私有的,每创建一个线程,虚拟机就会为这个线程创建一个虚拟机栈,虚拟机栈表示Java方法执行的内存模型,每调用一个方法,就会生成一个栈帧(Stack Frame)用于存储方法的本地变量表、操作栈、方法出口等信息,当这个方法执行完后,就会弹出相应的栈帧。栈帧分为三部分:局部变量区(Local Variables)、操作数栈(Operand Stack)和帧数据区(Frame Data)。
局部变量区(Loca Variables)
局部变量区被组织一个一个从0开始的字数组,byte、short、char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true,long和double占据两个字长。
操作数栈(Operand Stack)
操作数栈也被组织为一个字数组,但不同于局部变量区,它不是通过数组下标访问的,而是能过栈的Push和Pop操作,前一个操作Push进的数据可以被下一个操作Pop出来使用。
帧数据区(Frame Data)
这部分的作用主要有三部分:
- 常量池中数据的解析
- 方法执行完后处理方法返回,恢复调用方现场
- 方法执行过程中抛出异常时的异常处理,存储有一个异常表,当出现异常时虚拟机查找相应的异常表看是否有对应的Catch语句,如果没有就抛出异常终止这个方法调用
本地方法栈(Native Method Stack)
与虚拟栈类似,每个线程私有,在调用本地方法时使用
方法区(Method Area)
用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译后的代码等信息。
方法区是线程间共享的,当两个线程同时需要加载一个类型时,只有一个类会请求ClassLoader加载,另一个线程会等待。
对于每一个加载的类型,会在方法区中保存以下信息:
- 类及其父类的全限定名(java.lang.Object没有父类)
- 类的类型(Class or Interface)
- 访问修饰符(public, abstract, final)
- 实现的接口的全限定名的列表
- 常量池
- 字段信息
- 方法信息
- 除常量外的静态变量
- ClassLoader引用
- Class引用
对于每一个字段,会在方法区中保存以下信息(字段声明顺序也会保存):
- 字段名
- 字段的类型
- 字段的修饰符(public, private , protected, static, final, volatile, transient)
对于每一个方法,会在方法区中保存以下信息(方法声明顺序也会保存):
- 方法名
- 方法返回类型(或void)
- 参数信息
- 方法修饰符(public, private, protected , static, final, synchronized, native, abstract)
如果方法不是抽象方法也不是本地方法(Native Method),还会保存以下信息:
- 方法的字节码
- 本地变量表及操作数栈的大小
- 异常表
虚拟机需要存储一些数据,用来快速地访问一个类对象中的方法,一般实现为一个方法表。
方法区中还有一部分是运行时常量池,主要用来存储编译时生成的字面量和符号引用,常量也可以在运行时产生,如String的intern方法。
方法区中也可能存在GC,但虚拟机规范对此不做要求,主要是回收一些常量和卸载一些不用的类型信息,不过要卸载一个类的条件很难达到,而且些处GC其实也回收不了多少内存。
堆(Heap)
虚拟机中用于存放对象与数组实例的地方,垃圾回收的主要区域就是这里(还可能有方法区)。
如果垃圾收集算法采用按代收集(目前大都是这样),这部分还可以细分为新生代和老年代。
新生代又可能分为Eden区,From Survivor区和To Survivor区,主要是为了垃圾回收。所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。
Java堆只要求逻辑上是连续的,在物理空间上可以不连续。
垃圾回收
JVM在运行时,除了一个默认的主线程,还有GC线程 ,作为守护线程,伴随这主线程结束。当GC线程运行时,应用程序将暂停,保证GC线程不会与其他线程产生资源冲突。GC线程除了完成垃圾回收工作,还会把Young区中经过几次垃圾回收都没有回收的对象升级到Older区。
JVM使用的垃圾回收器不同,GC线程也有所区别。
Java有四种类型的垃圾回收器:
- 串行垃圾回收器(Serial Garbage Collector)
- 并行垃圾回收器(Parallel Garbage Collector)
- 并发标记扫描垃圾回收器(CMS Garbage Collector)
- G1垃圾回收器(G1 Garbage Collector)
每种类型都有自己的优势与劣势。重要的是,我们编程的时候可以通过JVM选择垃圾回收器类型。我们通过向JVM传递参数进行选择。每种类型在很大程度上有 所不同并且可以为我们提供完全不同的应用程序性能。理解每种类型的垃圾回收器并且根据应用程序选择进行正确的选择是非常重要的。
1、串行垃圾回收器
串行垃圾回收器通过持有应用程序所有的线程进行工作。它为单线程环境设计,只使用一个单独的线程进行垃圾回收,通过冻结所有应用程序线程进行工作,所以可能不适合服务器环境。它最适合的是简单的命令行程序。
通过JVM参数-XX:+UseSerialGC
可以使用串行垃圾回收器。
2、并行垃圾回收器
并行垃圾回收器也叫做 throughput collector 。它是JVM的默认垃圾回收器。与串行垃圾回收器不同,它使用多线程进行垃圾回收。相似的是,它也会冻结所有的应用程序线程当执行垃圾回收的时候
3、并发标记扫描垃圾回收器
并发标记垃圾回收使用多线程扫描堆内存,标记需要清理的实例并且清理被标记过的实例。并发标记垃圾回收器只会在下面两种情况持有应用程序所有线程。
- 当标记的引用对象在tenured区域;
- 在进行垃圾回收的时候,堆内存的数据被并发的改变。
相比并行垃圾回收器,并发标记扫描垃圾回收器使用更多的CPU来确保程序的吞吐量。如果我们可以为了更好的程序性能分配更多的CPU,那么并发标记上扫描垃圾回收器是更好的选择相比并发垃圾回收器。
通过JVM参数 XX:+USeParNewGC
打开并发标记扫描垃圾回收器。
4、G1垃圾回收器
G1垃圾回收器适用于堆内存很大的情况,他将堆内存分割成不同的区域,并且并发的对其进行垃圾回收。G1也可以在回收内存之后对剩余的堆内存空间进行压缩。并发扫描标记垃圾回收器在STW情况下压缩内存。G1垃圾回收会优先选择第一块垃圾最多的区域
通过JVM参数 –XX:+UseG1GC
使用G1垃圾回收器
Java中对象分为四种引用类型:
ReferenceQueue
queue
=
new
ReferenceQueue ();
PhantomReference
pr
=
new
PhantomReference (
object
,
queue
);
|
对于软引用,弱引用与虚引用,在创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,这相当于是一种通知机制。当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。
四种引用在垃圾回收时的区别:
下面表格说明:
垃圾回收算法(GC)
1.引用 计数器法JDK1.2之前,使用引用计数器法,当引用个数为0时,对对象进行回收。但当两个对象存在互相引用时,即引用图出现了环,那么两个对象的引用都为1,但其实两个对象已经没有其他的引用,引用计数器法便失效了。
2.根搜索算法
在JDK1.2之后,开始使用根搜索算法。根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。
目前java中可作为GC Root的对象有
1、 虚拟机栈中引用的对象(本地变量表)
2、 方法区中静态属性引用的对象
3、 方法区中常量引用的对象
4、 本地方法栈中引用的对象(Native对象)
在上文中已经提到了,我们的对象在内存中会被划分为5块区域,而每块数据的回收比例是不同的,根据IBM的统计,数据如下图所示:
图中的Java栈即为虚拟机栈。
JDK1.2之前主要通过引用计数器来标记是否需要垃圾回收,而1.2之后都使用根搜索算法来收集垃圾,而收集后的垃圾是通过什么算法来回收的呢?
1、 标记-清除算法
标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。
标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片!
2、 复制算法
复制算法采用从根集合扫描,并将存活对象复制到一块新的,没有使用过的空间中,这种算法当控件存活的对象比较少时,极为高效,但是带来的成本是需要一块内存交换空间用于进行对象的移动。
3、 标记-整理算法
标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。
目前的JVM主要采用分代回收机制,将堆中的对象分为年轻待,老年代,方法区中的对象为永久代。永久代即是方法区的一种实现。
年轻代存活时间短,回收频繁,使用标记-清除方法。
老年代存活时间长,回收次数少,使用标记-复制方法。
永久代一般不回收,当方法区中内存空间不够时,再进行回收。
在JDK1.8之后,取消了永久代,使用metaspace(元空间)来代替。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过设置参数来指定元空间的大小。
JAVA GC的监视和分析工具:
Java VisualVM
Java VisualVM使用是免费的,其需要安装Java SE SDK。看一下Java JDK的bin文件夹中(路径:\Java\jdk1.8.0\bin),这里面有很多javac和java工具,jvisualvm就是其中之一。
Java VisualVM能够被用于:
- 生成并分析堆的内存转储;
- 在MBeans上观察并操作;
- 监视垃圾回收;
- 内存和CPU性能分析;