当前位置: 首页 > news >正文

JVM-内存结构

目录

1.什么是JVM?

2.jvm的内存结构

2.1程序计数器

2.1.1定义

2.1.2Java程序的运行原理

 2.2虚拟机栈

2.2.1定义

2.2.2栈内存溢出

2.3线程运行诊断

3.本地方法栈

4.堆

4.1定义

4.2堆内存溢出

4.3堆内存诊断

5.方法区

5.1定义

5.2方法区的组成

5.3方法区内存溢出

1.8以前会导致永久带内存溢出

5.4运行时常量池

5.4.1二进制字节码

5.4.2常量池

5.4.3运行时常量池

5.5StringTable

5.5.1常量池与串池的关系

5.5.2字符串变量拼接

5.5.3编译器优化

5.5.4字符串延迟加载

5.5.5intern_1.8

5.5.6intern_1.6

5.5.7面试题

5.5.7StringTable总结

5.6StringTabled的位置

5.7StringTable 垃圾回收

5.8StringTable性能调优

6.直接内存

6.1基本使用

6.2直接内存的内存溢出问题


1.什么是JVM?

定义:

Java Virtual Machine - java程序的运行环境(java二进制字节码的运行环境)

好处:

*一次编写,到处运行

*自动内存管理,垃圾回收功能

*数字下标越界检查

*多态

jvm、jre、jdk的区别:

jvm(java虚拟机) jre(java运行时环境) jdk(java开发工具包)

2.jvm的内存结构

jvm的内存结构包括:

1.程序计数器

2.虚拟机栈

3.本地方法栈

4.堆

5.方法区

2.1程序计数器

2.1.1定义

Program Counter Register 程序计数器(寄存器)

作用:记录下一条jvm指令的执行地址

特点:

*程序计数器是线程私有的,是线程安全的

*程序计数器不会存在内存溢出,因为程序计算器仅仅只是一个运行指示器,它所需要存储的内容仅仅就是下一个需要待执行的命令的地址,无论代码有多少,最坏情况下死循环也不会让这块内存区域超限,因为程序计算器所维护的就是下一条待执行的命令的地址,所以不存在java.lang.StackOverFlowErrror(栈内存溢出)或者java.lang.OutOfMemoryError(内存溢出)

2.1.2Java程序的运行原理

java源代码经过编译,得到二进制字节码(jvm指令),再经过解释器,得到机器码,最后交给CPU执行。

其中程序计数器会记录jvm指令的地址,当第一条指令交给解释器之后,程序计数器会记录下一条jvm指令的地址,以此类推。

 2.2虚拟机栈

2.2.1定义

Java Vitrual Machine Stacks(java虚拟机栈)

*每一个线程运行时所需的内存,称为虚拟机栈

*每一个栈由多个栈帧(Frame)组成,对应着每次方法调用是所占用的内存

*每一个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

问题辨析

1.垃圾回收是否涉及栈内存?

栈内存是一次次的方法调用产生的栈帧内存,每一个栈帧会在被调用结束后自动弹出栈,即自动回收掉这些栈帧,所有根本不需要垃圾回收。

2.栈内存分配越大越好吗?

栈内存分配的越大,虚拟机栈的内存越大,但物理内存一定,故可以同时运行的线程数就会降低,影响效率。

3.方法内的局部变量是否线程安全?

*如果方法内的局部变量没有逃离方法的作用范围(即该变量没有return到全局变量),它是线程安全的

*如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

2.2.2栈内存溢出

栈帧过多导致栈内存溢出,例如死循环递归

另外,我们在引用第三方库的时候,可能会出现栈内存溢出,例如json数据转java,创建两个实体类,第一个实体类name,dept(引用第二个实体类),第二个实体类中有name,emps,此时在转换的时候两个类会有循环引用的问题,该问题解决方法是在一个引用变量上面加上注解@JsonIgnore,使得引用由双向变为单向。

栈帧过大导致栈内存溢出(一般不出现)

2.3线程运行诊断

案例1:cpu占用过多

定位

*在linux系统中使用top定位哪个进程对CPU的占用过高

*使用ps H -eo pid,tid,%cpu | grep 进程id进一步定位是进程中哪一个线程引起的CPU占用过高

H打印进程数,-eo打印进程的内容(pid进程id,tip线程id,%cpu对CPU的占用百分比),|管道符,(grep)筛选进程id。

*jstack 进程id,可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行号

如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。另外,jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态。

 定位到源代码的第8行。

案例2:程序运行很长时间没有结果(死锁)

 

 

 synchronized同步锁,两个同步锁都拿着对方想要的变量,造成死锁。

3.本地方法栈

定义:给本地方法(使用C/C++语言编写的方法)的运行提供内存空间。

4.堆

4.1定义

Heap堆

*通过new关键字,创建对象都会使用堆内存

特点

*它是线程共享的,堆中对象都需要考虑线程安全的问题

*有垃圾回收机制(堆中不再被引用的对象会被垃圾回收)

4.2堆内存溢出

