jvm内存结构总结笔记

jvm组成结构

先来一张图总体看看jvm的组成部分。

在这里插入图片描述
jvm主要由运行时数据区和类加载子系统和执行引擎等组成。
我们重点来总结一下运行时数据区:

1.
      栈为线程私有,每个线程都有自己的独立栈区域空间,主要用于存储局部变量。
在线程运行时,线程的每个方法,虚拟机都会在栈内单独分配一个独立的空间来用于局部变量的作用域表示,这部分独立的空间被称为栈帧。栈帧是基于线程的私有栈内存来进行分配的,例如:线程main中调用了类Class.methodA() 和Class.methodB(),那main线程栈内存就会首先分配一个栈帧1,用来存储Class变量,接着会在main线程栈内存中再分配一个栈帧2区域,用来存储methodA()的局部变量,接着还会在main线程栈内存再分配一个栈帧3区域,用来存储methodB()的局部变量。当methodB()执行完毕,首先释放栈帧3,接着栈帧2,再接着栈帧1,这就是栈的先进后出原则。
      来画一张图解释:
在这里插入图片描述
下面来详细了解一下栈帧的组成部分:
栈帧大致由4个部分组成:
1)局部变量表:用来存储局部变量
2)操作数栈:用来临时存放需要操作计算的对象,操作完成会存入到局部变量表
3)动态链接:栈帧当中会包含有一个执行常量池中当前栈帧所属方法的引用,这个引用一开始是符号引用,java运行时会转成直接引用,用来明确指向一个引用地址,转化的过程就是动态链接
4)方法出口:方法执行之前记录该方法在线程中的位置,方法执行完毕之后,回到对应线程的相应位置

画一张图来看看栈帧结构:
在这里插入图片描述
2.程序计数器(PC寄存器)
          程序计数器为每个线程私有,每个线程在运行期间都会分配一个独立的内存空间,作为jvm指令码对应的地址位置,一般为行号,比如有10行指令,执行到第几行,当前线程的程序计数器的值就是第几。程序计数器的作用是为了多线层并发执行的时候,发生阻塞或者挂起的时候,线程唤醒恢复重新执行的时候可以接着从记录的值开始继续执行指令码。

3.方法区
          在1.7之前叫做永久代,在1.8之后,又叫元空间,一般用于存储常量,静态变量,类元信息(ClassA.class)

4.本地方法栈
          调用本地C语言dll库中的方法,在java中表现为一个本地方法,没有方法体等,一般表现形式为:pricate native void methodXX();

5.
          堆一般存放java对象实例。
首先先来了解一下java对象的组成部分:
1)对象头:分为markword和Klass Pointer两个部分。
        markword:用来存储对象锁,对象运行时的信息,比如GC分代年龄,hashcode等等。
        Klass Pointer:对象本身指向类元数据的指针,用于指向当前对象属于哪个类的实例化。比如:A a1 = new A(); A a2 = new A();在堆中有2个对象实例为new A();他们都会指向A.class类元信息。这个指针就是Klass Pointer.简单的理解为:
在这里插入图片描述

2)实例数据:存放代码中真正字段或者属性的值的内容。

3)对其填充:HotSpot虚拟机要求对象大小必须为8的整数倍,当实例数据不等于8的整数倍,需要对其填充来补全,用不足的空间来补全字节空间。

说完了对象的基本组成,来详细了解一下堆内存的具体组成结构:

堆的组成结构:
1)年轻代
2)老年代

年轻代:分为伊甸区和幸存区,通常用单词eden,survivor区来表示。

survivor区同时有分为2部分,分别为from区和to区

内存分配比例,jvm默认老年代占用堆内存的2/3,年轻代占用1/3,年轻代的eden,from,to分别占用这堆内存1/3总量的比例为:
eden:8/10
from:1/10
to:1/10
举个例子:比如分配堆内存为1000M,那么年老代占用:9002/3=600M;年轻代占用:9001/3=300M,其中eden=3008/10=240M;from=3001/10=30M;to=300*1/10=30M

接下来说一下new一个对象在堆中是如何工作的?
举个例子:
1)A a = new A();
2)那么这个a会存入到线程栈中的局部变量表中,new A()的实例对象instanceA存放在堆中。
3)instanceA对象由于是new出来的,所以一开始会放入eden区;
4)当eden区不断有new对象出来,空间被占满的时候,就会触发minor GC将还存在引用的剩余对象转到from区,同时GC分代年龄+1;
5)当eden区再次被new对象占满,那么这次eden区和from区会同时触发minor GC,将还存在引用的剩余对象从eden区和from区转到to区,同时GC分代年龄+1;
6)当eden区又一次被new对象占满,那么这次eden区和to区同时触发minor GC,将还存在引用的剩余对象从eden区和to区转到from区
7)你会发现,他们会来回从from区和to区切换,这就是垃圾回收的算法之一:复制回收算法。
8)如果GC分代年龄超过15次(可配置),会将该对象实例转到老年代,当老年代空间被占满,那么就会触发full GC
9)当老年代full GC,又没有对象可以回收的时候,也就是老年代空间无法释放的情况,会抛出OOM。
           下面画一些张图来简单理解下:
在这里插入图片描述
顺便总结一下触发gc的条件:

1)Minor gc触发条件:eden区空间占满,触发minor gc

2)major gc触发条件:老年代old区触发gc

3)full gc触发条件:

