JVM - java内存结构

1. 描述一下java内存结构

java内存结构主要由5部分组成,包括栈、本地方法栈、程序计数器、堆、方法区(非堆)。其中方法区和堆是线程共用的,注意:这里5部分java内存结构是由jvm规范所规定的,不同的java虚拟机可以采用不同的实现方式,据我所知,一般方法区的实现会由有差异,上面描述的5部分可以如图:

1.1 栈

属于线程本身,里面主要存储了局部变量表和操作数。局部变量表里面存储了基本的类型和对象的引用等。并且局部变量表是由许多slot组成的,在编译的时候slot便定下来数量了。如果栈的深度超过了jvm规定的深度,会抛stackoverflow的异常,如果内存大小小于栈启动线程的大小,便会抛出oom的异常。可以通过-Xss10M,调节方法栈的大小。

1.2 本地方法栈

同栈一样,只是给本地方法用。

1.3 程序计数器

线程上下文切换的时候,记录当前线程执行到的字节码的行号。

1.4 堆

多个线程共用,几乎所有对象都是在堆中创建的,java内存回收的主要区域,java堆一般是不可变的,可以用-xmx和-xms来设置堆的大小。

1.5 方法区

方法区也是共用的,存储类信息、静态变量和常量以及jit编译的字节码信息也存储在这里。并且方法区可以选择不实现垃圾回收。在jdk8后采用元空间替代方法区。

1.6 直接内存

nio中mmap,可以通过直接内存将java内存映射到操作系统的内存上。避免了java堆和Native堆的数据的来回复制。

2.什么是oop-Klass模型

2.1 Klass模型

在理解元空间和运行时常量池之前,我们有必要了解到java的类和对象究竟是如何在内存中存储的。众所周知,java是由C++写的,而在类加载的时候,其实就是将java字节码读入到了内存中。JVM解析字节码,并且生成一个Klass对象。

Klass对象其实就是对一个java类的描述,可以看他的一些属性:

_annotations:保存该类的所有注解 ;

_java_fields_count:已声明的Java字段数量;

_constants:保存该类的常量池指针

......

所以总结出来就是,jvm通过类加载器将字节码文件加载到内存中,并且在元空间生成一个Klass文件,同时基于Klass对象创建一个java的Class对象,并且将静态变量附在Class对象的末尾。

可以看出,Klass对象在元空间,Class对象在堆,静态变量也在堆中。

2.2 oopDesc
 

oopDesc其实就是c++对java对象的描述,里面分成3部分,分别是对象头、实例数据和对象填充。

对象头:对象头分成MarkWord和类的元数据信息指针两部分,MarkWord包括hash码、gc分代年龄、锁状态标志、偏向锁Id等;元数据信息其实就是执行对象所属类的元数据信息。

实例数据:即对象里各种类型字段的内容。

对象填充:对象要求是8的倍数,不是8的倍数的话需要补齐。

再补充一点,java提供了-XX:+UseCompressedOops这个参数,可以让64为地址的寻址压缩成32位,再堆中32位的指针引用在4个字节,但是64位的指针引用占8个字节。这样会导致内存占用更多。所以在64为机器中,可以将对象地址分为堆的基地址+偏移量,将偏移量/8保存到32位地址中。实现了指针的压缩。

3.静态常量池、运行时常量池和字符串常量池

3.1 静态常量池

静态常量池其实就是java字节码的constant pool部分,是一段二进制数据。

3.2 运行时常量池

运行时常量池其实就是将将java字节码中的constant pool加入到内存中在方法区中形成的内存数据。里面主要存储两部分内容,一是符号引用(比如类名、字段名称、方法名),通过symbol Table存储;二是字符串常量池的引用数据,也即String Table。

3.3. 字符串常量池

字符串常量池,其实就是为了让字符串能够重复利用,当新建一个一个字符串中,会首先去堆中字符串常量池查找是否有该字符串,如果有便返回元空间中字符串的引用。所以jvm在缓存字符串的时候,其实是会新建一个String对象,这个对象的地址存储在元空间的String Table中,真正的对象是存储在堆中。所以我们一般任务在jdk1.8后字符串常量池都是存储在堆中的。

