前面说了jvm的概念,不太理解的朋友可以看看我上一篇博客,不过jvm也是比较复杂的一个东西,想深入了解看一两篇文章是不够的,可以去看看《深入理解java虚拟机》,这里我带大家来康康jvm的内存结构,中间不乏字数较多熬,毕竟是概念性的东西,嘿嘿嘿
首先我们得知道,jvm运行时数据区域分为五大部分:堆,java虚拟机栈,本地方法区,程序计算器,方法区(jdk1.8版本后叫做元区间,在jdk1.8版本后位置移到了本地内存)。
不知道大家看到这五个名词第一反应是什么,但是我第一次看到我直接阿巴阿巴,这啥啊,都是干啥的啊,随后我找了一些资料,也有了一些了解。
堆:该区域是线程共享的,主要用于用于存放对象实例。绝大多数创建的对象都会被存放到这里(除了部分由于逃逸分析而在对外分配的对象,该部分只是在方法体被引用,故被分配到了栈上)。
堆这块区域也是jvm虚拟机中占用最大的,因为我们写项目会写很多类,它们都被放在堆里面,如果java堆空间不足了,程序会抛出OutOfMemoryError异常。因此也是垃圾回收的主要目标。
而堆也划分了几个区域:年轻代(young)、老年代(old)、永久区(permanent 对HotSpot虚拟机而言,JDK1.8之后为metaspace(元区间)替代永久代)。
永久代
JDK1.8之后为metaspace(元区间)替代永久代,它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且HotSpot也有取消永久代的趋势,在JDK 1.7中HotSpot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
老年代
在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。
上面也说了,堆内存是jvm中管理的内存中最大的,也是垃圾回收器最主要的目标,我们所有的对象实例都存放在堆当中,给堆内存分代其实是为了提高对象内存分配和垃圾回收的效率,如果我们不进行区域划分,所有新创建的对象和生命周期很长的对象存放在一起,随着程序的执行,我们需要频繁的对堆内存进行垃圾回收,而每次回收都要遍历所有的对象,遍历这些对象所花费的时间代价是巨大的,这很影响我们程序的性能!
有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行GC,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是内存分代带来的好处。
看到上面我粗糙的绘制的图,年轻代的堆也划分了区域,分别是Eden,form survivor,to survivor。
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。
HotSpot将新生代划分为三块,一块较大的Eden(伊甸)空间和两块较小的Survivor(幸存者)空间,默认比例为8:1:1。划分的目的是因为HotSpot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象除外,大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
复制算法回收新生代的过程:
GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
重点:
如果出现java.lang.OutOfMemoryError: Java heap space异常,说明是Java虚拟机的堆内存不够。原因有二:
1、Java虚拟机的堆内存设置不够,可以通过参数-Xms、-Xmx来调整。
2、代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)。
指路设置参数-Xms、-Xmx:https://jingyan.baidu.com/album/624e7459653ca534e8ba5a26.html?picindex=3
如果出现java.lang.OutOfMemoryError: PermGen space,说明是Java虚拟机对永久代Perm内存设置不够。原因有两个:
1、程序启动需要加载大量的第三方jar包。例如:在一个Tomcat下部署了太多的应用。
2、大量动态反射生成的类不断被加载,最终导致Perm区被占满
每个线程一块,指向当前线程正在执行的字节码代码的行号。如果当前线程执行的是native方法,则其值为null。
该区域也是线程共享的,该区域也是每个线程所独有的,每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈和出栈。
某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。
**每当线程调用一个Java方法时,虚拟机都会在该线程的Java栈中压入一个新帧。而这个新帧自然就成为了当前帧。**在执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等数据。
如果java栈空间不足了,程序会抛出StackOverflowError异常
Java方法可以以两种方式完成。一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管以哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的帧就成为当前帧了。
Java帧上的所有数据都是此线程私有的。任何线程都不能访问另一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。
堆内存与栈内存需要说明
基础数据类型直接在栈空间分配,方法的形式参数,直接在栈空间分配,当方法调用完成后从栈空间回收。引用数据类型,需要用new来创建,既在栈空间分配一个地址空间,又在堆空间分配对象的类变量 。方法的引用参数,在栈空间分配一个地址空间,并指向堆空间的对象区,当方法调用完成后从栈空间回收。局部变量new出来时,在栈空间和堆空间中分配空间,当局部变量生命周期结束后,栈空间立刻被回收,堆空间区域等待GC回收。方法调用时传入的literal参数,先在栈空间分配,在方法调用完成后从栈空间收回。字符串常量、static在DATA区域分配,this在堆空间分配。数组既在栈空间分配数组名称,又在堆空间分配数组实际的大小。
这是官方的说明,,,
其实,说明白一点,看图吧!
我只做了new方法和直接使用基础数据类型的,引用数据类型其实和new方法的差不多叭,如果使用数组来定义一串字符,即在栈内存分配一个地址空间,在堆内存分配对象区,,,而方法传递的参数,在栈空间分配完后方法调用结束从栈空间收回
这个区域是每个线程锁独有的,主要用于VM的Native方法(本地方法)。这部分是有VM自行管理,程序员基本上不需要关系该部分。
方法区是各个线程共享的区域,存放类信息、常量、静态变量、即时编译器编译后的代码等,垃圾回收器对这块区域的回收主要是针对常量池和类。
还是那句话,看一些文章只能了解jvm的一个大概,如果想深入了解,看书比较好哦,阿巴阿巴,这两天看完jvm这几个知识点,头发又掉了不少呢,个人觉得堆和栈比较复杂,但是理解完也还好哦!献丑了啊哈哈哈有什么不足欢迎指点!一定会改正!有问题也可以留言评论哇,不过可能我不会嘿嘿嘿
最后附上一张用来学习的画图,比我画的清楚一点(caimeiyou):
https://www.processon.com/view/link/5b891cc9e4b0fe81b620b2c0