JVM(1)运行时数据区

前言

得益于网络上大量的公开课以及前辈们的博客分享,让我有长足的进步。在此我也分享一下我的笔记希望能够帮助更多的人,欢迎留言指正及交流。

文章说明:
这篇文章,我把侧重点放在了堆空间和GC,相信也是大多数读者希望的。文章来源于我个人的笔记摘录,原笔记是按照我个人的记录习惯记录的,我以尽力修改,但是可能任然存在部分内容大家看的一头雾水,请留言,我会尽力修改。

一.栈空间(线程私有,不存在垃圾回收)

1.1.PC寄存器

  1. 每个 “线程” 都有一个PC寄存器,是线程私有的,也是在线程启动的时候创建的,就是一个指针,指向方法区中的方法字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令。
  2. PC寄存器占用的空间很小,几乎可以忽略不记,它是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  3. 如果执行的是一个native方法,那这个计数器是空的(因为native方法不归Java管理)。
  4. 在多个线程之间来会切换的时候,就是通过寄存器明确线程执行到哪一步了。
  5. 保存的是下一条将被执行指令的地址。

1.2.java栈

1.2.1.什么是java栈?

栈也叫栈内存,主管Java程序的运行,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就Over,生命周期和线程一致,是线程私有的,8种基本类型的变量 + 对象的引用变量 + 实例方法,都是在函数的栈内存中分配,栈的大小和具体 JVM 的实现有关,通常在256K~756K之间,约等于1Mb左右。

1.2.2.栈针

栈针存在于内存中,并不是CPU中,每一个线程独立拥有一个方法栈,每一个方法对应一个 “栈针”,栈针里存储着如下4项内容:
(1)局部变量表(LocalVariable Table)
(2)操作数栈
(3)Dynamic Linking(这个有点难理解,留着后续补充)
(4)return address
    a()方法 调用 b()方法,b()方法的返回值存放的位置,
    也可以理解为,
    a()方法 调用 b()方法,在调用 b()方法结束后,需要通过 return address 找到 a() 方法执行到哪里了,以便继续 a() 方法

可以看到字节码内容是:
(1)bipush 8
  把 “8” 丢进栈中(压栈)
