JVM 内存结构

620 阅读7分钟

现在我们知道,一个Java类在经过编译之后,其实大致可以分为class类摘要、常量池、方法栈帧这几部分,并且在jvm启动之后,是通过类加载器将这些类加载到jvm中,那么一个类在加载之后,他是如何可以被使用,可以被调用的呢?

简单介绍 jvm运行过程中会遇到的一些内存部分

我们要知道,jvm在运行过程中,所有的占用,都是直接占用的内存,所以java应用最耗的是内存空间

运行过程

  1. 首先在编译的过程中,如果遇到了引用类型,需要初始化的,会将初始化的对象加载到堆内存中,然后将对象的引用,替换到当前的字段里作为标识
  2. 然后在编译完成之后,会将所有编译后的类信息,存储到元空间中,jdk8之前叫方法区
  3. 当一个线程调用栈(线程)被创建,在当前线程中,每个被调用的方法,都会通过元空间中的信息组成栈帧,让当前线程按顺序调用
  4. 栈帧被创建出来的时候,我们知道其实栈帧的大小、栈的深度等,都已经算好,经过每个栈帧的操作栈数据的压栈以及出栈,出栈之后的当前栈帧被销毁掉,继续调用下一个栈帧,直到调用链结束,结果返回,线程被销毁
  5. 如果某个栈帧中,调用了一些计算等等一些基础方法,虽然方法在java中定义,但实际是由C++等等进行实际计算机资源调用,这些栈帧占用的内存被划分到另外一个区域,叫本地方法栈,除了内存空间是不同的,其他的都和调用栈帧一样
  6. 线程调用栈运行过程中,通过程序计数器来记录下一条指令执行的位置
  7. 如果线程调用栈在运行过程中申请不到足够的空间去创建栈帧,就会抛出内存溢出的异常
  8. 线程调用栈运行过程,也会遇到很多的需要初始化的对象,也会都将初始化之后的对象加载的堆内存中,如果堆内存的对象太多,无法加载新的对象就会抛出内存溢出的异常

使用到的空间

  • 堆内存
  • 元空间(方法区)
  • 线程栈
    • 栈帧
    • 程序计数器

内存空间划分

我们知道,方法的调用都是由线程发起的,我们CPU早已经过了单核的时代,现在的CPU,双核,四核,八核,再加上超线程技术,现在CPU都是可以同时N多线程一起调度,在jvm中也一样,可能一个类中的方法,同时有N个线程在同时在使用。所以jvm将内存部分归类至两类,一类是线程共享内存,一类是线程私有内存

线程共享

堆内存

上边运行过程有说到,jvm从启动开始,所有创建对象,都会存在堆内存中,以java面向对象的这个概念来说,我们都能猜到,jvm绝大一部分内存空间都是被堆内存占用了。所以这部分内存的管理尤为重要,如果我们不停的去创建对象,一直创建,一直存一直存,那有多少内存都是不够用的,所以以这个为基础,jvm不考虑我们如何使用内存,而是自己对堆空间进行了划分,管理。使用垃圾回收的机制,让我们创建的java对象,在不使用的时候,被回收,释放内存,所以产生了整个对象生命周期,从创建到销毁的一个概念

堆内存的划分
  • Young年轻代
    • Eden Space (新生区)
    • Survive From (存活1区)
    • Survive To (存活2区)
  • Old 老年代
为什么做划分?

java对象都是存在堆里,为啥还要把堆里面分成这么多块呢?,其实我的理解这部分主要就是配合jvm 垃圾回收机制进行的划分,为了方便GC算法的使用,简单说一下对象如何在堆划分的区域进行移动(使用标记整理的垃圾回收算法进行说明)

  1. 首先新创建的对象都会放在Eden Space,但是如果对象比较大,会直接放到Old老年代
  2. Eden Space满的时候,会进行youngGC(年轻代GC),将Eden Space还存活的对象复制Survive From中,然后将Eden Space全部清空,然后继续在Eden Space中创建对象
  3. Eden Space又满了,并且Survive From也有存活对象,又会进行youngGC,不过这次有点不一样的是,这次的youngGC,将Eden SpaceSurvive From中存活的对象都复制到Survive To中,然后清除Eden SpaceSurvive From区域
  4. 当下次Eden Space再次满了,并且Survive To也有存活对象,又会youngGC,这次的操作和上次一样,只不过变化的是将Eden SpaceSurvive To中存活的对象都复制到Survive From
  5. 以此进行循环回收,当有些对象在Survive FromSurvive To中循环N次(默认十五次)之后,还存在,就会放入Old老年代
  6. Old老年代内存满了,就会进行Full GC,Full GC会将Old老年代Young年轻代所有的不存活的对象都清除

大致是以上的流程,具体的年轻代如何清除垃圾,老年代如何清除垃圾,下篇文章会详细解释

必知必会
  1. -Xmx -Xms 参数可以设置堆大小,最好给一样的值,否则扩容会耗资源
  2. 堆内存不设置的话,默认是总内存的4分之1
  3. 默认年轻代和老年代占用的比例是 1:2 ,可以通过–XX:NewRatio参数进行设置
  4. 默认 Eden Space、Survive From、Survive To的比例是总年轻代的8:1:1 可以通过–XX:SurvivorRatio参数进行设置
  5. 以上所有介绍都是以java8默认垃圾回收,并行GC进行的解释

元空间

java8之前叫方法区,也叫永久代,和堆使用同一块内存,Java8之后,和堆做了分离,单独拆出来一块内存,专门存放java元数据

必知必会
  1. Java1.7通过 -XX:PermSize和-XX:MaxPermSize 参数设置元空间的大小
  2. java1.8通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize 参数设置元空间的大小
  3. java1.8中,元空间如果不指定大小,默认是无限大

线程私有

线程栈

线程栈就是我们常说的栈,随着线程的创建而创建,这个线程里方法的调用链里的每个方法,都是栈里的栈帧,线程栈里面的所有的变量都是私有的,随着线程结束,整个线程栈进行销毁。我们字节码部分已经详细介绍过栈帧里面的内容,所以不再多说

必知必会
  1. 当线程栈要使用全局共享变量的时候,并不是直接使用,而是先将变量值加载到局部变量中,然后操作完之后,再更新回去,所以线程与线程之间,变量是不可见的,所以多线程的情况下,会遇到很多线程与变量的问题,具体后续会单独文章讲解
  2. 通过-Xss参数可以设置一个线程栈的大小

本地方法栈

本地方法栈和上边线程栈是一样的概念,不同的是本地方法栈调用的一般都是原生方法,通过C++的封装来调用的

程序计数器

程序计数器这部分主要的作用就是记录当前线程的调用信息,只有这部分空间不会有内存溢出的问题,这部分没有做详细了解

总结

这篇文章主要介绍了jvm内存空间的分配,以及在运行时各个空间是如何使用的,为什么这么分配,通过这部分的了解,我们可以对jvm内存结构有一个更深层次的认识。