1

一、概述

闲来有空翻翻书,捡捡一些基础点,就当静下心多写字。
Java基础的东西无论怎么样都会想到JVM,而提JVM必然想到最常见的一些点:字节码加载,类初始化,方法执行,对象内存分配和回收,线程和锁机制等等。归纳整理的时候,怎么可以少了它们。不过,我打算换个方式,不写太多概念(网上一搜一把的),想从一些代码、例子、题目或者疑问等方面来写写。

二、背景知识

JVM内存管理需要理解的点:内存空间的划分、内存分配和内存回收。

  • 内存空间:认识方法区、堆区、本地方法栈等几个空间,了解堆的分代管理。

  • 内存分配:在Java中这块比较容易,就是栈和堆,而创建对象都在堆区。

  • 内存回收:实战上最主要是关注什么时候会触发回收(GC)和回收对性能的影响,学习上可以了解不同回收起和回收算法。

验证和测试需要的点:常见的启动参数、GUI类工具。

  • 常见的启动参数:-Xms -Xmx -Xmn 等等

  • GUI类工具:JProfiler(推荐)、JVisualVM、MAT、JMap、JHat。

三、一个GC题目

1)当用-Xms30m -Xmx30m -Xmn10m -XX:+UseParallelGC 执行上面的代码时会执行几次Minor GC和几次Full GC呢?
2)分别说明你的结果是如何推出来的?

public static void main(String[] args) throws Exception{
    List<Object> caches=new ArrayList<Object>();
    for(int i=0;i<7;i++){
        caches.add(new byte[10241024*3]);
    }
    caches.clear();
    for(int j=0;j<2;j++){
        caches.add(new byte[10241024*3]);
    }
    Thread.sleep(5000);
}

先思考下,别忙着往下看,万一我的分析并不对呢!!
先思考下,别忙着往下看,万一我的分析并不对呢!!
先思考下,别忙着往下看,万一我的分析并不对呢!!

四、小白式的分析过程

第一步分析启动参数:
首先,看到"UseParallelGC"参数,表示这里采用的是“Parallel Scavenge+Serial Old”。那么,从回收器的类型,可以知道堆的新生代是基于复制算法的gc(即内存模型有from和to区),堆的老年代则基于标记-整理算法。
其次,看到"-Xms30m -Xmx30m"参数,表示堆区最大为30M,且不会动态扩展。
最后,再看到"-Xmn10m"参数,表示新生代区为10m,而且采用默认的7.5:1,即Eden7.5M(7680k),而from和to区各为1.25M。

第二步分析第一个循环
i0(即表示i=0产生的对象,后面规律相同)和i1 会直接放入Eden区。因为优先放Eden区,而且够放,此时Eden为 6m/7.5m.(两者表示 已用/剩余 。此处的6m为近似值,其他jvm对象之类占用内存的,不细讨论)。

i2来了,要放入Eden区,发现空间不够。触发MinorGC。然后把i2放到Eden。
结果将i0~i1直接诶转入老年代 ,原因是对象3m太大放不了From区(前面提到才1.25m)。
此时Eden区 3m/7.5m 老年代6m/20m
gc的log --> [PSYoungGen: 6451K->272K(8960K)] 6451K->6416K(29440K)

i3来了,直接继续放入Eden区。 此时Eden区 6m/7.5m 老年代6m/20m

i4来了,跟i2一样的情况,发现Eden不够放了。再次触发MinorGC。然后把i4放到Eden。
结果将i2~i3直接诶转入老年代,去陪i0和i1了。
此时Eden区 3m/7.5m 老年代12m/20m
gc的log --> [PSYoungGen: 6650K->256K(8960K)] 12794K->12544K(29440K)

i5来了,继续放入Eden区,此时加上前面i4,Eden区 6m/7.5m 老年代12m/20m

i6来了,跟前面i2、i4情况一样,触发了一次MinorGC。然后把i6放到Eden。
结果: i6 在Eden,i0~i5 6个在老年代。 此时Eden区 3m/7.5m 老年代18m/20m
gc的log -->[PSYoungGen: 6453K->224K(8960K)] 18741K->18656K(29440K)
这里还多了一次FullGC。
gc的log -->[PSYoungGen: 240K->0K(8960K)] [PSOldGen: 18432K->18593K(20480K)]
暂时未能完全分析明白这点,但gc日志来猜测,应该是标记-整理起作用了,为了整理出连续的空间吧。

第三步: 由于执行了caches.clear(); 等于宣告前面的i0~i6的7个对象都不可用了(即GC Roots不可达)
但还由于各区都有足够大的空间,只要程序运行未达到"GC安全点"是不会触发GC的。

第四步:
j0来了,它会继续放到Eden区,此时陪着还没回收的i6。
此时Eden区 6m/7.5m 老年代18m/20m

j1来了,这时候内存管理会发现新生代和老年代都不够空间申请了,即触发FullGC。
结果:从老年代把不可用的i0~i5全部干掉,再把j0移入老年代,再把新生代中的i6干掉,然后j1放入新生代。
此时Eden区 3m/7.5m 老年代3m/20m
gc的log -->[PSYoungGen: 6178K->0K(8960K)] [PSOldGen: 18593K->3233K(20480K)]

五、通过运行程序,验证分析

运行上面的程序,参数为: -Xms30m -Xmx30m -Xmn10m -XX:+UseParallelGC -XX:+PrintGCDetails
加上-XX:+PrintGCDetails,这样可以打印GC的log情况来验证前面的分析。

ps:不同的虚拟机版本打印的输出多少会有差异,以下log,是我在sum的1.6.0_43版本打印出来的,另外删减掉一些不关心的输出。

[GC [PSYoungGen: 6451K->320K(8960K)]
[GC [PSYoungGen: 6698K->240K(8960K)]
[GC [PSYoungGen: 6437K->240K(8960K)]
[Full GC [PSYoungGen: 240K->0K(8960K)] [PSOldGen: 18432K->18594K(20480K)] 18672K->18594K(29440K)
[Full GC [PSYoungGen: 6178K->0K(8960K)] [PSOldGen: 18594K->3234K(20480K)] 24773K->3234K(29440K)
Heap
PSYoungGen total 8960K, used 3248K
    >eden space 7680K, 42% used
    >from space 1280K, 0% used
    >to space 1280K, 0% used
PSOldGen total 20480K, used 3234K
PSPermGen total 21248K, used 3043K

六、最后补充涉及到的知识点

6.1 关于堆区分代管理

新生代GC(Minor GC):主要是发生在新生代的收集动作,据说IBM做过调查,绝大多数对象都是朝生夕死,所以MinorGC非常频繁,速度也比较快。
老年代GC(Full GC):是指发生在老年代的收集动作,但是通常也会对年轻代进行垃圾收集。

6.2 GC几个参数说明

-Xmx 设置JVM最大可用内存为30M。
-Xms 设置JVM扩展内存为30M。(此值一般设置与-Xmx相同,以避免每次垃圾回收完后JVM重新分配内存)
-Xmn:设置年轻代大小为10m。 (整个堆大小=年轻代大小 + 年老代大小 持久代大小 : 持久代PermGen是非堆的,可以通过jconsole查看)
-Xss128k:(设置每个线程的堆栈大小)
持久代,是通过PermSize和MaxPermSize
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。

6.3 不同类型处理器

串行处理器: 适用数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用。 缺点:只能用于小型应用
并行处理器: 适用“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。 缺点:应用响应时间可能较长
并发处理器: 适用“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境。

6.4 其它

GC Roots不可达:GC回收过程判断对象是否可以回收的一种方式,叫可达性测试!
打印gc日志:启动程序时加vm参数-XX:+PrintGCDetails。


码农贾维斯
320 声望5 粉丝

写写代码挺好。。