3.4 关系

4. 什么是方法区?

4.1 方法区的迭代

jdk1.6时,方法区是其实是堆的一部分,这个时候字符串常量池,运行时常量池和静态变量都是存储在这里面的。这个时候用永久代来实现的方法区,即没有垃圾回收。string的intern方法可以在运行时将字符串放入到字符串常量池中,所以可能会导致oom。

所以在jdk1.7的时候,将字符串常量池和静态变量移入到了堆中.

后来在jdk1.8的时候,直接废弃掉了永久代,采用元空间metaspace来实现方法区.并且元空间是放入到直接内存中的。

4.2元空间

4.2.1.元空间发展历程

栈、方法区、元空间、本地方法栈、方法区都是一个逻辑概念,是jvm规范,即任何虚拟机都有这几部分。但是在具体实现上,可以不同,比如java6的时候,堆分成年轻代,老年代和永久代,而方法区就放在堆的永久代中,永久代没有gc,所以如果字符串常量过多,会导致永久代溢出。以前调节方法区大小其实就是调节永久代的大小,也即-XX:PerGermSize=10M或者-XX:MaxPerGermSize=10M。在java8的时候,字符串常量池和静态变量放入到了java堆中,但是运行时常量池(java的字节码类信息加载到内存中)放入到了元空间中(可以通过以下三个参数改变元空间的信息:)
-XX:MetaSpaceSize=10M,默认是不限制,即直接使用本地内存的空间。
-XX:MaxMetaSpaceSize=10M设置元空间最到大小为10M
-XX:MaxMetaSpaceFreeRatio,默认是70%,表示GC过后空闲元空间最多占比,如果大于该值,元空间便会缩小。
-XX:MinMetaSpaceFreeRatio,默认是40%,如果小于该值,便会扩大元空间。

4.2.2 元空间的组成

元空间分成Klass区和非Non-Klass区,其中Klass主要存储类的元数据信息,Non-Klass区是存储运行时常量池和即时编译器的字节码文件的信息。

1. Klass区

这块区域其实就是前面用来存储java的Klass文件的地方,默认是1G。

有时会通过通过jmap查看jvm内存的时候会看到有一块区域叫ccs,其实就是开启压缩指针过后的Klass区,可以通过-XX:CompressedClassSpaceSize来进行设置。如果开启压缩指针过后,这块区域因为地址是起始地址+偏移量,为了实现这一效果,应该是一片连续的内存。
 

2. no-Klass区

它主要存储的是运行时常量池和本地代码缓存(jit即时编译的代码放在这里)。这是一块不连续的区域。

4.2.3. 触发元空间gc的时机

元空间的超过阈值或者ccs区超过阈值,所以元空间不要设置得太小。并且可以通过MaxMetaSpaceSize来设置元空间的大小。也可以通过CompressedClassSpaceSize设置Klass区的大小,并且jvm运行过后大小便不能更改。

3.5 元空间的内存分配

3.5.1 元空间内存分配架构

1. 元空间每次向操作系统要2Mb大小的Node,这些Node组成一个链表。ccs区是一个很大的区域,所以也把他当成一个1个g的节点
2. 每个Node下面会分成很多chunk,其中chunk可以是1k,4K和64k,根据类加载器需要,划分不同的 chunk,比如bootstrap类加载器便一次分配4m的内存。
3.类加载器每次根据分配的chunk得到他当前需要的资源。

3.5.2 匿名类的处理

由于匿名类的存活时间很短,所以没有必要让他占用元空间的大量时间,所以匿名类的元空间是属于匿名类本身的,而不属于它的类加载器。同时根据类加载器过程,可以看出应该减少小的类加载器的诞生。

5.OutOfMemory和StackOverflow的区别是什么?

5.1 oom

oom其实就是内存超过了可用限制,一般是内存超过了可用的额度,便会抛出oom的异常。通常抛出oom的异常会由以下几种场景。

