现在我们知道,一个Java类在经过编译之后,其实大致可以分为class类摘要、常量池、方法栈帧这几部分,并且在jvm启动之后,是通过类加载器将这些类加载到jvm中,那么一个类在加载之后,他是如何可以被使用,可以被调用的呢?
简单介绍 jvm运行过程中会遇到的一些内存部分
我们要知道,jvm在运行过程中,所有的占用,都是直接占用的内存,所以java应用最耗的是内存空间
运行过程
- 首先在编译的过程中,如果遇到了引用类型,需要初始化的,会将初始化的对象加载到
堆内存
中,然后将对象的引用,替换到当前的字段里作为标识 - 然后在编译完成之后,会将所有编译后的类信息,存储到
元空间
中,jdk8之前叫方法区 - 当一个
线程调用栈
(线程)被创建,在当前线程中,每个被调用的方法,都会通过元空间
中的信息组成栈帧
,让当前线程按顺序调用 - 在
栈帧
被创建出来的时候,我们知道其实栈帧的大小、栈的深度等,都已经算好,经过每个栈帧的操作栈
数据的压栈以及出栈,出栈之后的当前栈帧
被销毁掉,继续调用下一个栈帧
,直到调用链结束,结果返回,线程被销毁 - 如果某个
栈帧
中,调用了一些计算等等一些基础方法,虽然方法在java中定义,但实际是由C++等等进行实际计算机资源调用,这些栈帧
占用的内存被划分到另外一个区域,叫本地方法栈
,除了内存空间是不同的,其他的都和调用栈帧
一样 - 在
线程调用栈
运行过程中,通过程序计数器
来记录下一条指令执行的位置 - 如果
线程调用栈
在运行过程中申请不到足够的空间去创建栈帧,就会抛出内存溢出的异常 - 在
线程调用栈
运行过程,也会遇到很多的需要初始化的对象,也会都将初始化之后的对象加载的堆内存
中,如果堆内存
的对象太多,无法加载新的对象就会抛出内存溢出的异常
使用到的空间
- 堆内存
- 元空间(方法区)
- 线程栈
- 栈帧
- 程序计数器
内存空间划分
我们知道,方法的调用都是由线程发起的,我们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算法的使用,简单说一下对象如何在堆划分的区域进行移动(使用标记整理的垃圾回收算法进行说明)
- 首先新创建的对象都会放在
Eden Space
,但是如果对象比较大,会直接放到Old老年代 - 当
Eden Space
满的时候,会进行youngGC(年轻代GC)
,将Eden Space
还存活的对象复制Survive From
中,然后将Eden Space
全部清空,然后继续在Eden Space
中创建对象 - 当
Eden Space
又满了,并且Survive From
也有存活对象,又会进行youngGC
,不过这次有点不一样的是,这次的youngGC
,将Eden Space
和Survive From
中存活的对象都复制到Survive To
中,然后清除Eden Space
和Survive From
区域 - 当下次
Eden Space
再次满了,并且Survive To
也有存活对象,又会youngGC
,这次的操作和上次一样,只不过变化的是将Eden Space
和Survive To
中存活的对象都复制到Survive From
中 - 以此进行循环回收,当有些对象在
Survive From
和Survive To
中循环N次(默认十五次
)之后,还存在,就会放入Old老年代
- 当
Old老年代
内存满了,就会进行Full GC
,Full GC
会将Old老年代
和Young年轻代
所有的不存活的对象都清除
大致是以上的流程,具体的年轻代如何清除垃圾,老年代如何清除垃圾,下篇文章会详细解释
必知必会
- -Xmx -Xms 参数可以设置堆大小,最好给一样的值,否则扩容会耗资源
- 堆内存不设置的话,默认是总内存的4分之1
- 默认年轻代和老年代占用的比例是 1:2 ,可以通过–XX:NewRatio参数进行设置
- 默认 Eden Space、Survive From、Survive To的比例是总年轻代的8:1:1 可以通过–XX:SurvivorRatio参数进行设置
- 以上所有介绍都是以java8默认垃圾回收,并行GC进行的解释
元空间
java8之前叫方法区,也叫永久代,和堆使用同一块内存,Java8之后,和堆做了分离,单独拆出来一块内存,专门存放java元数据
必知必会
- Java1.7通过 -XX:PermSize和-XX:MaxPermSize 参数设置元空间的大小
- java1.8通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize 参数设置元空间的大小
- java1.8中,元空间如果不指定大小,默认是无限大
线程私有
线程栈
线程栈就是我们常说的栈,随着线程的创建而创建,这个线程里方法的调用链里的每个方法,都是栈里的栈帧,线程栈里面的所有的变量都是私有的,随着线程结束,整个线程栈进行销毁。我们字节码部分已经详细介绍过栈帧里面的内容,所以不再多说
必知必会
- 当线程栈要使用全局共享变量的时候,并不是直接使用,而是先将变量值加载到局部变量中,然后操作完之后,再更新回去,所以线程与线程之间,变量是不可见的,所以多线程的情况下,会遇到很多线程与变量的问题,具体后续会单独文章讲解
- 通过-Xss参数可以设置一个线程栈的大小
本地方法栈
本地方法栈和上边线程栈是一样的概念,不同的是本地方法栈调用的一般都是原生方法,通过C++的封装来调用的
程序计数器
程序计数器这部分主要的作用就是记录当前线程的调用信息,只有这部分空间不会有内存溢出的问题,这部分没有做详细了解
总结
这篇文章主要介绍了jvm内存空间的分配,以及在运行时各个空间是如何使用的,为什么这么分配,通过这部分的了解,我们可以对jvm内存结构有一个更深层次的认识。