153.JVM(一):虚拟机的内存结构

目录

一、jvm的基本介绍

1、什么是 JVM ?

2、常见的 JVM

3.jvm基本结构

二、JVM内存结构

1.程序计数器

(1)代码的运行流程

(2)程序计数器的作用

 (3)程序计数器特点

2.虚拟机栈

(1)什么是虚拟机栈

(2)虚拟机栈的一些细节

(3)栈内存溢出

(4)线程运行诊断(重要)

3.本地方法栈

4.堆:Heap

(1)定义

(2)堆内存溢出

(3)堆内存诊断

 5.方法区

(1)定义

 (2)方法区内存溢出

(3)运行时常量池

(4)StringTable(串池)

 (5)StringTable的位置

(6)StringTable垃圾回收

​ (7)StringTable性能调优

6.直接内存

(1)基本概念

(2)使用直接内存的好处

(3)直接内存回收机制总结


一、jvm的基本介绍

1、什么是 JVM ?

1)定义
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
2)好处

  • 一次编译,处处执行
  • 自动的内存管理,垃圾回收机制
  • 数组下标越界检查

3)比较
JVM、JRE、JDK 的关系如下图所示:

2、常见的 JVM

我们主要学习的是 HotSpot 版本的虚拟机。 

3.jvm基本结构

(1)jvm = 类加载器 + 内存结构 + 执行引擎

(2)二进制字节码类相关放在方法区,类创建的实例对象放在堆,在调用方法时会用到虚拟机栈、程序计数器、本地方法栈

(3)方法执行时,每行代码由解释器逐行执行。热点代码由JIT即时编译器优化执行。不用的对象由GC垃圾回收。对于操作系统中对应的方法需要由本地方法接口组成

二、JVM内存结构

1.程序计数器

(1)代码的运行流程

java源代码 -- 》二进制字节码+虚拟机指令 --》由解释器将二进制字节码变成机器码 --》交由cpu执行

(2)程序计数器的作用

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

注:物理上,我们是通过寄存器来记录地址行号的。

二进制字节码+虚拟机指令这一步骤,每一行二进制字节码+jvm指令前有一个数字记录这条指令对应的行号(地址)(主要用于定位),程序计数器在执行第一条二进制字节码+jvm指令的时候,会把他下一条的二进制字节码+虚拟机指令地址记录下来,这样,在cpu执行第一条的同时,解释器可以通过程序计数器快速定位下一条指令

 (3)程序计数器特点

  • 是线程私有的
  • 不会存在内存溢出

2.虚拟机栈

(1)什么是虚拟机栈

每个线程运行需要的内存空间,称为虚拟机栈

每个栈由多个栈帧(Frame)组成,对应着每个调用方法时所占用的内存。因为每个方法的参数、局部变量、返回地址这些都是需要内存来存放的。

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

(2)虚拟机栈的一些细节

  • 垃圾回收不涉及栈内存。栈内存是方法调用产生的,方法调用结束后会弹出栈释放内存。
  • 栈内存分配不是越大越好。物理内存是一定的,栈内存越大,可执行的线程数就会越少。栈默认大小是1024KB,一般不会增大栈内存
    • 设置栈内存:-Xss1024k  -Xss1m
  • 方法内的局部变量是否线程安全
    • 变量在方法内定义,并且在方法内结束。即既没有引用对象,也没有返回对象(逃离),就是安全的
    • 如果是局部变量引用了对象(入参),或者逃离了方法的访问(出参),那就要考虑线程安全问题。

(3)栈内存溢出

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!

*1)调整栈内存大小

 *2)两个类循环引用

部门类里有员工,员工类里也有部门。

解决方案:在部门类里,对于员工的字段上加:@JsonIgnore

如果json使用的是com.alibaba.fastjson,就用@JSONField(serialize = false)

(4)线程运行诊断(重要)

*1)在linux环境,我们输入:top。可以查看到各线程运行情况

 *2)查看线程的运行指标:

ps H -eo user,pid,tid,%cpu

ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号

*3)jstack 进程 id

哪个用户创建的线程,需要先su 那个用户。然后把我们查询的进程id转化为16进制就可以查询到了

3.本地方法栈

本地方法:不是由java代码编写的方法,却能与操作系统打交道的方法,带有关键字native。

一些与操作系统底层打交道的方法只能是C或者C++,我们java想跟操作系统打交道,只能通过本地方法这个媒介。本地方法所占用的内存,就是本地方法栈。

本地方法:clone(),hashCode(),notify(),wait()

4.堆:Heap

(1)定义

通过new关键字创建的对象都会被放在堆内存

  • 它是线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

(2)堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出

垃圾回收只能回收没人用的对象,如果对象一直增加,且一直有人用,就会有堆内存溢出的情况。

可以使用 -Xmx8m 来指定堆内存大小

(3)堆内存诊断

  • jps工具
  • jmap
    • jmap -heap 进程ID
  • jconsole
  • jvisualvm

 5.方法区