1、老年代空间不足放不下

2、元空间空间不足放不下

3、代码中调用System.gc直接触发,但是可能不会执行

下面写一个测试类看下jvm变化过程:

先来一个java类:

import com.google.common.collect.Lists;

import java.util.List;

public class Test {

    byte[] arr = new byte[1024*1000];
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) {

        List<Test> list = Lists.newArrayList();
        while (true){
            Test test = new Test();
            test.setName("张三");
            test.setAge(100);
            list.add(test);
            System.out.println("------"+list.size());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们打开java visualVM gc面板:
在这里插入图片描述
根据图片可以看到:
eden区当前分配空间为:30*(1/3)(8/10)=8M
from区当前分配空间为:30
(1/3)(1/10)=1M
to区当前分配空间为:30
(1/3)*(1/10)=1M

重新设置jvm分配堆大小,观测运行一段时候后的情况:
         从图片可以看到,新生的对象都会放入到eden区,当eden区被占满,那么就会触发minor gc,将未清理对象转移到from(S0)区;第二次eden区被占用,再次触发minor gc,这次将eden和from区同时进行回收,将未清理对象转移到to(S1)区;
         以此类推,每次都会同时回收eden区和from或to区,转移到from或to区,也就是从from和to来回复制,清空eden和另外一个,这就是互相复制回收算法。至于如何标记需要被回收的对象,这个以后再写篇文章,大致就是标记引用算法和GC root规范来实现。我们来看一张图:
在这里插入图片描述
##当from和to区也放不下,eden区同时又被占满的情况下,jvm会将还存活的对象转移到老年代old区,老年代old区,老年代如果也被占满了,怎么办?
这里先提到2个概念:
1:major gc:针对old区进行垃圾收集
2:full gc:针对整个堆进行垃圾收集,包括年轻代和老年代
一般情况下他们两个是等价的,也可以指定在full gc之前先触发major gc,区别在于full gc会有较长的stop the world,也就是会对用户所有线程有较长的暂停。

         继续执行,当老年代空间被占满,会触发full gc,但是对象又无法回收的时候,就会出现堆溢出异常:
在这里插入图片描述查看GC日志:

[Full GC [PSYoungGen: 64209K->64002K(75776K)] [ParOldGen: 204229K->204228K(204800K)] 268439K->268231K(280576K) [PSPermGen: 3170K->3170K(21504K)], 0.0117159 secs] [Times: user=0.16 sys=0.00, real=0.01 secs] 
------268
[Full GC [PSYoungGen: 65024K->65003K(75776K)] [ParOldGen: 204228K->204228K(204800K)] 269252K->269231K(280576K) [PSPermGen: 3170K->3170K(21504K)], 0.0036842 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC [PSYoungGen: 65003K->65003K(75776K)] [ParOldGen: 204228K->204199K(204800K)] 269231K->269202K(280576K) [PSPermGen: 3170K->3170K(21504K)], 0.0180723 secs] [Times: user=0.16 sys=0.00, real=0.02 secs] 
**Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at Test.<init>(Test.java:7)
	at Test.main(Test.java:31)**
[Full GC [PSYoungGen: 65024K->0K(75776K)] [ParOldGen: 204228K->1190K(129024K)] 269252K->1190K(204800K) [PSPermGen: 3200K->3200K(21504K)], 0.0123572 secs] [Times: user=0.16 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 75776K, used 1300K [0x00000000f9c00000, 0x00000000ff080000, 0x0000000100000000)
  eden space 65024K, 2% used [0x00000000f9c00000,0x00000000f9d453b8,0x00000000fdb80000)
  from space 10752K, 0% used [0x00000000fdb80000,0x00000000fdb80000,0x00000000fe600000)
  to   space 10752K, 0% used [0x00000000fe600000,0x00000000fe600000,0x00000000ff080000)
 ParOldGen       total 129024K, used 1190K [0x00000000ed400000, 0x00000000f5200000, 0x00000000f9c00000)
  object space 129024K, 0% used [0x00000000ed400000,0x00000000ed529b78,0x00000000f5200000)
 PSPermGen       total 21504K, used 3201K [0x00000000e8200000, 0x00000000e9700000, 0x00000000ed400000)
  object space 21504K, 14% used [0x00000000e8200000,0x00000000e8520678,0x00000000e9700000)
Disconnected from the target VM, address: '127.0.0.1:65185', transport: 'socket'

Process finished with exit code 1

后来我反复测试:发现了一个点,自己记忆一下,当eden区第一次触发minor gc的时候,如果存活对象的大小大于from区,那么jvm就会直接先将对象存入to区装满,然后将剩下的直接放入老年代中。再后面eden区继续发生minor gc,如果from区和to区都放不下,那么就直接放入老年代,from和to区就都处于空闲状态,有兴趣的可以自己去试一试。。
在这里插入图片描述
马上进行一次minor gc,查看结果:
在这里插入图片描述
可以验证上面的话,之后会直接放入老年代,s0和s1一直处于空闲状态;

这里顺带补充一下:
jdk1.8之前,方法区又叫永久代,也就是perm区,字符串常量池和运行时常量池都存放在该区域。
jdk1.8,移除了方法区,改名为元空间,独立出字符串常量池和运行时常量池在元空间
jdk1.8之后,元空间和常量池又是独立开来;

我的另一篇文章,垃圾回收,相关链接:JVM垃圾回收

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值