JVM总结(1)

1、JVM组成:

JVM由类加载器子系统、运行时数据区、执行引擎以及本地方法接口组成。



2、JVM运行原理:

Java源文件经编译器,编译成字节码程序,通过JVM将每一条指令翻译成不同平台机器码,通过特定平台运行。


4、运行时数据区组成

运行时数据区由程序计数器、Java虚拟机栈、本地方法栈、Java堆和方法区组成

程序计数器

可以看做当前线程做执行的字节码的行号指示器,每条线程都会有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储,所以可称这类内存区域是线程私有的内存。

Java虚拟机栈

这部分内存描述的是Java方法运行的内存模型:每个方法2在执行的同时都会创建一个栈帧,用于存储局部变量表等信息。平时所指的栈就是虚拟机栈,或者说虚拟机栈中的局部变量表。

本地方法栈

为虚拟机使用的native方法服务。

Java堆

存放对象实例,这是运行时数据区最大的一块内存空间。线程共享。

方法区

存放常量、静态常量和编译后的代码等数据。线程共享。

3、Java垃圾回收区域

Java垃圾回收只针对堆和方法区的内存。

线程方法区、虚拟机栈和程序计数器随着线程而生,随线程而灭因此不用管。



4.如何确定垃圾

采取引用计数是否可行?即统计有多少人引用了,如果0人引用,则判断为垃圾。缺点是:东西很多时,消耗太大;循环引用时失效:A引用了B,B引用了A,其实A\B都是垃圾,但他们引用永远都是1,不会被回收掉,陷入了死循环。

可达性分析算法:从垃圾回收的根(RC root)出发判断是否可见,这也是JVM采用的可达性分析算法。



如图object5,6,7都是可回收的垃圾


5.垃圾回收算法(方法论)

标记--清除算法:缺点产生大量不连续的内存碎片,碎片化很严重,导致程序运行时需要分配较大的对象时,无法找到足够的内存存放,不得不提前再进行一次GC

标记--整理算法:缺点是实际消耗很大,垃圾是一块一块的,要把回收空出的内存向前移动,不能整块移动,需要切成很多小块移动,所以消耗很大。



拷贝算法:先把内存分为两块,内存1往内存2拷贝,内存1全部清除,缺点是内存只能使用一般,浪费性能。


JVM实际使用的分代垃圾回收算法,就是把上面的的朴素算法综合一下

基础假设:大部分对象只存在很短的时间,这个假设确实也是对的

基于上面的假设,JVM将内存分为新生代和老生代。大部分只存在很短的时间,就放在新生代。对于少部分存在很长时间的对象,放在老生代去。

对新生代和老生代采取不同的做法

新生代经常性进行GC,因此要优化性能,采取类似Copy算法

将新生代分为Eden,Survivor1和Survivor2,Eden和survivor比例在8:1。




Eden是要回收的区域。1表示存活了1次垃圾回收,2表示存活了2次垃圾回收。


第一次回收,从Eden回收存活下来的对象放入Survivor1,然后清掉Eden。


又产生了垃圾

第二次回收,从Eden和Survivor1存活的对象放入Survivor2

清理掉eden和survivor1,如此反复进行,当survivor不够用时,就把survivor中年纪大的对象放入老生代
Full GC是对老生代进行GC,使用的是标记--整理算法。


6、内存分配与回收策略

对象优先在Eden区分配

老年GC(full GC/Major GC)一般比新生代GC(Minor GC)慢十倍以上

大对象直接进入老年代,大对象是指需要大量连续内存空间的Java对象,比如很长的字符串或者数组。

长期存活的对象将进入老年代。默认15岁

动态对象年龄判定。如果Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代。

空间分配担保:当出现大量对象Minor GC后仍然存活的情况,需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。


JVM垃圾回收参数



持久代(JDK8改进成元空间)



如何解决OutOfMemoryError:PermGen Space的问题?

使用-XX:MaxPermSize调整,调大一点

Java 1.8帮我们做了改进,取消了持久代,改进成元空间


元空间


调用String.intern()方法的时候,会将共享池中的字符串与外部的字符串(s)进行比较,如果共享池中有与之相等的字符串,则不会将外部的字符串放到共享池中的,返回的只是共享池中的字符串,如果不同则将外部字符串放入共享池中,并返回其字符串的句柄(引用)-- 这样做的好处就是能够节约空间

一个初始时为空的字符串池,它由类 String 私有地维护。 当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串。否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。 它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true


谈谈Java垃圾回收机制

回答垃圾回收在什么时候运行?JVM分配内存失败的时候会运行。还可以手动调用System.gc(),JVM会知道你想垃圾回收了,至于到底是否进行GC由JVM自行判断,一般都是进行的。

垃圾回收对什么对象进行回收?从垃圾回收的根节点出发,看的见的不回收,看不见的都回收。

垃圾回收算法对内存划分成了哪些区域?新生代、老生代、Metaspace(元空间),新生代又分为了Eden、Survivor1、Survivor2。并回答具体的算法。

7、垃圾收集器(具体实现)

Serial,新生代收集,单线程,GC的时候必须暂停其他所有工作线程。JVM Client模式下的默认新生代收集器

ParNew,新生代收集,Serial的多线程版本。JVM Server模式下的默认新生代收集器。实现了GC线程与工作线程同时工作。举例就是,一边打扫卫生的时候,还可以一边往地上扔纸屑。

单CPU环境中,ParNew绝对不会有比Serial更好的效果。

Serial和ParNew都是与CMS配合工作。

Parallel Scavenge,新生代收集,关注“吞吐量”。比如JVM总共运行了100分钟,其中GC花了1分钟,那吞吐量就是99%。

新生代的垃圾收集器都是“复制”算法。

Serial Old,单线程,使用“标记-整理”算法负责老年代。

Parallel Old ,多线程,“标记-整理”,和Parallel Scavenge同时使用提高“吞吐量”。

CMS,并发,低停顿,唯一的“标记-清除”因此可能导致老年代碎片化严重,无法容纳新生代提升上来的大对象,从而CMS失败,退回到Serial Old算法,导致GC时间过长,可以尝试调大Survivor的空间和调整 CMS 垃圾收集在老年代占比达到多少时启动来减少问题发生频率,越早启动问题发生频率越低,但是会降低吞吐量,具体得多调整几次找到平衡点;另外,如果GC频率太快,说明空间不足,首先可以尝试调大新生代空间和晋升阈值,专注最短GC停顿时间,使网站响应更快,服务不会出现长时间停滞。

G1,多线程,分代收集,“标记-整理”,可预测的停顿。如果追求低停顿,可以尝试G1。

介绍一下CMS的工作流程

  • 初始标记:用户线程暂停,标记所有和根对象直接相连的对象。有停顿,“stop the world”

  • 并发标记:用户线程起来,标记所有可达的对象。有停顿,“stop the world”

  • 预处理阶段:用户线程起来之后,可能有修改的对象,这个阶段就是标记从新生代转移到老年代的对象,老年代新分配到的对象以及修改的对象等等。对新生代的处理方式是minor GC,对老年代的处理方式是使用card table(字节数组)扫描老年代,进行分块,如果在并发标记阶段被修改了,就标记为dirty card,预处理阶段通过可达性分析消除dirty card。

  • 重新标记:暂停线程,重新标记可达对象,经过预处理后速度变快。

  • 并发清除:与用户线程同时进行,清理垃圾。