(1)定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域方法区在虚拟机启动时被创建

逻辑上方法区是堆的一部分,但具体实现不同jvm的实现方式不一样。jdk1.8以前,用的永久代,方法区这个时候就是堆的一部分。jdk1.8以后,用的元空间,此时用的是操作系统的内存而不是堆的内存。但是jdk1.8的时候,串表还是在堆里面而不是在元空间。

 (2)方法区内存溢出

  • 1.8 之前会导致永久代内存溢出
    • 使用 -XX:MaxPermSize=8m 指定永久代内存大小
  • 1.8 之后会导致元空间内存溢出
    • 使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

(3)运行时常量池

*1)反编译查看

二进制字节码 = 类的基本信息 + 常量池 + 类方法定义(包含了虚拟机的指令)

我们先编译好一段代码,然后找到字节码文件,然后进行反编译:

#反编译
javap -v 文件名

第一部分是类的基本信息,Constant pool是常量池,后面还有一个类方法定义 

*2) 基本概念

常量池:

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

运行时常量池

常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。即如果我们上常量池变成运行时常量池,#1,#2,#3就会变成真实的地址,而不是1,2,3

(4)StringTable(串池)

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象。懒加载
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder。String ab = s1+s2。这里的s1+s2使用的是StringBuilder.append()
  • 字符串常量拼接的原理是编译期优化。String s = "ab";String s2 = "a"+"b"。这两个本质上一样,在编译期优化了。
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

jdk1.8:

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功。如果放入成功,原本堆对象就会放入串池,变成串池对象
  • 如果串池有该字符串对象,则放入失败。
  • 无论放入是否成功,都会返回串池中的字符串对象。

jdk1.6:

  •  如果串池中没有该字符串对象,原本的堆对象会复制一份,然后将复制的那一份放到串池,也就是说,放到串池的是复制体,他本身还是堆对象
  • 如果串池有该字符串对象,则放入失败。
  • 无论放入是否成功,都会返回串池中的字符串对象。

 (5)StringTable的位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

#-Xmx10m:设置堆内存10m
#-XX:-UseGCOverheadLimit:如果不加这个配置,虚拟机发现花费98%的时间都清理不了2%的代码,虚拟机就会直接不干了,给出报错:java.lang.OutOfMemoryError: GC overhead limit exceeded
#加了-XX:-UseGCOverheadLimit,虚拟机还会继续干下去,我们才会发现堆内存不足
jdk1.8: -Xmx10m -XX:-UseGCOverheadLimit

#发现报错永久代内存不足
jdk1.6:  -XX:MaxPermSize=10m

 jdk1.8中报的是堆内存不足,1.6报的是永久代。

(6)StringTable垃圾回收

-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息

 如果我们只有int i=0,那么原本有1733个string对象:

 我们for循环100次:

 我们for循环1万次:

开头执行了一次垃圾回收,再看string数量,确实没有增加1万个,一些没用的就被gc回收掉了 

 (7)StringTable性能调优

*1)设置StringTableSize

因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间。

如果字符串很多,可以调大数值,可以明显提高性能。

-XX:StringTableSize=桶个数(最少设置为 1009 以上)

*2)使用intern()

对于很大数据量的字符串数据,其中又有很多是重复的,我们可以对字符串使用intern,这样多个重复的都只会放到串池中一份,大大节省堆内存。

6.直接内存

(1)基本概念

  • 常见于 NIO 操作时,用于数据缓冲区(比如DirectBuffer-> ByteBuffer bb = ByteBuffer.allocateDirect(_1Mb))。
  • 分配回收成本较高,但读写性能高。
  • 不受 JVM 内存回收管理

(2)使用直接内存的好处

常规的读取方式:

cpu先从用户态切换成内核态,然后系统读取磁盘文件并放到系统缓冲区,然后内核态切换到用户态,然后java生成一块java缓冲区,去读取系统缓冲区的内容。 

 使用直接内存之后:

cpu先从用户态切换成内核态,然后系统读取磁盘文件放到直接内存,直接内存是一个公共区域,java和系统都可以访问。从内核态切换到用户态,java直接读取直接内存数据

减少了原本的从系统缓冲区同步数据到java缓冲区的过程

(3)直接内存回收机制总结

直接内存也是存在内存溢出的问题。他的回收不是GC回收

  • 使用了 Unsafe 类来完成直接内存的分配回收,回收需要主动调用freeMemory 方法
  • ByteBuffer 的实现内部使用了 Cleaner(虚引用)来检测 ByteBuffer 。一旦ByteBuffer 被垃圾回收,那么会由 ReferenceHandler(守护线程) 来调用 Cleaner 的 clean 方法调用 freeMemory 来释放内存

处理直接内存回收的是 Unsafe:

一般用 jvm 调优时,会加上下面的参数:

-XX:+DisableExplicitGC  // 禁止显示的 GC

意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。

猜你喜欢

转载自blog.csdn.net/qq_40594696/article/details/120759883
今日推荐