JVM入门
Posted lijizhi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM入门相关的知识,希望对你有一定的参考价值。
1、JAVA代码执行过程
Java源程序(.java)经过Java编译器(javac)以后, 生成一个或多个字节码(.class)文件,
2、JVM模型图(>=JDK1.8)
(阅读图中备注)其中类信息包含类的方法,方法由所有对象共享所以只有一份,成员变量保存在 [ 堆内存 ] 对象的实例中—线程内存模型中的主内存(堆)区域。从堆中拷贝进线程的工作内存(局部变量表),进行操作,再回传至堆内存,线程不安全就发生在这个阶段。
JVM将栈内存中分配给运行的线程,每个线程有自己独享的栈内存空间,线程栈内存又根据执行的方法分配栈帧,每一个栈帧包含局部变量表,操作数栈,动态连接,及方法的出口。
局部变量表由32位的数据槽组成若为long或double类型则占用两个槽位,用来【临时保存】在执行该方法时的需要的局部变量。操作数栈用于字节码指令中的压栈和出栈的等操作。动态连接指向运行时方法区中该栈帧所属方法的引用。方法出口指向方法结束后字节码指令的标记。
3、垃圾回收GC
由于栈,本地方法区和程序计数器和线程的生命周期相同,当线程执行完毕会自动撤销,所以不是gc主要关注的内存区域。主要回收内存为线程共享的堆内存,及方法区内存。
堆内存又可细分出年轻代和老年代(JDK1.7中还有永久代及方法区,JDK1.8以后将方法区移到系统内存中,所以JVM调优要合理分配方法区内存)。按照内存区划分为Eden区(伊甸园)、S0(From)、S1(To)、老年区,其中S0和S1共同组成了Survivor区,即在一次Minor GC完成后依然存活的对象会从Eden复制到S0。(gc复制算法)
此图来自网络,侵删
3.0 对象分配规则
-
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次MinorGC(针对年轻代的gc)
-
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象,如巨长的数组)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)。
-
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次MinorGC那么对象会进入Survivor区,之后每经过一次MinorGC那么对象的年龄加1,知道达到阀值(15)对象进入老年区。
-
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。
-
空间分配担保。每次进行MinorGC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次FullGC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行FullGC。
3.1 判断对象死活
引用计数法:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1,引用失效减1,值为0则表示对象可被回收。无法解决循环引用问题。
A a = new A() //new A +1 B b = new B() //new B +1 a.x=b //new B +1 b.x=a //new A +1 a=null // -1 b=null // -1 // 计数器中A、B 依然不为 0无法回收
可达性分析法: 通过一系列的 ‘GC Roots’ 的对象作为起始点,从这些节点出发所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连的时候说明对象不可用。
在Java语言中,可以作为GCRoots的对象包括下面几种:
(1). 栈内存,栈帧中的局部变量表中引用的对象。
(2). 方法区中的static变量引用的对象。
(3). 方法区中final引用的对象。
(4). 本地方法栈中Native方法引用的对象。
堆内存主要回收“死掉的对象”,方法区垃圾回收主要两部分内容:废弃的常量和无用的类。
3.2 搞定方法区
判断废弃常量:一般是判断没有该常量的引用。
判断无用的类:要以下三个条件都满足
-
该类所有的实例都已经回收,也就是 Java 堆中不存在该类的任何实例。
-
加载该类的 ClassLoader 已经被回收。
-
该类对应的 java.lang.Class 对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
3.3 垃圾回收算法
标记-清除算法:算法执行过程和名字一致,后果是会带来大量内存碎片,如果碰到要放大对象的时候就不得不触发GC
复制算法:解决前一种方法的不足,但是会造成空间利用率低下。因为大多数新生代对象都不会熬过第一次 GC。所以没必要 1 : 1 划分空间。可以分一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性复制到另一块 Survivor 上,最后清理 Eden 和 Survivor 空间。大小比例一般是 8 : 1 : 1,每次浪费 10% 的 Survivor 空间。但是这里有一个问题就是如果存活的大于 10% 怎么办?这里采用一种分配担保策略:多出来的对象直接进入老年代
标记-整理算法:先标记,再将存活对象移动到一块变成连续的,再清除边界外的部分。
3.4 内存回收具体实现
内存回收的具体实现(针对HotSpot VM):
①Serial收集器:单线程(Stop The World以下简称STW)使用标记-整理算法,新生代收集器
②ParNew收集器:Serial的多线程版本,新生代收集器
③Parallel Scavenge收集器:也叫吞吐量优先收集器,是一个采用多线程基于复制算法并工作在新生代的收集器,其关注点在于达到一个可控的吞吐量。吞吐量=运行用户代码时间/(垃圾收集时间+运行用户代码时间)
④Serial Old 收集器:Serial 的老年代版本
⑤Parallel Old收集器:Parallel Scavenge收集器的老年代版本
⑥CMS(ConcurrentMarkSweep)收集器:以获取最短回收停顿时间为目标,工作在老年代,使用标记-清除算法,整个过程是跟随用户线程并发执行的,包含4个步骤:
1)初始标记(initial mark)标记“GCRoots”能够直接关联的对象,但是仍然会STW;
2)并发标记(concurrent mark)和用户线程并行进行GCRoots Tracing的过程;
3)重新标记(remark)修正并发标记期间由于用户程序继续运行而导致标记产生变动的那部分记录,SWT。
4)并发清除(concurrent sweep)可以和用户线程并行工作;
它的缺点是对CPU资源敏感和会产生大量内存碎片,还有就是在标记过后用户线程产生的垃圾没法及时清理,必须等待下一次GC。
⑦G1 收集器:是JDK1.7提供的一个工作在新生代和老年代的收集器,基于“标记-整理”算法实现,在收集结束后可以避免内存碎片问题。
优点:
1)能使用多cpu缩短停顿,通过并发使得用户线程不需要停顿
2)空间整合:整体基于标记-整理算法,局部使用复制算法-不产生内存碎片
3)停顿可预测,并且能由用户指定
4)分代收集
第三点的实现实际上是把全区域分割成多个Region进行回收并跟踪,并将各个Region垃圾堆积的价值大小建立优先级并按照优先级回收。
上述7中垃圾收集器的允许配合使用的方案;
3.5 查看JVM垃圾收集策略
打开cmd,输入
java -XX:+PrintCommandLineFlags -version
控制台输出如下,注意红色框中内容,对照如下截图可知当前JDK版本默认使用Serial Old+ Parallel Scavenge 协作进行GC操作。
以上是关于JVM入门的主要内容,如果未能解决你的问题,请参考以下文章