(2)istore_1
  把 “栈顶元素(出栈)” 丢进 “局部变量表1” 的位置中(cp_info#17) 【到这一步完成了 int j = 8 的过程】
(3)iload_1
  把 “局部变量表1” 中的元素取出来,压栈
(4)iinc 1 by 1
  把 “局部变量表1” 中的元素+1 【到这一步完成了 j++,这时局部变量表中是9,栈中是8】
(5)istore_1
  把 “栈顶元素(出栈)” 丢进 “局部变量表1” 的位置中 【到这一步完成了 j = j++ 的赋值操作】
(6)return
  所以最终得到了 8
  总结:
  可以看到,字节码文件中还是优先完成了 j+1,然后再赋值给 j,只是最后出栈时,
  用栈中的 8 把局部变量表中的 9 给覆盖了,最后才返回 8。【如上图】

这次把 j = j++ 修改为 j = ++j,
可以看到字节码文件中只有 “iload_1” 和 “iinc 1 by 1” 换了位置,
这样的话会在局部变量表中完成 +1,再用 9 完成压栈和出栈,最终返回 9。

如果是 int j = 200; 超过了 -128~127 的范围,字节码文件不是 “bipush” 而是 “sipush” 【如上图】

public void run() {
  int k = 100;
  System.out.println(k);
}
在局部变量表中可以看到 this 参数,但是在 main() 方法中怎么没有 this 参数呢?
那是因为只有在非静态方法中才允许有 this 对象。 【如上图】

public static void main(String[] args) {
  new MyJVM().run(3, 4);
}
public void run(int a, int b) {
  int c = a+ b;
}
局部变量表:
  index[0] this
  index[1] a
  index[2] b
  index[3] c
字节码文件:
  0 iload_1
  1 iload_2
  2 iadd
  3 istore_3
  4 return
字节码文件解析:
  (1)把 3 压栈
  (2)把 4 压栈
  (3)把 3、4出栈,并相加得出的 7 压栈
  (4)把 7 出栈,放入 index[3] 的局部变量表中

1.3.本地方法栈(Native Method Stack)

  • 与java方法栈的区别是,这里存储 native 方法

1.4.本地方法接口(Native Interface)

写在这里是便于和 "本地方法栈" 比较,真实物理地址不是 "栈空间"

什么是本地接口?

可以调用第三方函数库的方法,如 private native void start0();

普通方法和native方法存储的位置?

普通方法存储在 -> java栈(java stack)
native方法存储在 -> 本地方法栈(native method stack)

二.堆空间(线程共享,存在垃圾回收)

2.1.方法区(Method Area)

供各线程共享的运行时内存区域

它存储了每一个类的结构信息(类似模板),例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容(通过.class文件进行类加载的Class对象),
“方法区” 是一种逻辑上的概念而不是真实的物理地址,在不同虚拟机里头实现是不一样的,最典型的区别就是 永久代(PermGen space) 和 元空间(Metaspace)
But,
实例变量存在堆内存中,和方法区无关

在不同的JDK版本中,拥有不同的实现方案

JDK7 之前:方法区 f = new 永久代(PermGen space)   存在于堆    存储常量池(8之后转移到了 “堆内存”)
JDK8 之后:方法区 f2 = new 元空间(Metaspace)    存在于物理内存

2.2.堆

堆内存结构

新生代(1/3)伊甸区(Eden区)(80%)
幸存者0区(S0/from区)(10%)
幸存者1区(S1/to区)(10%)
伊甸区快要满的时候开启GC/YGC
老年代(2/3)养老区[1]养老区满的时候开启Full GC/FGC
[2]当多次调用FGC,当养老区还是满载时,
报OOM,java.lang.OutOfMemoryError: Java heap space 异常,即堆内存溢出
[3]当出现OOM异常时,说明java虚拟机堆内存不足,原因有二:
a.java虚拟机堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
b.代码中创建了大量的大对象,并且长时间不能被垃圾收集器收集(即,存在被引用)。
java7,永久代,将一部分的堆内存分配给了永久代
java8,元空间,使用的是本机物理内存
  • 堆内存组成
    (1)堆内存逻辑上,分为3个组成:新生代,老年代,元空间
    (2)堆内存物理上,分为2个组成:新生代、老年代
  • MinorGC(轻GC)过程:复制 -> 清空 -> 交换
    (1)eden、from复制到to年龄+1
    首先eden满的时候触发第一次GC,把还活着的对象拷贝到from,
    当eden再一次触发GC时候,会同时扫描eden和from区域,同时对这两个区域进行垃圾回收,通过这次垃圾回收还存活的对象会被拷贝到to区,年龄+1。
    (2)清空eden、from
    然后清空eden、from,也即复制之后有交换,谁空谁是to。
    (3)to和from交换
    最后to和from交换,原来的to区变成下一个GC时from区,部分对象会在to和from来回复制, 交换15次,会存入老年代。
      扩展:什么时候进入老年代?
      [1]正常交换 15 次之后(GC Age 是4为二进制数,所以不能超过15)
      [2]动态年龄判断:当 eden+from 存活对象超过 to 区的50%时,直接把年龄最大的对象放入老年代,这个对象年龄不到15岁也无所谓
      比如:
      年龄1的33%,年龄2的33%,年龄3的34%,经过一次 Young GC ,假设都没有被回收,那么会直接把年龄2和年龄3的对象都要晋升。
  • 对象生命周期

    (1)产生一个对象,先尝试放入 “栈” 中,能放的话就放进去,出栈的时候生命结束
    (2)不能的话看看有多大,如果非常大直接放入 “老年代”,经过 Full GC 完成垃圾回收
    (3)不是特别大的话就放入 “年轻代”,经过 Young GC 进入 “老年代”,再经过 Full GC 完成垃圾回收

三.GC调优

3.1.垃圾收集算法

java 和 C++ 在垃圾回收的区别?

java 自动回收垃圾,开发效率高,执行效率低
C++ 手动回收垃圾,开发效率低,执行效率高

如何找到垃圾?

(1)reference count(引用计数)
每当有一个引用指向它的时候,就 +1,没有引用指向就 -1,直到为 0,就是垃圾对象了。
但是无法解决 “循环引用” 的问题,对象A指向对象B,对象B指向对象A,当外部没有一个其他引用指向这个整体的时候,对象A、对象B就无法回收。
(2)Root Searching(可达算法)
从一个被称为GC Roots的对象开始向下搜索,如果一个对象到GC Roots没有任何引用链相连时,则说明此对象不可用。
在java中可以作为GC Roots的对象有以下几种:
虚拟机栈中引用的对象、方法区类静态属性引用的对象、方法区常量池引用的对象、本地方法栈JNI引用的对象。

找到垃圾对象之后,如何清除?(GC4大算法)

(1)引用计数法
  [1]给对象添加一个引用计数器,每当有一个地方引用他时,计数器值就+1;当引用失效时,计数器值就-1,当计数器值为0是开启GC
  [2]缺点:①每次对对象的赋值均要维护计数器,且计数器本身也有一定的消耗
      ②难以处理循环引用的问题
(2)复制算法
  [1]主要用于新生代,因为对象存活率低,区域小,复制的对象相对少
  [2]优点:①没有垃圾碎片
      ②只需要扫描一次,效率还可以
  [3]缺点:①浪费一半的内存(from、to)
      ②若对象存活率很高时,则需要复制的对象就会很多,这段时间将不可忽略
(3)标记清除
  [1]主要用于老年代,因为 “对象存活率高”,区域大,复制的对象比较多,不适用于复制算法
  [2]算法分为标记和清除两个阶段,先标记要回收的对象,然后统一回收这些对象,
    GC触发时,将程序暂停,先标记一遍,然后统一收集这些对象,完成清除工作后,让程序继续运行
  [3]优点:不会浪费内存空间
  [4]缺点:①2次扫描耗时严重(第一次扫描由 GC Roots 确定哪些对象是不可回收的,进而确定需要清除的垃圾,第二次扫描清除被标记过的垃圾)
      ②产生内存碎片(即,存活下来的对象分布杂乱,为了应付这一点JVM不得不维持一个空闲列表,这又是一种开销)
      ③GC时程序暂停
(4)标记压缩(或标记整理)
  [1]主要用于老年代,与标记清除相同
  [2]机制:在整理压缩阶段不再对标记的对象做回收处理,而是让所有存活的对象向一端移动,然后直接清除边界以外的内存,
    这样当需要给新对象分配内存空间时,JVM只需要存储一个内存起始地址即可,这比维护一个空闲列表要少了很多开销
  [3]优点:没有内存碎片
  [4]缺点:需要移动对象的成本, 耗时也更加严重(需要扫描2次),效率不高,不仅标记所有存活对象,还要整理所有存活对象的引用地址,
    从效率上来讲,不如复制算法
(5)总结
  [1]内存效率: 复制 > 标记清除 > 标记压缩
  [2]内存整齐度: 复制 = 标记压缩 > 标记清除
  [3]内存利用率: 标记清除 = 标记压缩 > 复制

3.2.垃圾收集器

什么是垃圾收集器?

垃圾收集器是GC算法的实现方式。

四种垃圾回收方式?

(1)串行垃圾回收(serial)
  JVM参数:-XX:+UseSerialGC
  它为单线程环境设计,且只使用一个线程进行垃圾回收,会暂停所有的用户线程,所以不适合服务器环境。
(2)并行垃圾回收(parallel)
  JVM参数:-XX:+UseParallelGC
  多个垃圾收集线程并行工作,此时用户线程是暂停的,适用于科学计算、大数据处理等和前台交互少的环境。
(3)并发标记清除(CMS)
  JVM参数:-XX:+UseConcMarkSweepGC
  用户线程和垃圾收集线程同时执行(不一定是并行,可能是交替执行),不需要停顿用户线程,这种收集器使用频率最多,
  适用于对响应时间有要求的场景(即,和前台交互多)。
(4)G1垃圾回收(G1)
  JVM参数:-XX:+UseG1GC
  G1垃圾收集器将堆内存分割成不同的区域然后并发的对其进行垃圾回收。

七种垃圾收集器,在不同区域的使用情况?

tip:Serial 串行、Parallel 并行、Concurrent 并发(CMS中的C)

1.用于新生代的收集器有:
  Serial Copying(串行回收)、 Parallel Scavenge(并行回收)、ParNew(并行回收)收集器
2.用于老年代的收集器有:
  Serial Old(是Serial Copying的老年代版本)、Parallel Old(并行回收)、CMS(并行回收)
3.既可以用于新生代,也可以用于老年代:
  G1

在实际生产环境中,常见的组合方式:
  (1)Serial Copying + Serial Old
  (2)Parallel Scavenge + Parallel Old
  (3)ParNew + CMS

JDK 最初的时候采用的是 Serial,为了提高效率产生了 Parallel,后来又为了配合 CMS 诞生了 ParNew

不同垃圾回收器,线程情况

以下我画的图形中,实线表示垃圾回收线程,虚线表示工作线程

3.3.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值