目录
一、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)直接内存回收机制总结
1)定义
Java Virtual Machine ,Java 程序的运行环境(Java 二进制字节码的运行环境)。
2)好处
3)比较
JVM、JRE、JDK 的关系如下图所示:
我们主要学习的是 HotSpot 版本的虚拟机。
(1)jvm = 类加载器 + 内存结构 + 执行引擎
(2)二进制字节码类相关放在方法区,类创建的实例对象放在堆,在调用方法时会用到虚拟机栈、程序计数器、本地方法栈
(3)方法执行时,每行代码由解释器逐行执行。热点代码由JIT即时编译器优化执行。不用的对象由GC垃圾回收。对于操作系统中对应的方法需要由本地方法接口组成
java源代码 -- 》二进制字节码+虚拟机指令 --》由解释器将二进制字节码变成机器码 --》交由cpu执行
作用:是记录下一条 jvm 指令的执行地址行号。
注:物理上,我们是通过寄存器来记录地址行号的。
在二进制字节码+虚拟机指令这一步骤,每一行二进制字节码+jvm指令前有一个数字记录这条指令对应的行号(地址)(主要用于定位),程序计数器在执行第一条二进制字节码+jvm指令的时候,会把他下一条的二进制字节码+虚拟机指令地址记录下来,这样,在cpu执行第一条的同时,解释器可以通过程序计数器快速定位下一条指令。
每个线程运行需要的内存空间,称为虚拟机栈。
每个栈由多个栈帧(Frame)组成,对应着每个调用方法时所占用的内存。因为每个方法的参数、局部变量、返回地址这些都是需要内存来存放的。
每个线程只能有一个活动栈帧,对应着当前正在执行的方法。
栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!
*1)调整栈内存大小
*2)两个类循环引用
部门类里有员工,员工类里也有部门。
解决方案:在部门类里,对于员工的字段上加:@JsonIgnore
如果json使用的是com.alibaba.fastjson,就用@JSONField(serialize = false)
*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进制就可以查询到了
本地方法:不是由java代码编写的方法,却能与操作系统打交道的方法,带有关键字native。
一些与操作系统底层打交道的方法只能是C或者C++,我们java想跟操作系统打交道,只能通过本地方法这个媒介。本地方法所占用的内存,就是本地方法栈。
本地方法:clone(),hashCode(),notify(),wait()
通过new关键字创建的对象都会被放在堆内存。
java.lang.OutofMemoryError :java heap space. 堆内存溢出
垃圾回收只能回收没人用的对象,如果对象一直增加,且一直有人用,就会有堆内存溢出的情况。
可以使用 -Xmx8m 来指定堆内存大小
Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区在虚拟机启动时被创建。
逻辑上方法区是堆的一部分,但具体实现不同jvm的实现方式不一样。jdk1.8以前,用的永久代,方法区这个时候就是堆的一部分。jdk1.8以后,用的元空间,此时用的是操作系统的内存而不是堆的内存。但是jdk1.8的时候,串表还是在堆里面而不是在元空间。
*1)反编译查看
二进制字节码 = 类的基本信息 + 常量池 + 类方法定义(包含了虚拟机的指令)
我们先编译好一段代码,然后找到字节码文件,然后进行反编译:
#反编译 javap -v 文件名
第一部分是类的基本信息,Constant pool是常量池,后面还有一个类方法定义
*2) 基本概念
常量池:
就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
运行时常量池:
常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址。即如果我们上常量池变成运行时常量池,#1,#2,#3就会变成真实的地址,而不是1,2,3
jdk1.8:
调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中
jdk1.6:
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报的是永久代。
-Xmx10m 指定堆内存大小
-XX:+PrintStringTableStatistics 打印字符串常量池信息
-XX:+PrintGCDetails
-verbose:gc 打印 gc 的次数,耗费时间等信息
如果我们只有int i=0,那么原本有1733个string对象:
我们for循环100次:
我们for循环1万次:
开头执行了一次垃圾回收,再看string数量,确实没有增加1万个,一些没用的就被gc回收掉了
*1)设置StringTableSize
因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间。
如果字符串很多,可以调大数值,可以明显提高性能。
-XX:StringTableSize=桶个数(最少设置为 1009 以上)
*2)使用intern()
对于很大数据量的字符串数据,其中又有很多是重复的,我们可以对字符串使用intern,这样多个重复的都只会放到串池中一份,大大节省堆内存。
常规的读取方式:
cpu先从用户态切换成内核态,然后系统读取磁盘文件并放到系统缓冲区,然后内核态切换到用户态,然后java生成一块java缓冲区,去读取系统缓冲区的内容。
使用直接内存之后:
cpu先从用户态切换成内核态,然后系统读取磁盘文件放到直接内存,直接内存是一个公共区域,java和系统都可以访问。从内核态切换到用户态,java直接读取直接内存数据。
减少了原本的从系统缓冲区同步数据到java缓冲区的过程。
直接内存也是存在内存溢出的问题。他的回收不是GC回收
处理直接内存回收的是 Unsafe:
一般用 jvm 调优时,会加上下面的参数:
-XX:+DisableExplicitGC // 禁止显示的 GC
意思就是禁止我们手动的 GC,比如手动 System.gc() 无效,它是一种 full gc,会回收新生代、老年代,会造成程序执行的时间比较长。所以我们就通过 unsafe 对象调用 freeMemory 的方式释放内存。