JVM学习笔记 02、JVM的内存结构(上)
@[toc]
前言
本篇博客是跟随黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓的学习JVM的笔记,若文章中出现相关问题,请指出!
所有博客文件目录索引:博客目录索引(持续更新)
JVM整体视角
一、程序计数器(私有)
1.1、介绍
Program Counter Register
程序计数器(寄存器)
作用:是记住下一条jvm指令的执行地址
特点:
- 是线程私有的
- 不会存在内存溢出
1.2、作用
java从编写到执行过程:首先是java源代码,使用javac编译成字节码文件(java代码->字节码),接着使用解释器来将字节码转为机器码交由CPU来执行。
- 可以看到对应的字节码左边都有执行的顺序编号,指的就是指令的内存地址,当这些指令被加载到虚拟机以后,就可以根据这些地址信息来找到对应的命令。
- 过程:解释器拿到一条字节码指令将其转为机器码执行,与此同时程序计数器中就已经记住了下一条jvm指令的执行地址,解释器会去程序计数器中取到对应的执行地址,进而来执行下一条字节码指令。
程序计数器功能:记住下一条jvm指令的执行地址。方便之后解释器执行完一条命令接着从计数器中取下一条指定。
- 物理上是使用寄存器来实现的,寄存器是读取速度最快的一个单元,用来存储地址读取地址。
1.3、特点
①线程私有
每个线程都有其自己的程序计数器
②不会存在内存溢出
JVM本身提到了程序计数器不会内存溢出的特点,所以一些厂商在实现自己jvm过程中也不需要考虑该问题。
二、虚拟机栈(私有)
2.1、初识
栈
:线程运行时所需要的内存空间,一个栈内分为多个栈帧组成,栈帧指的是每个方法所需要的运行内存。
过程:线程执行方法1(方法1入栈),方法1中调用方法2(方法2入栈),方法2调用方法3(方法3入栈),入栈的都是栈帧,栈帧中保存了参数、局部变量、返回地址…,一旦某个方法结束,该栈帧就会出栈(也就是相当于释放该栈帧内存)。
2.2、定义
Java Virtual Machine Stacks
(Java 虚拟机栈)
- 每个线程运行时所需要的内存,称为虚拟机栈。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存。
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。
帧栈的演示:
2.3、问题辨析(3个)
问题辨析
- 垃圾回收是否涉及栈内存?
- 栈内存分配越大越好吗?
- 方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围(作为返回对象、引用类型作为参数传递到方法),需要考虑线程安全。
解答:
- 栈帧内存无非就是方法调用而产生的栈帧内存,栈帧内存在每次方法调用之后都会被释放掉根本就不需要垃圾回收来进行回收。
- 栈内存可以通过运行虚拟机时的参数所设定,如指令:
-Xss size
,默认三个平台(linux、macos、oracle solaries)都是1024KB,而windows的话根据虚拟内存来决定的。栈内存越大,反而会让你的线程数越少。划分大了仅仅只是能够更多次的递归调用,不会增强你运行的效率,反而会影响线程数目的变少。 - 看是否是线程安全,主要看这个变量是公有的还是私有的。若是staic这类公有变量每个线程都能够对该变量进行操作,那么就是线程不安全的。
2.4、栈内存溢出(案例演示)
说明
栈帧过多导致栈内存溢出:不断的进行方法调用,始终没有出栈,导致栈帧的内存超过了栈的内存。例如:递归调用一直没有出口,导致有无限方法一直被调用帧栈一直在入栈,就会出现栈内存溢出。
栈帧过大导致栈内存溢出:某个栈帧过大直接将你的栈内存给占满了。
案例演示
①栈帧过多:无线递归调用
class Main {
private static int count;
public static void main(String[] args) {
try {
test();
}catch (StackOverflowError e){
e.printStackTrace();
System.out.println(count);
}
}
public static void test(){
count++;
test();
}
}
结果:
①直接运行,最终报错,输出41385。也就是说递归调用41385次出现了栈内存溢出。
②在运行时设置虚拟机参数-Xss256k,最终报错输出2738,表示调用了2738次。
结论:通过设置-Xss参数可以调节栈的内存空间大小。
②帧栈过多的演示二:
两个类循环引用之后,例如使用一些转json的工具类也会导致stackoverflow
class Test2{
public String name;
public Test1 test1;
}
class Test1{
public String name;
public Test2 test2;
}
class Main {
public static void main(String[] args) throws JsonProcessingException {
//类与类之间循环引用
Test1 test1 = new Test1();
Test2 test2 = new Test2();
test1.test2 = test2;
test2.test1 = test1;
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(test1));
}
}
这里的话,由于对象与对象之间出现循环依赖,所以使用ObjectMapper将其转为JSON时就会不断无线递归下去。
在Spring中也有循环引用的情况,使用循环依赖三级缓存来解决。
2.5、线程运行诊断
2.5.1、案例1: cpu 占用过多
定位过程:
-
用
top
命令定位哪个进程对cpu的占用过高:只能定位到某个进程号。 -
ps H -eo pid,tid,%cpu | grep 进程id
:用ps命令进一步定位是哪个线程引起的cpu占用过高。# H表示所有的线程数,将信息都展示出来,-eo表示对哪些应用感兴趣,如pid、tid、cpu,tid就是对应的线程号 # 能够查看所有线程的指标了 ps H -eo pid,tid,%cpu # 若是想要定位某个进程号的线程,此时就能够定位到指定的一个线程 ps H -eo pid,tid,%cpu | grep 进程号 # 更详细的信息:列出了进程id, 线程id和cpu占有率,同时按照cpu占有率排序 ps H -eo user,pid,ppid,tid,time,%cpu,cmd --sort=%cpu
-
jstack 进程id
,可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号。接着会打印多个线程的信息,此时要注意的是我们需要根据之前使用ps排查到cpu占用过大的线程号来进行找到指定的线程执行的代码。
需要将十进制的线程编号换算为十六进制,接着使用这十六进制在jstack中进行查找定位。
32665 => 7f9b
//问题代码
public class Main {
public static void main(String[] args) {
new Thread(null,()->{
System.out.println("1...");
while(true){
}
},"thread1").start();
new Thread(null,()->{
System.out.println("2...");
try {
Thread.sleep(100000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"thread2").start();
new Thread(null,()->{
System.out.println("3...");
try {
Thread.sleep(100000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"thread3").start();
}
}
2.5.2、案例2:程序运行很长时间没有结果
有可能是线程发生了死锁导致最终没有得到结果。
同样使用命令:jstack 进程号
,若是有死锁问题在最后会有对应的死锁提示信息。
三、本地方法栈(私有)
不是由java代码编写的方法,java代码有一定的限制,不能够直接跟操作系统底层打交道,所以需要用c或者c++编写的本地方法来真正与操作系统、底层的API来打交道,而java通过调用本地方法来去调用一些底层的功能,这些本地方法使用的内存就是本地方法栈。
作用:给本地方法的运行提供内存空间!
使用的位置:java的基础类库、执行引擎中都会使用、调用本地方法。
本地方法使用native来进行声明!
四、堆(公共)
4.1、定义
Heap
堆:通过 new 关键字,创建对象都会使用堆内存
特点:
- 它是线程共享的,堆中对象都需要考虑线程安全的问题
- 有垃圾回收机制:堆中不再被引用的对象就会被当成垃圾进行回收。
4.2、堆内存溢出(案例演示)
说明
介绍:对象当做垃圾回收的条件:没人再使用它。但是如果我不断的产生对象,而产生的新对象仍然有人在使用它们,此时就意味着这些对象不能作为垃圾,这样的对象达到一定的数量就会导致堆内存被耗尽,也就是堆内存溢出。
描述:默认的堆空间为4G。有时候内存占用非常大可能不会立刻暴露出内存溢出的问题,随着时间的累计若是编码不当就会出现内存溢出的问题!
排查堆内存溢出问题:可以通过设置-Xmx参数,尽可能设小,能够让程序更快的暴露堆内存的问题。
代码演示
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
String message = "changlu";
int count = 0;
List<String> list = new ArrayList<>();
try {
while (true) {
list.add(message);
message += message;
count++;
}
}catch (Throwable t){
t.printStackTrace();
System.out.println(count);
}
}
}
通过设置-Xmx参数将堆内存空间设小:
此时就能够更快的报出异常错误,让我们更快的去检测问题
4.3、堆内存诊断(三个工具)
工具介绍
jps
:打印当前运行java的所有进程号。jmap
:查看当前时间段堆内存占用情况。如:jmap - heap 进程id
jconsole
:内存、线程监控工具。
案例演示
三个阶段:阶段1是不创建任何对象。阶段2创建一个10MB的数组。阶段3垃圾回收。这三个阶段能够让我们看到对应的堆内存的起伏现象!
public class Main {
public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
Thread.sleep(30000);
byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
System.out.println("2...");
Thread.sleep(20000);
array = null;
System.gc();//进行垃圾回收
System.out.println("3...");
Thread.sleep(1000000L);
}
}
jps:确定运行的进程号
D:\workspace\workspace_idea\mavenexer>jps
16016 Main
18992
2880 Jps
4660 Launcher
jmap工具:分别查看三个阶段的堆内存状态
# 阶段1:没有任何手动创建对象
D:\workspace\workspace_idea\mavenexer>jmap -heap 16016
Attaching to process ID 16016, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.201-b09
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 5337251840 (5090.0MB) # 可以看到默认最大堆内存空间为5G
NewSize = 111673344 (106.5MB)
MaxNewSize = 1778909184 (1696.5MB)
OldSize = 223870976 (213.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
# 新生代
Eden Space:
capacity = 84410368 (80.5MB)
used = 8441304 (8.050254821777344MB) # 当前已经使用8MB
free = 75969064 (72.44974517822266MB)
10.000316548791732% used
From Space:
capacity = 13631488 (13.0MB)
used = 0 (0.0MB)
free = 13631488 (13.0MB)
0.0% used
To Space:
capacity = 13631488 (13.0MB)
used = 0 (0.0MB)
free = 13631488 (13.0MB)
0.0% used
# 老年代
PS Old Generation
capacity = 223870976 (213.5MB)
used = 0 (0.0MB)
free = 223870976 (213.5MB)
0.0% used
3169 interned Strings occupying 260032 bytes.
# 阶段2:创建了一个10MB空间的数组
D:\workspace\workspace_idea\mavenexer>jmap -heap 16016
Attaching to process ID 16016, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.201-b09
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 5337251840 (5090.0MB)
NewSize = 111673344 (106.5MB)
MaxNewSize = 1778909184 (1696.5MB)
OldSize = 223870976 (213.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 84410368 (80.5MB)
used = 18927080 (18.050270080566406MB) # 可以看到当前使用空间为18MB
free = 65483288 (62.449729919433594MB)
22.422695752256406% used
From Space:
capacity = 13631488 (13.0MB)
used = 0 (0.0MB)
free = 13631488 (13.0MB)
0.0% used
To Space:
capacity = 13631488 (13.0MB)
used = 0 (0.0MB)
free = 13631488 (13.0MB)
0.0% used
PS Old Generation
capacity = 223870976 (213.5MB)
used = 0 (0.0MB)
free = 223870976 (213.5MB)
0.0% used
3170 interned Strings occupying 260080 bytes.
# 阶段3:进行了一次垃圾回收
D:\workspace\workspace_idea\mavenexer>jmap -heap 16016
Attaching to process ID 16016, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.201-b09
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 5337251840 (5090.0MB)
NewSize = 111673344 (106.5MB)
MaxNewSize = 1778909184 (1696.5MB)
OldSize = 223870976 (213.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 84410368 (80.5MB)
used = 1688224 (1.610015869140625MB) # 可以看到当前只使用了一点多MB空间大小
free = 82722144 (78.88998413085938MB)
2.0000197132181676% used
From Space:
capacity = 13631488 (13.0MB)
used = 0 (0.0MB)
free = 13631488 (13.0MB)
0.0% used
To Space:
capacity = 13631488 (13.0MB)
used = 0 (0.0MB)
free = 13631488 (13.0MB)
0.0% used
PS Old Generation
capacity = 223870976 (213.5MB)
used = 1302384 (1.2420501708984375MB)
free = 222568592 (212.25794982910156MB)
0.581756520327137% used
3156 interned Strings occupying 259088 bytes.
使用jconsole可以实时进行监控:并且在右边我们可以执行GC垃圾回收
4.4、jvisualvm工具
案例引出
接下来我们借助一个案例来引出这个jvisualvm工具,它能够给我们具体分析出指定方法中哪个类里的实例大小。
public class Main {
public static void main(String[] args) throws InterruptedException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) { //200MB
students.add(new Student());
// Student student = new Student();
}
Thread.sleep(1000000000L);
}
}
class Student {
private byte[] big = new byte[1024*1024];//1MB
}
该程序在运行时,会向集合中不断添加对象,每个对象内存空间为1MB,那么最终就会添加200MB内存的空间,我们先使用jconsole来看一下:
若是想要具体分析哪里产生出这么大的内存空间,光只是使用jconsole是不够的,接着我们来进行使用jvisualvm
工具。
jvisualvm工具
该工具也是jdk自带的,我们只需要执行jvisualvm
即可:
可以看到下面有堆Dump按钮,点击它可进行堆转储,将堆内存快照抓取下来进行对堆进行分析,我们来进行查询最大的前20个对象:
此时我们点击进入查看详细内容,可以看到问题原因就出在了这个集合中
五、方法区(公共)
5.1、介绍与组成
介绍
所有java虚拟机中线程的共享区域。存储了跟类的结构相关的信息,如成员变量、方法数据、成员方法与构造器方法代码部分,特殊方法(类的构造器)、运行时常量池。
方法区在虚拟机启动时被创建。方法区逻辑上是堆的组成部分(概念上定义了方法区),具体的jvm厂商不一定会坚持jvm逻辑上的定义,不同的厂商实现方式上不一样。
- 例如Hotspot:JDK8以前就是使用了一个永久代,就是使用了堆的一部分来作为方法区。
- 在1.8以后,将永久代移除了,换了个名字叫做元空间,用的不是堆的内存,而是本地内存、操作系统的内存空间。
方法区内存溢出定义:方法区若是申请内存时发现不足了,也会让虚拟机抛出一个内存溢出的错误。
方法区组成(HotSpot)
JDK1.6
:可看到方法区是属于在JVM中,由JVM来进行管理的,此时的常量池在方法区里。
JDK1.8
:方法区已经不再jvm中,被移动到本地内存里,由操作系统来进行管理,并且可以看到常量池被移动到了堆中。
5.2、方法区内存溢出
元空间默认不会设置上限。
应用场景:在使用一些框架时就会进行动态加载一些代理类,若是在此过程中出现了内存溢出,我们要去查看一下是否是框架使用不当而导致的结果。
下面的代码是使用ClassWriter
来生成字节码的一系列信息,接着让类加载器进行加载,这一过程就是在给方法区添加类的结构信息:
JDK1.8
JDK1.8:在jdk1.8中并不是在jvm虚拟机范围,而是由操作系统来进行管理,叫做元空间,此时并没有给其设置最大上限,所以我们需要设置一个指定的元空间大小,使用该参数:-XX:MaxMetaspaceSize=8m
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;
/**
* 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MaxMetaspaceSize=8m
*/
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 10000; i++, j++) {
// ClassWriter 作用是生成类的二进制字节码
ClassWriter cw = new ClassWriter(0);
// 版本号, public, 类名, 包名, 父类, 接口
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回 byte[]
byte[] code = cw.toByteArray();
// 执行了类的加载
test.defineClass("Class" + i, code, 0, code.length); // Class 对象
}
} finally {
System.out.println(j);
}
}
}
JDK1.6
JDK1.6:在jdk1.6中叫做永久代,其是在堆中的,要是想设定值需要使用其他的参数:-XX:MaxPermSize=8m
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;
/**
* 演示永久代内存溢出 java.lang.OutOfMemoryError: PermGen space
* -XX:MaxPermSize=8m
*/
public class Demo1_8 extends ClassLoader {
public static void main(String[] args) {
int j = 0;
try {
Demo1_8 test = new Demo1_8();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter cw = new ClassWriter(0);
cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
byte[] code = cw.toByteArray();
test.defineClass("Class" + i, code, 0, code.length);
}
} finally {
System.out.println(j);
}
}
}
测试结果:在我本地竟然没有测出来jdk1.6的,无论设置多小都能够正常加载20000个。
5.3、运行时常量池
常量池的作用:就是给我们的指令提供一些常量符号,根据常量符号以查表的方式来查到它,这样虚拟指令才能够查到并执行。
下面是我们要进行反编译的java代码:
public class Main{ // 注释
public static void main(String[] args) {
System.out.println("Hello,world!");
}
}
执行反编译命令:javap -v 类名.class
,反编译java字节码文件,-v表示显示详细信息。
//1、类的基本信息
Classfile /D:/workspace/workspace_idea/mavenexer/target/classes/com/changlu/JVM/Main.class
Last modified 2021-11-15; size 548 bytes //修改时间
MD5 checksum bd7bd37fdebcba045aeaafc9a6251d35 //签名
Compiled from "Main.java"
public class com.changlu.JVM.Main //类的信息
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER //类的访问修饰符
//2、常量池
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // Hello,world!
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/changlu/JVM/Main
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/changlu/JVM/Main;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 Main.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 Hello,world!
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/changlu/JVM/Main
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
//3、类的方法定义
public com.changlu.JVM.Main(); //默认构造
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/changlu/JVM/Main;
public static void main(java.lang.String[]); //main方法
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
//4、虚拟机指令
0: getstatic #2 //获取静态变量 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 //加载一个参数 // String Hello,world!
5: invokevirtual #4 //执行虚方法调用 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "Main.java"
可以看到67-69行中的#数字
,表示的是查表编号,在jvm使用解释器去执行时,会根据这个#数字
去常量池进行查表调用方法
- 点赞
- 收藏
- 关注作者
评论(0)