Java Jvm内存区域定义介绍

本文主要用于个人笔记记录,主要针对jdk1.8

一、Java内存区域(运行时数区)

图片这X掉的是方法区,方法区是JVM的规范,大家可能会搞混永久代和方法区,其实永久代就是Jdk 1.8以前 HotSpot对方法区的实现。

 (图片取自java guide)

直接内存是非运行时数据区的一部分。

Java 内存可以粗糙的区分为堆内存(Heap)和栈内存 (Stack)。栈内存大多指的是虚拟机栈中局部变量表部分。

 

二、线程私有内存区域

先看看线程内的内存区域:

1.程序计数器:用于记录下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。为了线程切换后能恢复到正确的执行位置,每个线程的程序计数器都是独立的,这部分线程独立的内存区域称为线程私有。

注意:程序计数器是唯一一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡

2.Java 虚拟机栈:生命周期和线程相同,描述的是 Java 方法执行的内存模型,每次方法调用的数据都是通过栈传递的。

    虚拟栈存着一个个栈帧,栈帧包括:局部变量表、操作数栈、动态链接、方法出口信息。

    局部变量表,从名字显而易见,是存放执行方法的时候变量的信息,所以主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

方法/函数的调用,就是创建相应函数的栈帧压入Java栈,调用结束后,就会有栈帧弹出。

     虚拟机栈主要是存放线程执行方法的一个个栈帧,栈帧占用栈多少空间是根据具体方法的参数等信息决定的,虚拟机栈有可能出现StackOverFlowError 和 OutOfMemoryError

     每个线程的栈大小,通过-Xss设置,公司内默认为256k,相同物理内存下,-Xss小,则能生成更多的线程,但操作系统对一个进程的线程是有限制的。-Xss过小,会出现栈溢出,特别是有递归,大的循环的时候;-Xss过大,影响创建栈的数量,如果是多线程应用,会出现内存溢出。

  • StackOverFlowError: 若 Java 虚拟机栈的内存大小不允许动态扩展(Hotspot不允许),那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: Java 虚拟机栈的内存大小可以动态扩展(以前的Classic虚拟机允许), 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

3.本地方法栈

和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native (一些c、c++代码编写的代码调用操作系统指令)方法服务。在 HotSpot 虚拟机中本地方法栈和 Java 虚拟机栈合二为一。

异常抛出一样会StackOverFlowError 和 OutOfMemoryError,栈帧也同样是用于存放本地方法的局部变量表、操作数栈、动态链接、出口信息。

 

三、线程共享区域

1.堆

用于存放对象实例,“几乎”所有的对象实例以及数组都在这里分配内存。

但是,随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,JDK1.7起默认开启逃逸分析,如果某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),那么对象可以直接在栈上分配内存,就不会再分配到堆里。

Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Garbage Collected Heap)。

从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间。进一步划分的目的是更好地回收内存,或者更快地分配内存。 

JDK 8后PermGen(永久代)已被 Metaspace(元空间) 取代,元空间使用的是直接内存。以前永久代用的是堆内存空间。

大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁)或者累积的某个年龄对象数量大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值;这些对象就会被晋升到老年代中。对象晋升到老年代的年龄阈值,通过参数 -XX:MaxTenuringThreshold 来设置。

对象流转

对象年龄:0             取对象年龄超过一半的数和MaxTenuringThreshold最小值作为迭代到老年代的阈值并修改         对象年龄:MaxTenuringThreshold

Eden         ->             S0 或者 S1(看现在标记整理清除使用的是哪个新生代)                                                   -> 老年代(Tenured)

堆最容易出现OutOfMemoryError错误。但有好几种:

 1.java.lang.OutOfMemoryError: GC Overhead Limit Exceeded:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。

2.java.lang.OutOfMemoryError: Java heap space :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。(和配置的最大堆内存有关,且受制于物理内存大小。最大堆内存可通过-Xmx参数配置。默认值看VM配置,-client取小于物理内存的 1/4 或 1GB,有的版本不受1GB影响;可以用这个命令查看java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize', -XX:+PrintConmandLineFlags也可以但要看具体jvm。

 

2.方法区(元空间)

用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。注意:永久代就是 HotSpot 虚拟机对虚拟机规范中方法区的一种实现方式。而jdk1.8后是元空间。

jdk1.8后用这两个命令设置元空间大小,与永久代很大的不同就是,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。

-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)