虽然堆内存有垃圾回收机制,但是回收的内容是不被调用的对象,此时该程序中的ArrayList集合一直被调用添加,而String a则一直被拼接,由于是使用“+”拼接,a变量会不断在堆内存新建变量,并且ArrayList也不断增大(但是ArrayList的大小是有限的,ArrayList的扩容长度受限于vms即虚拟机的最大数组限制,MAX_ARRAY_SIZE),从而导致堆内存溢出。

4.3堆内存诊断

1.jps工具

JPS(全称:Java Virtual Machine Process Status Tool)。安装JDK后在%JAVA_HOME%/bin目录下面自带的一个工具。用来查看计算机上面运行的JAVA进程。

*查看当前系统中有哪些java进程

 运行代码,使用命令行窗口

 

 当程序运行到输出台每打印一次之后,使用jmap -heap 18756查看该线程的堆内存的情况

通过分析可知,当程序执行到array = null;时,执行了垃圾回收机制,堆内存变为1MB

2.jmap工具

*查看堆内存占用情况 jmap - heap 进程id

3.jconsole工具

*图形界面的,多功能的监测工具,可以连续监测

 

 

5.方法区

5.1定义

Chapter 2. The Structure of the Java Virtual Machine
方法区的定义很多地方比较含糊,我这里找到最权威的定义——jvm规范中的定义,链接参考
摘录方法区部分的定义:

2.5.4. Method Area
The Java Virtual Machine has a method area that is shared among all Java Virtual Machine threads. The method area is analogous to the storage area for compiled code of a conventional language or analogous to the “text” segment in an operating system process. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors, including the special methods (§2.9) used in class and instance initialization and interface initialization.
Java虚拟机中有一个被所有jvm线程共享的方法区。方法区有点类似于传统编程语言中的编译代码块或者操作系统层面的代码段。它存储着每个类的构造信息,譬如运行时的常量池,字段,方法数据,以及方法和构造方法的代码,包括一些在类和实例初始化和接口初始化时候使用的特殊方法。

The method area is created on virtual machine start-up. Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it. This specification does not mandate the location of the method area or the policies used to manage compiled code. The method area may be of a fixed size or may be expanded as required by the computation and may be contracted if a larger method area becomes unnecessary. The memory for the method area does not need to be contiguous.
方法区在jvm启动时候被创建。虽然方法区在逻辑层面上是堆的一部分,但是就简单实现来说既不会被回收也不会被压缩。这个规范并不强制指定方法区存放的位置也不会对编译过的代码有管理策略的限制。方法区可能有一个固定的大小或者也可以通过计算大小去扩展也可以在不需要的时候被压缩。方法区的内存也不需要是连续的。

A Java Virtual Machine implementation may provide the programmer or the user control over the initial size of the method area, as well as, in the case of a varying-size method area, control over the maximum and minimum method area size.
Jvm虚拟机实现可以提供给编程人员或者用户初始化方法区的大小,同时在方法区可变大小的情况下,控制这个方法区的最大值和最小值。
The following exceptional condition is associated with the method area:
下面这种异常情况是和方法区有关联的:
If memory in the method area cannot be made available to satisfy an allocation request, the Java Virtual Machine throws an OutOfMemoryError.
如果方法区满足不了构造所需要的内存,jvm就会抛出OutOfMemoryError。
————————————————
版权声明:本文为CSDN博主「敏叔V587」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zhuxuemin1991/article/details/103900190

5.2方法区的组成

5.3方法区内存溢出

1.8以后会导致元空间内存溢出

以下代码可以根据二进制字节码文件循环生成并且加载多个类

由于1.8以后方法区是在元空间内,即本地内存(系统内存),故该程序不能让产生元空间内存溢出的错误,我们可以通过修改元空间的最大内存大小(-XX:MaxMEtaspaceSize=8m)来让虚拟机报错:

1.8以前会导致永久带内存溢出

应用场景:

spring中的动态代理

mybatis

5.4运行时常量池

5.4.1二进制字节码

第一步,先在idea中的终端中切换到该类的class文件的位置

第二部,使用Javap -v HelloWorld.class,查看该二进制文件的反编译信息

 

 

以下来举例:

getstatic #2👉#2 = Fieldref(成员变量)#21:#22👉(#21 = Class #28)(#22 = NameAndType #29:#30) 👉(#28 = Utf8 java/lang/System)(#29 = Utf8 out)(#30 = Utf8 Ljava/io/PrintStream;)

#2 = Field java/lang/System.out:Ljava/io/PrintSteam

5.4.2常量池

常量池,就是一张表,虚拟机指令根据这张常量表找到要执行的类名,方法名,参数类型,字面量等信息。

5.4.3运行时常量池

常量池*.class文件中的,当该类被加载,它的常量池信息就会被放入运行时常量时,并把里面的符号地址变为真实地址

5.5StringTable

5.5.1常量池与串池的关系

 

常量池存在与字节码文件中,当运行时会被加载到运行时常量池,等到具体执行到相应代码时,例如ldc #2会把a符号变为"a"字符串对象,然后先检查串池中是否有该字符对象,如果有则放入,没有则不将该字符对象放到StringTable[]中(串池)。

5.5.2字符串变量拼接

 

