Java虚拟机jvm

jvm

Java内存区域

运行时数据区域

在这里插入图片描述
程序计数器:当前线程执行的字节码的行号指示器。如果线程执行的时Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是native方法,则为空。每条线程都有一个独立的程序计数器,是存在于线程的私有内存中的
虚拟机栈:描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈桢用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程,就对应一个栈桢在虚拟机栈中入栈到出栈的过程。也是线程私有的。其中的局部变量表所需的内存空间是在编译期就确定的
本地方法栈:native方法服务的栈。也是线程私有的。
:存放对象实例。是垃圾收集器管理的主要区域。可细分为新生代、老年代;堆中还可能划分出多个线程私有的分配缓冲区(TLAB)

  • 1 字符串常量池:是Java堆内存中的一个特殊存储区域,用于存储字符串字面量。当你创建一个字符串字面量时,JVM会首先检查字符串常量池中是否已存在相同的字符串。如果存在,则不会创建新的字符串对象,而是返回对该字符串的引用。如果不存在,则会在字符串常量池中创建一个新的字符串对象,并返回其引用。
    需要注意的是,字符串常量池仅存储字符串字面量。对于通过new操作符创建的字符串对象,它们不会在字符串常量池中存储,而是存储在Java堆中的普通对象区域。这些对象即使内容相同,也是不同的对象,它们的引用不相等。

String a=“zifuchuan”,后面的“zifuchuan”就叫字符串字面量。

方法区:存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据
,又称为永久代(不应该这样叫)

-XX:ReservedCodeCacheSize

  • 2 运行时常量池:是方法区的一部分,class文件中有一项信息是常量池,用于存放编译期生成的各种字面量和符号引用;具备动态性,即运行期间才将新的常量放入池中。
  • 3 方法区的常量池(又名class文件常量池):class类文件的一部分,可以看做一张表,它包含了类在编译期生成的各种字面量(literal)和符号引用(symbolic references)。常量池可以看做一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等类型。例如,类名、方法名、接口名、文本字符串、final修饰的常量等。当类被虚拟机加载到内存后,JVM就会将class常量池中的内容集中存放到一块内存,这块内存就是运行时常量池,并且把里面的符号地址变为真实地址。

在jvm中,对于类加载时的静态常量,字符串常量存储在堆的字符串常量池,非字符串常量存储在方法区的常量池。

直接内存:不是虚拟机运行时数据区的一部分。NIO中,使用Native函数库直接分配堆外内存,然后通过一个在Java堆中的DirectByteBuffer对象作为引用进行操作,避免了在Java堆和Native堆中来回复制数据,提高了性能

java8的改动-元空间meta space
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小
-XX:MetaspaceSize设置元空间的初始大小
-XX:MaxMetaspaceSize设置元空间的最大大小

-Xmx和-Xms只是针对堆内存的分配,其他内存区域虚拟机栈、本地方法栈和程序计数器的大小和分配是由JVM根据运行时情况自动管理的。

直接内存
直接内存,也称为堆外内存,不是JVM运行时数据区的一部分,它通过java的NIO类进行分配和管理。
直接内存大小可以通过-XX:MaxDirectMemorySize设置
使用如下方式,创建直接内存:ByteBuffer directBuffer = ByteBuffer.allocateDirect(10);
优点:
1.无需垃圾回收:直接内存并不受Java堆的垃圾回收机制管理,不会占用宝贵的堆空间,也不会对垃圾回收器产生额外的压力。
2.零拷贝:在使用直接内存进行I/O操作时,可以通过零拷贝技术将数据直接从直接内存(内核缓冲区)传输到网络或磁盘上,避免了数据复制的开销,提高了性能。
缺点:
1.分配和释放成本较高:由于直接内存需要与操作系统进行交互,所以它的分配和释放成本相对较高,而且需要谨慎管理以避免资源泄漏。

TLAB
Thread Local Allocation Buffer,即线程本地分配缓存区。
TLAB空间的内存非常小,缺省情况下仅占有整个Eden空间的1%,也可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。
TLAB的本质其实是三个指针管理的区域:start,top 和 end,每个线程都会从Eden分配一块空间,例如说100KB,作为自己的TLAB,其中 start 和 end 是占位用的,标识出 eden 里被这个 TLAB 所管理的区域,卡住eden里的一块空间不让其它线程来这里分配。
TLAB只是让每个线程有私有的分配指针,但底下存对象的内存空间还是给所有线程访问的,只是其它线程无法在这个区域分配而已。从这一点看,它被翻译为 线程私有分配区 更为合理一点
当一个TLAB用满(分配指针top撞上分配极限end了),就新申请一个TLAB,而在老TLAB里的对象还留在原地什么都不用管——它们无法感知自己是否是曾经从TLAB分配出来的,而只关心自己是在eden里分配的。
TLAB是为了减少不同线程之间因为对象分配而造成的竞争,提高了多线程环境下对象分配的效率。
TLAB的空间大小不是固定的,而是有JVM根据运行情况计算而得。默认情况下,TLAB的大小很小,仅占有整个Eden空间的1%。具体可以通过参数设置 -XX:TLABSize来设置,但请注意,这个参数在某些JVM版本中可能不可用。
注意,当一个线程申请新的TLAB时,老的TLAB相当于退化成了Eden的一部分,因为TLAB只对对象内存分配这一步有影响,对于后续没有影响。
TLAB的缺点
事务总不是完美的,TLAB也又自己的缺点。因为TLAB通常很小,所以放不下大对象。

  • TLAB空间大小是固定的,但是这时候一个大对象,我TLAB剩余的空间已经容不下它了。(比如100kb的TLAB,来了个110KB的对象)
  • TLAB空间还剩一点点没有用到,有点舍不得。(比如100kb的TLAB,装了80KB,又来了个30KB的对象)
    所以JVM开发人员做了以下处理,设置了最大浪费空间。
    当剩余的空间小于最大浪费空间,那该TLAB属于的线程在重新向Eden区申请一个TLAB空间。进行对象创建,还是空间不够,那你这个对象太大了,去Eden区直接创建吧!
    当剩余的空间大于最大浪费空间,那这个大对象请你直接去Eden区创建,我TLAB放不下没有使用完的空间。
    当然,又回造成新的病垢。
  • Eden空间够的时候,你再次申请TLAB没问题,我不够了,Heap的Eden区要开始GC,
  • TLAB允许浪费空间,导致Eden区空间不连续,积少成多。以后还要人帮忙打理
    正是因为有这样的缺点,所以虚拟机主版本并没有使用TLAB,需要设置参数来使用它。

