153.JVM(一):虚拟机的内存结构
作者:互联网
目录
一、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 的方式释放内存。
标签:153,虚拟机,回收,串池,内存,JVM,线程,方法 来源: https://blog.csdn.net/qq_40594696/article/details/120759883