Java内存与垃圾回收篇(本地方法栈堆方法区)

Posted 狗哥狗弟齐头并进

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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执行完内存中是否存在内存碎片。

对象的分配有以下几个步骤:

  1. new的对象先存放在Eden区。此区大小有限制。
  2. 当Eden区的空间填满时,程序需要创建对象,JVM的垃圾回收器将堆Eden区进行垃圾回收,此时垃圾回收为Minnor GC ,将伊甸园区的不再被使用的对象的引用进行i笑傲会,再加载新的对象Eden区。
  3. Minor Gc之后,将Eden区的剩下的对象移入到S0区。
  4. 如果再此触发GC,那么Eden区gc剩下的对象和S0区Gc生下来的对象被存放再S1区。那么如果再出现GC就会相同的方法从S1到S0.
  5. 那么问题来了,老年代是干啥的?啥时候判断一个对象老了。可以设置参数-XX:MaxTenuringThreshold= 进行设置。默认为15次GC。
  6. 老年代的垃圾回收没有那么频繁。当老年代进行垃圾回收的时候会触发Major GC,如果当Major GC之后发现依然无法进行对象的保存就会触发OOM异常。

语言描述此过程

  1. 新new的对象是在伊甸园区,但是当伊甸园区满的时候,YGC表示年轻的垃圾回收机制就会判断哪一个对象为垃圾,哪一个对象不是垃圾。图中红色为垃圾,绿色不是垃圾。然后将绿色的放入S0中,此时提供一个年龄计数器,并且设置其年龄为1。
  2. 重复伊甸园区的满步骤,当伊甸园区又满了的时候,又会将不为垃圾的对象放到to空间。如果刚才在S0的对象还需要使用就方法在S1。然后S0就空了,往复循环,谁空谁就是to空间。
  3. 特殊情况:当循环次数大于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)

  1. 堆区是线程共享区域,任何线程都可以访问到堆区中得共享数据
  2. 由于对象实例得创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全得。
  3. 为了避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。

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内存与垃圾回收篇(本地方法栈堆方法区)的主要内容,如果未能解决你的问题,请参考以下文章

Java 虚拟机

Java中的内存分配与垃圾回收

Java虚拟机垃圾收集器与内存分配策略

JVM 垃圾回收机制

详解JVM 的垃圾回收算法和垃圾回收器

jvm 深入理解自动内存分配与垃圾回收