1. heap space:堆中抛出oom的异常,堆中存在内存泄漏或者大对象的时候,会抛出oom。一般用-Xms和-Xmx设置堆内存大小。并且可以加上参数-XX:+HeapDumpOnOutOfMemoryError,在出现oom的时候能够自动保存堆内存快照。

2. metaspce/pergem space:元空间或者永久代抛出oom的异常,一般是加载很多动态代理的类,或者字符串常量池超过永久代限制的,会抛出oom。可以通过-XX:MetaSpaceSize设置元空间的大小。

3.Native heap:一般是执行native方法导致堆内存不足,会抛出oom。

4.GC overhead Limit Exeeded:没有足够的空间gc会抛出该异常。

5.栈空间也会抛出oom的异常,在某些虚拟机中,栈是可以动态扩展的,如过需要的栈的大小超过虚拟机内存的大小便会抛出oom。注意,hotspot虚拟机是不会动态扩展的,所以只会抛出stackoverflow的异常。

5.2 StackOverflow

StackOverflow主要表示栈溢出,当方法调用超过栈的深度的时候,就会抛出StackOverFlow。可以通过-Xss设置栈的大小。

6.对象分配的优化-TLAB

1.什么是TLAB

TLAB是虚拟机在堆内存的eden空间中划分出来的一块线程专属的区域,它是线程安全的。当对象分配的时候,如果jvm开启了jit,首先会考虑在栈上分配。如果在栈上分配失败后,便会在这里分配。

2.为什么要有TLAB

TLAB其实就是利用空间解决对象分配的并发问题。由于每个线程有自己单独的对象分配空间,所以不存在线程共享的问题(堆内存一定是对象共享的这一说法其实是不严谨的)。TLAB可以看做是给每个线程划分了一个界限,圈住的这一部分区域,一定是独属于线程的。所以在这部分上分配的对象到达gc分代年龄过后也可能加入到老年代。并且TLAB空间满后,便会申请的新的区域,但是以前分配的对象地址并不会改变。

3.TLAB的优缺点以及解决办法

如果待分配对象需要的内存空间超过了TLAB的剩余空间的时候,可以通过设置一个"最大浪费空间"的值。如果待分配对象的空间超过"最大浪费空间",便会直接在堆中分配内存;否者,便会重新分配一个TLAB,并且分配内存。

参考资料

1.周志明.深入理解java虚拟机

2.OutOfMemory和StackOverflow的区别是什么 

3.深入理解堆外内存 Metaspace 深入理解堆外内存 Metaspace_Javadoop

4.https://www.bilibili.com/read/cv13294155/] https://www.javadoop.com/post/metaspace

  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
JVM8的内存结构图如下所示:\[1\] - 程序计数器 - 虚拟机栈(JVM Stack) - 本地方法栈 - 元空间(MetaSpace) - Java堆(Heap) 其中,程序计数器用于记录当前线程执行的字节码指令的地址;虚拟机栈用于存储方法调用的局部变量、操作数栈、动态链接、方法出口等信息;本地方法栈用于支持本地方法的调用;元空间用于存储类的元数据信息,取代了JDK1.8之前的永久代(PermGen);Java堆用于存储对象实例和数组。 此外,JVM8还有直接内存,它是独立于JVM内存之外的内存,可以直接和NIO接口交互,提升了程序性能。\[2\] 在Java内存中,内存需要划分成新生代和老年代。新生代又分为eden、from和to三块区域,默认比例是8:1:1。每次创建对象时,对象会先存储到eden区域,当eden区域满了后,会触发minor GC回收该区域,未回收的对象会放入from或to区域。每经过一次GC,from和to两块空间的对象会进行一次移动,未回收的对象年龄也会增加1。当对象年龄达到一定阈值(默认为15岁),就会被晋升到老年代。当老年代满了时,会触发Full GC回收。如果堆内存不足,就会出现OutOfMemoryError。可以通过配置JVM参数(如-Xmx)来设置最大堆内存大小。\[3\] #### 引用[.reference_title] - *1* [JVM内存结构详解](https://blog.csdn.net/weixin_42173451/article/details/105805231)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [最简单的JVM内存结构图](https://blog.csdn.net/duyabc/article/details/114679595)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值