-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小

为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?

《深入理解Java虚拟机》第三版2.2.5提到

 

1.整个永久代有一个 JVM 本身设置的固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,虽然元空间仍旧可能溢出,但是比原来出现的几率会更小。溢出时会报错:java.lang.OutOfMemoryError: MetaSpace

2.可以使用 -XX:MaxMetaspaceSize 标志设置最大元空间大小,默认值为 unlimited就意味着它只受系统内存的限制。-XX:MetaspaceSize 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。

3.在 JDK8,合并 HotSpot 和 JRockit 的代码时, JRockit 从来没有一个叫永久代的东西, 合并之后就没有必要额外的设置这么一个永久代的地方了。 

 

3.运行时常量池

方法区一部分。存在方法区的类存储(包括类的版本、字段、方法、接口等描述信息、常量池表)。存放编译期生成的各种字面量和符号引用。

同样因为在方法区中,当运行时常量池无法申请到内存时,会抛出java.lang.OutOfMemoryError。

但字符串常量放到了堆中。

JDK1.8版本的字符串常量池中存的是字符串对象,以及字符串常量值。

几个问题:

1.那么JVM 常量池中存储的是对象还是引用呢?字符串常量池呢?

 运行时常量池其中的引用类型常量(例如CONSTANT_String、CONSTANT_Class、CONSTANT_MethodHandle、CONSTANT_MethodType之类)都存的是引用,实际的对象还是存在Java heap上的。字符串常量池则看情况而定。如果使用StringBuilder.append先创建堆对象,后面再intern,则是把常量池中的对象指向堆对象,常量池中也是个引用,普通的情况,字符串常量池是存了一个真的对象。

 

2.String s = new String("abc")这个语句创建了几个对象?

创建了2个对象,第一个对象是”abc”字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象(对象存储的就是字符常量池的引用),s指向的就是堆中这个String对象。

详细见此文章:https://blog.csdn.net/wangwenjie1997/article/details/108325863

 

3.如果是用"+"拼接的字符串呢?

"+"会被替换为采用了StringBuilder进行加号的拼接,只会在堆中创建一个String对象,并不会在常量池中存储对应的字符串。而“+”创建完字符串后,再调用intern做池化,因为JDK7之后对字符串常量池放到了堆中,所以当intern调用时,如果常量池没有这个字符串,就会在常量池中存了堆中对应字符串的引用。

即:当字符串常量池中并不存在对应字符串时,调用intern方法返回的地址为堆中对象的地址。

 

4.String.intern()方法流程?

String类的intern()方法:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果字符串常量池池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象(注意是常量池中的对象,不是堆中的对象)的引用。 对于任意两个字符串s和t,当且仅当s.equals(t)为true时,s.intern() == t.intern()才为true。所有字面值字符串和字符串赋值常量表达式都使用 intern方法进行操作。
1.7后使用intern不同的地方是,如果存在堆中的对象,字符常量池会直接保存这个堆中对象的引用,而不会重新创建对象。
详细的String.intern作用描述可以看该文章https://www.cnblogs.com/yrjns/p/12507892.html。
 

5.那么的String.intern如何做到节省内存呢?

比如一个10w次的循环,new String(String.valueOf(sample[i % sample.length])),循环中如果字符串有重复的时候,new String创建都会判断常量池是否有该字面量对象,如果没有,都会创建一个,再创建一个堆中的字符串对象;也就是一直会在堆中创建一个对象指向字符串常量池的字符串对象。但如果用了new String(String.valueOf(sample[i % sample.length])).intern();new String().intern实际上如果字符串常量池有这个字符串对象,就会直接返回常量池的对象引用给到变量,这个过程是不会在 Java 堆中再创建一个 String 对象的。

 

4.直接内存

直接内存并不是虚拟机运行时数据区的一部分,也不是虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用。也可能导致 OutOfMemoryError 错误出现。

JDK1.4 中新加入的 NIO(New Input/Output) 类,引入了一种基于通道(Channel)与缓存区(Buffer)的 I/O 方式,它可以直接使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样就能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆之间来回复制数据。本机直接内存的分配不会受到 Java 堆的限制,但是,是内存就会受到本机总内存大小以及处理器寻址空间的限制。

 

posted @   klm-kain  阅读(70)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示