开启TLAB,对象的内存分配流程
1.检查TLAB:检查TLAB中是否有足够内存容纳新对象。如果有,跳转步骤3;否则继续步骤2
2.重新填充TLAB或在Eden区进行常规分配
如果TLAB中的空间不足以容纳新对象,那么虚拟机会进行两种可能的操作之一:一是尝试重新为线程分配一个新的、更大的TLAB;二是如果无法分配新的TLAB或者新的TLAB仍然不足以满足需求,那么线程会退出TLAB模式,直接在堆上进行内存分配
3.在TLAB中分配对象:一旦确定TLAB中有足够空间,线程就会直接在TLAB中分配内存。因为无需与其他线程同步,因此速度非常快
4.初始化对象

new指令对象的创建过程

(1)检查指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,则先执行类加载过程
(2)接下来为新生对象分配内存。对象所需内存大小在类加载完后便可完全确定。为对象分配空间的任务等同于把一块确定大小的内存从java堆中划分出来

划分内存有两种方法:指针碰撞和空闲列表。选择哪种方法由java堆是否规整决定,而java堆是否规整又由所采用的垃圾收集器是否带有空间压缩能力决定。因此使用Serial、ParNew等带压缩整理过程的垃圾收集器时,系统采用指针碰撞法,即简单又高效;当使用CMS这种基于清除算法的收集器时,理论上只能采用较为复杂的空闲列表法。
分配内存这个行为是需要同步的,虚拟机默认采用CAS加上失败重试保证更新的原子操作;另一种方式是通过TLAB来增加效率

(3)将分配到的内存空间都初始化为零值(这个说法是错误的。对于在声明时就进行了初始化的引用类型变量,它们会被初始化为指定的对象实例,而不是 null。在声明时,没有显示设置初始值的,才会被初始化为零值。),保证对象的实例字段不赋初始值就能使用。如果使用TLAB,这项工作将提前至TLAB分配时顺便进行。例如,数值类型的字段会被设置为0,引用类型的对象会被设置为null。

例如,如下两个变量会被直接初始化为真实值:

public class MyClass {
     
    private AnotherClass myObject = new AnotherClass();  
    private int a = 10; 
    public MyClass() {
     
    }  
}

注意,类的静态变量,是在类加载阶段初始化的。

(4)对象头的设置,如对象是哪个类的实例、如果找到类的元数据信息、对象的hashCode、对象的GC分代年龄等。还有是否启用偏向锁等,这些信息存放在对象头中
(5)执行构造方法,如果没有定义构造方法,Java会提供一个默认的构造方法。
(6)返回引用。构造方法执行成功后,虚拟机就会返回这个新创建对象的引用。我们就可以通过这个引用来访问和操作对象了。

new指令大致执行过程

if(对象已经初始化){
   
	l=获取对象长度
	if(配置了TLAB){
   TLAB中分配对象
	}
	if(对象没分配成功){
   Eden区分配对象,通过cas+失败重试
	}
	if(分配到内存了){
   
		为对象初始化零值;
		if(启用了偏向锁){
   
			设置偏向锁到对象头
		}
		设置对象头
	}
}
对象的内存布局

对象在内存中分为3块区域:对象头、实例数据、对齐填充
对象头:包括两部分信息,一部分用于存储对象自身的运行时数据,如hashCode、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等;另一部分是类型指针,即对象指向它的类元数据的指针。此外,如果对象是一个数组,那么对象头中还必须有一块记录数组长度的数据
实例数据:对象真正存储的有效信息,也就是程序代码中锁定义的各种类型的字段内容
对齐填充:这并不是必然存在的,仅仅起着占位符的作用。任何对象的大小都必须是8字节(byte)的整数倍

对象的访问定位

我们通过reference来操作具体的对象。reference可能在栈的本地变量表,也可能在对象的实例数据里

  • 句柄。Java堆中划分出一块内存作为句柄池,reference中存储的是句柄地址,而句柄则是对象实例数据和类元信息的指针
  • 直接指针。Hotspot使用第二种,定位对象减小一次开销

StackOverflowError:栈溢出。死循环导致虚拟机栈的栈深度无限增长超过Java虚拟机规定的最大深度时会出现
OutOfmemoryError:内存溢出。
内存泄漏:指内存不再使用但却没有清理

String.intern()
public static 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值