Java内存与垃圾回收篇(本地方法栈堆方法区)
Posted 狗哥狗弟齐头并进
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java内存与垃圾回收篇(本地方法栈堆方法区)相关的知识,希望对你有一定的参考价值。
Java内存与垃圾回收篇(完整)
1 本地方法接口
1.1 什么是本地方法?
一个Native Method就是一个Java调用非Java代码的接口。本地方法的实现不是基与Java而是基于C或者C++;
作用: 是为了融合不同的编程语言为Java所用,它的初衷是融合C++程序。
1.2 为什么要使用本地方法呢?
应用需求: 有时候Java应用需要与Java外面的环境交互,我们开发一般都是面向的Spring框架,很少有人去关注JVM底层。而底层最终还是需要与操作系统或者某些硬件进行信息的交换。本地方法只提供接口,无需了解其繁琐的实现。
与操作系统交互:Java支持者Java语言本身和运行时,是Java程序生存的平台。说到底JVM并不是完整的系统,还需要底层操作系统的h支持。
通过使用本地方法,我们得以用Java实现jre的与底层系统的交互,甚至JVM的一些部分就是用C写的。
2 本地方法栈
首先明确一点就是当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的师姐。它和虚拟机拥有同样的全新啊,
- 本地方法可以通过本地方法接口来访问虚拟机内部运行时数据区
- 可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
- 本地方法栈用来管理本地方法的调用
- 是线程私有的,而且是用C语言实现的
在HotSpot虚拟机中,本地方法栈与虚拟机栈合二为一叫做栈。
3 堆(重点)
3.1 堆的核心概述
一个JVM实例只存在一个堆内存,堆是Java内存管理的核心区域。Java堆区在JVM启动国得时候就呗创建了,空间大小可以自己通过参数设置(继续阅读)。堆可以处于物理上不连续的内存空间中,但是在逻辑上应该被视为连续的。所有的线程是共享Java堆的,其中一个重要的概念是TLAB.
*几乎所有的对象实例都在堆中分配内存。
数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或数组在堆中的位置。
对象在使用完是通过垃圾回收机制移除的。
最重要的是堆是GC执行垃圾回收的重点区域。
3.2堆的内存细分
如图:
Java 7之前分为:新生代(Eden区和s0,s1区)+老年代+永久代
Java8以及之后为:新生代(Eden区和s0,s1区)+老年代+元空间。
3.3 设置堆的内存大小与OOM
3.3.1 设置堆内存大小
-Xms 10M 表示初始内存 等价于-XX:initislHeapSize
-Xmx 20M 表示最大内存 等价于-XX:MaxHeapSize
这俩参数放在一起使用,表示初始化堆内存为10M,最大堆内存为20M
但是默认情况下,初始内存大小为物理内存大小的六十四分之一。最大内存为物理内存大小的四分之一。
开发中建议把初始堆内存和最大堆内存设置成相同的值。
3.3.2 OOM (OutOfMemory)
OOM值得是内存溢出,具体是哪里溢出会在错误信息中爆出来。
public class OOM{
public static void main(String args[]){
Arraylist<Picture> list =new ArrayList<>();
while(true){
list.add(new Picture(new Random().nextInt(1024*1024)))
}
}
}
无限制的往堆内添加数据,会导致堆内存溢出。
3.2 年轻代与老年代
存储在JVM中的Java对象可以被划分为两类:
1、生命周期较短的瞬时对象,这类对象的创建和消亡很迅速。
2、生命周期很长,很有可能与JVM的周期一样。
Java堆区如果进一步细分的话,可以划分为年轻代与老年代
年轻代又可以划分为:Eden空间、Survivor0、Survivor1区。或者是From to区,主要是描述垃圾回收的过程从哪到哪。
年轻代是垃圾回收很频繁的区域,因为年轻所以存放的是一些生命周期较短的,根据垃圾回收机制,生命周期较长或者对象很大都被移入老年代空间了。
通过参数-XX:NewRation=?可以配置新生代与老年代的占比。默认是1:2
在新生代中,Eden区与S0,S1区的比例是 8:1:1.
因为新生代是存放生命周期短的对象,所以几乎所有的Java对象都是在Eden区被new出来的。但是对于占据空间很大的大对象,他们的创建是在老年代。同样的Eden区也是进行垃圾回收最频繁的地方。基本满足“朝生夕死”。
-Xmn设置新生代的空间的大小 优先级高 但是一般不设置
3.4 对象的分配过程
为新对象分配内存是意见非常严谨和复杂的任务,JVM的设计者门不仅需要考虑内存如何分配,在哪里分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存中是否存在内存碎片。
对象的分配有以下几个步骤:
- new的对象先存放在Eden区。此区大小有限制。
- 当Eden区的空间填满时,程序需要创建对象,JVM的垃圾回收器将堆Eden区进行垃圾回收,此时垃圾回收为Minnor GC ,将伊甸园区的不再被使用的对象的引用进行i笑傲会,再加载新的对象Eden区。
- Minor Gc之后,将Eden区的剩下的对象移入到S0区。
- 如果再此触发GC,那么Eden区gc剩下的对象和S0区Gc生下来的对象被存放再S1区。那么如果再出现GC就会相同的方法从S1到S0.
- 那么问题来了,老年代是干啥的?啥时候判断一个对象老了。可以设置参数-XX:MaxTenuringThreshold= 进行设置。默认为15次GC。
- 老年代的垃圾回收没有那么频繁。当老年代进行垃圾回收的时候会触发Major GC,如果当Major GC之后发现依然无法进行对象的保存就会触发OOM异常。
语言描述此过程:
- 新new的对象是在伊甸园区,但是当伊甸园区满的时候,YGC表示年轻的垃圾回收机制就会判断哪一个对象为垃圾,哪一个对象不是垃圾。图中红色为垃圾,绿色不是垃圾。然后将绿色的放入S0中,此时提供一个年龄计数器,并且设置其年龄为1。
- 重复伊甸园区的满步骤,当伊甸园区又满了的时候,又会将不为垃圾的对象放到to空间。如果刚才在S0的对象还需要使用就方法在S1。然后S0就空了,往复循环,谁空谁就是to空间。
- 特殊情况:当循环次数大于15 当年龄大于15的时候,就从年轻代进入老年代区。 可以通过-XX:MaxTenuringThreshold=设置。
幸存者区满的时候不触发垃圾回收机制,当伊甸园区满的时候会同时对伊甸园区和幸存区进行垃圾回收。
对于对象很大的情况,需要方法老年代,当老年代也放不下的时候就及逆行GC,MGC和FGC如果都放不下,就报OOM异常。
3.5 MinorGC、MajorGC、 FullGC
首先要明确一点是,JVM虚拟机并非每次进行GC的时候,都对上面三个内存区域一起回收,大部分时候回收的都是指新生代。
针对HotSpot虚拟机,它里面的GC按照回收区域又分为两大类型,一种是部分收集,一种是整堆收集。
部分收集:不是完整收集整个Java堆的垃圾收集。其中分为:
新生代收集(Minor GC/ Young GC):只是新生代的垃圾收集
老年代收集(Major GC/ Old GC):只是老年代的垃圾收集
目前只有CMS 才会进行单独收集老年代的行为,
目前只有G1 垃圾回收器会进行混合收集。
整堆收集:Full GC收集震哥哥Java堆和方法区的垃圾收集。
3.6堆空间分代思想
为什么要进行Java堆分代,不分代就不能正常工作了吗?
- 研究表面,不同对象的生命周期不同。70%-99%的对象是临时对象。
- 不分代也是完全可以得,分代得唯一理由就是优化GC新跟那个。如果不分代,那所有得对象都在一块,就如同把一个学校得人都关在一个教室。GC得时候要找到哪些对象没用,这样就会堆得所有区域进行扫描。而很多对象都是很快消亡得。如果分代,把新创建得对象放到某一地方,当GC得时候先把这块存储“朝生夕死”对象区域进行回收,这样就会腾出很大得空间出来。
3.7 TLAB与内存分配测量
如果对象在Eden区出生并经过第一次GC之后后仍然存活,如果S0区还有空间那么就将对象移动到S0区,并将对象年龄设为1.对象在S区中没熬过一次GC年龄就加1,当年龄增加到我们自己设置的值,或者是15岁得时候,就会被晋升为老年代,
-XX:MaxTenuringThreshold
TLAB存在得意义是什么?
TLAB(Thread Local Allocation Buffer)
- 堆区是线程共享区域,任何线程都可以访问到堆区中得共享数据
- 由于对象实例得创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全得。
- 为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。
TLAB是什么?
- 从内存模型而不是垃圾收集得角度,对Eden区继续进行划分,JVM为每个线程分配了一个私有得缓冲区,包含在Eden空间。
- 读线程同时分配内存得时候,使用TLAB可以避免一些列的非线程安全问日,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配反方式称为快速分配策略。
- JVM将TLAB作为内存分配的首选,
- 通过-XX:UseTLAB 设置是否开启TLAB空间
- 默认情况下,TLAB空间的内存非常小,仅栈整个Eden空间的1%,当然我们可以通过-XX:TLABWasteTargetPercent设置TLAB占用Eden空间的百分比。
- 一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。
对象分配图解如下:
TLAB是堆中存在的每一个线程所私有的小空间。
4 方法区
4.1栈、堆、方法区的交互关系
Person person =new Person();
Person 在方法区生命对象类型数据
person 是栈上的引用指向堆空间 的new Person()实体
new Person()在堆空间中开启内存。
4.2 方法区的基本理解
- 方法区与Java堆一样,是各个线程共享的内存区域。
- 方法区在JVM启动的时候被创建,它的实际物理内存与Java堆中的一样都可以是不连续的。
- 方法区的大小也是可以被四肢的。
- 方法区的大小决定了系统中可以保存多少个类,如果定义了太多的类,导致方法区溢出,则虚拟机同样会抛出内存溢出的错误。
- 关闭JVM就会释放这个区域的内存。
JDK8 之后方法区被替换成元空间,元空间是存储在本地内存中,意味着只要本地内存够大就不会出现OOM。
4.3 方法区的内部结构
方法区中包含了两部分,一部分是类型信息,一部分是运行时常量池字符串常量。
类型信息:
对每个加载的类型(类class、接口、枚举、注解),JVM都必须在方法区中存储以下类型信息。
- 这个类型的完整有效名称(包名.类名)
- 这个类型直接父类的完整有效名
- 这个类型的修饰符(public,abstract,final的某个子集)
- 这个类型直接接口的一个有序列表
此外jvm还需要保存field与方法的一些信息。
运行时常量池与常量池对比
- 常量池是存放在字节码文件中,运行时常量池是在方法区内部
常量池中有什么?
常量池中存放的是:数量值,字符串值,类引用,字段引用和方法引用。
运行时常量池是什么?
- 运行时常量池是方法区的一部分。
- 常量池表是Class文件的一部分,用于存放编译器生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。
- 运行时常量池,在加载类和接口到虚拟机之后,就会创建对应的运行时常量池。
- JVM为每个一家在的类型都维护一个常量池,池中的数据可以通过索引访问。
- 运行时常量池存放的是真正的地址,而不是常量池中的字段引用,
4.4 方法区的演进细节
首先明确:只有HotSpot才有永久代。
静态变量存放在哪里?
- jdk 6 jdk7 jdk 8中开辟的静态变量数组实体存放在老年代中。 new的东西都在堆空间中。
- 只要是对象实例必然会在Java堆中分配。
- 这里静态变量指的是静态变量名
String Table为什么要调整?
jdk7 中将StringTable放在了堆空间,因为永久代回收效率很低,只有进行fullGC才会,full GC是老年代空间不足、永久代不足时才会触发,这就导致StrignTable回收效率不高。开发中有大量的字符串被创建,回收效率低,导致永久代内存不足。放到堆里,能及时回收内存。
以上是关于Java内存与垃圾回收篇(本地方法栈堆方法区)的主要内容,如果未能解决你的问题,请参考以下文章