由图可知,s4位两字符串变量相加(拼接),然后使用StingBuilder()的一系列方法,该方法创建的对象是在堆中,而s3则在串池中,故两者不相等。

串池中存在常量,而变量存在于堆中。

5.5.3编译器优化

javac 在编译期间的优化,结果已经在编译期确定为ab

5.5.4字符串延迟加载

当程序执行到该延迟加载时,串池才会去加载,这种情况称为字符串的延迟加载

5.5.5intern_1.8

将该字符串对象尝试放入串池,如果有则不会放入串池,如果没有则放入,并且会把串池中的对象返回。

5.5.6intern_1.6

将该字符串对象尝试放入串池,如果有则并不会放入,如果没有会把次对象复制一份,放入串池,然后把串池中的对象返回。

5.5.7面试题

分析:s1,s2放入串池,s3在编译期间进行优化,将“ab”放入串池,s4是两个变量相加则放入堆中,s5将“ab”放入串池但此时串池已经有“ab”故不放入,s6是将s4尝试放入串池但串池中已经有该对象故将该对象返回给s6,x2是在堆中有c,d,cd三个对象,而x1是将对象“cd”放入常量池中,并且x2.调用intern()尝试将x2的堆中对象放入常量池,此时由于常量池中已经有了“ab”对象,故不放入,x2的对象还是在堆中。

1.s3 != s4因为前者是在常量池,后者是在堆中。

2.s3 == s5因为s3和s5都是在常量池中取对象“ab”

3.s3 == s6因为intern()在jdk1.8中,若常量池没有该对象,则放入并返回,若有则直接返回,故s6是将堆中的对象“ab”尝试放入常量池中,但常量池已经有了该对象,故将常量池的对象返回,赋值给s6。

4.x1 != x2因为一个是常量池中,一个是堆中。

 

如果x1和x2调换位置,则常量池中便会事先存在“cd”,故在jdk1.8中,x2表示在堆中有c,d,cd三个对象,然后执行到intern()表示将x2的对象“cd”放入常量池,此时x2对象是在常量池,故x1==x2。

在此基础上用jdk1.6的话,x2会拷贝自身一份到常量池中,而自身还是属于堆的变量,故两者不相等。

5.5.7StringTable总结

常量池中的字符串仅是符号,第一次用到时才变为对象。

利用串池的机制,来避免重复创建字符串对象。

字符串变量拼接的原理是StringBuilder(1.8)。

字符串常量拼接的原理是编译期优化。

可以使用intern方法,主动将串池中还没有的字符串对象放入串池

*1.8将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池,会把串池中的对象返回

*1.6将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有会把此对象复制一份,放入串池,会把串池中的对象返回

5.6StringTabled的位置

 在jdk1.8中StingTable的位置是在堆中,而在jdk1.6中StringTable的位置是在永久代中。

需要先设置虚拟机堆内存的大小,不然比较难报错-Xmx10m

由该程序可知在jdk1.6时,StringTable的位置是在永久代中。

 由该程序可知在jdk1.8时,StringTable的位置是在堆中。

5.7StringTable 垃圾回收

 初始程序的字符串常量是1754个。

 当执行100次之后,字符串常量变为100。

 当执行10000次之后,触发垃圾回收机制,字符串常量为7226。

 GC为垃圾回收。

5.8StringTable性能调优

1.调整-XX:StringTableSize=桶个数

 

通过调节-XX:StringTable=?来避免hash冲突,提高hash分布,进而提高StringTable的性能。

 2.考虑将字符串对象是否入池

 

 

 

将字符串对象存入常量池可以过滤掉重复的信息,从而减少内存的占用。

6.直接内存

Direct Memory

*常见于NIO操作时,用于数据缓冲区

*分配回收成本高,但读写性能高

*不受JVM内存回收管理

6.1基本使用

1.使用io读写文件

 

 

 

2.使用ByteBuffer缓冲区读写文件

 

 

 

结论:使用ByteBuffer缓冲区读写比使用IO流读写更快。

6.2直接内存的内存溢出问题

以上程序证明直接内存存在着内存溢出的问题。

直接内存不能通过垃圾回收来清除,但可以通过ByteBuffer对象的父类DirectByteBuffer对象中的Cleaner.create(this,new Deallocator(base, size, cap))方法可以监测DirectByteBuffer对象是否被垃圾回收掉,如果被回收掉则触发虚引用对象中的该方法, 该方法会执行thunk.run(),而run()是调用了unsafe.freeMemory()去释放了直接内存。

总结:

1.使用了Unsafe对象完成直接的分配回收,并且回收需要主动调用freeMemory方法

2.ByteBuffer的实现类内部,使用了Cleaner(虚引用)来监测ByteBuffer对象,一旦ByteBuffer对象被垃圾回收,那么就会由ReferenceHandler线程通过Cleaner方法调用freeMemory来释放直接内存

 虚拟机配置加上-XX:+DisableExplictGC,可以防止直接内存被显示的释放内存方法释放掉。

 需要在不影响其他程序的性能下,让直接内存释放掉,可以使用unsafe的freeMemory()。

相关文章: