JVM(相关知识点整理一)

JVM相关知识点整理

平时在开发Java程序时,没有仔细查看过jvm的相关知识,与是来整理这块的知识点。

JDK

所谓的JDK是指程序开发的最小环境,基本上Java程序设计语言、Java虚拟机、Java API类库这三部分组成了JDK

JRE

JRE则是JAVA运行的标准环境,包含Java的api和java的虚拟机组成。

运行时数据区域的分布

属于线程私有的数据
  1. 程序计数器
    程序计数器表示当前线程执行的字节码的行号指示器,JAVA的方法才有这个值,如果调用本地的native方法则该值是不存在的。
  2. 虚拟机栈
    每个方法都会创建一个栈帧,主要存储操作数栈、局部变量表、动态链接,方法出口等数据。
属于线程共享的数据

  1. 所谓的堆可以简单理解为平时我们new对象时存放的区域,它分为新生代和老年代,还有叫做Eden区,Servior区。
  2. 方法区
    方法区存放了虚拟机加载类的信息,静态变量,常量等数据。

  3. 运行时常量池
    用于存放编译时生成的各种字面常量和符合引用。比如Stirng s=”hello” 也是直接存放在常量池中的。

笔者认为以上是JVM中最基础的内容了,那么如何来模拟堆和栈的溢出呢,来看看对应的代码实现,以下代码都是基于JDK1.8实现的。

哪些对象需要回收

要回收的垃圾指的是不可能再被任何途径使用的对象,需要怎么样才能找到这些对象呢?

  • 引用计数法
    它的大概思想是每当一个地方引用这个对象时,计数器值+1,当引用失效时,计数器则-1,但是Java虚拟机并没有采用这种算法来进行判断,来看下面一段测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public Object instance = null;
private static final int _1MB = 1024 * 1024;

/**
* 占点内存,以便 GC 日志观看
*/
private byte[] bigSize = new byte[2 * _1MB];

public static void main(String[] args) {
testGC();
}

public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;

objA = null;
objB = null;

System.gc();
}
}

我们以虚拟机参数-XX:+PrintGCDetails -XX:+UseSerialGC为例查看gc日记:

1
2
3
4
5
6
7
8
9
10
[Full GC (System.gc()) [Tenured: 0K->638K(87424K), 0.0055890 secs] 10995K->638K(126720K), [Metaspace: 3204K->3204K(1056768K)], 0.0056711 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
def new generation total 39424K, used 1052K [0x0000000081200000, 0x0000000083cc0000, 0x00000000ab6a0000)
eden space 35072K, 3% used [0x0000000081200000, 0x0000000081307280, 0x0000000083440000)
from space 4352K, 0% used [0x0000000083440000, 0x0000000083440000, 0x0000000083880000)
to space 4352K, 0% used [0x0000000083880000, 0x0000000083880000, 0x0000000083cc0000)
tenured generation total 87424K, used 638K [0x00000000ab6a0000, 0x00000000b0c00000, 0x0000000100000000)
the space 87424K, 0% used [0x00000000ab6a0000, 0x00000000ab73f9e0, 0x00000000ab73fa00, 0x00000000b0c00000)
Metaspace used 3224K, capacity 4494K, committed 4864K, reserved 1056768K
class space used 354K, capacity 386K, committed 512K, reserved 1048576K

可以看到[Tenured: 0K->638K(87424K), 0.0055890 secs] 10995K->638K(126720K) 意味着虚拟机有对这两个对象的引用进行垃圾回收。

  • 可达性分析算法
    所谓的可达性指的是通过一系列的“GC Roots”的对象作为起点,从这些节点开始向下搜索,所有走过的路径作为引用链,当一个对象到GC roots没有任何引用的时候就会被回收。
    用白话来可以这样形容:比如JAVA中的方法执行与结束是存放在栈帧中的,如果栈帧中存放对象A和对象B,假如A依赖于B,那么A和B就组成一对引用链,不会被回收。如果A没有引用B,且B没有被其他的对象引用,此时B是可以被回收的。
    GC Roots包含的对象包含下面几种:
  1. 虚拟机栈
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中(Native方法)引用的对象。

垃圾回收算法

垃圾回收算法包括以下几种:

  1. 标记清除算法:该算法是这样的,首先需要标记出所有需要回收的对象,标记完成之后统一回收所有被标记的对象,这种算法效率不会很高,而且清除之后会造成空间不连续,导致程序运行到后期需要分配大对象的时候堆的空间不够抛出异常。
  2. 复制算法:该算法主要是把内存分成两块来管理,每次只会用到其中一块,当其中一块使用完了,就使用另一块的空间,然后再把使用过的一块的内存清除掉。这样只要对整个半区进行操作,该算法能很好管理内存的使用,不会造成内存碎片,但是空间有限。HotSpot虚拟机默认的使用Eden区和Survivor区的比例为8:1,Survivor区又分成From和To两块区域,每次只会用到其中一块。在垃圾回收时,如果Survivor区不能存放幸存的对象,此时需要依赖老年代进行担保。

现在商业虚拟机大部分都是这两种算法的整合,如果有大量对象死去,需要回收,则采用复制算法,复制成本低。如果对象的成活率高,需要回收的垃圾比较少,此时就采用标志整理算法,剔除不需要的对象。

总结

本章主要介绍Java虚拟机的一些基础知识,包括虚拟机的内存分布及回收算法,这些知识点在面试高级岗位中都有可能会被问到,需要好好理解。