JVM内存处理全流程
实例
-
package ex2; /** * @author King老师 * VM参数 * -Xms30m -Xmx30m -XX:MaxMetaspaceSize=30m * */ public class JVMObject { public final static String MAN_TYPE = "man"; // 常量 public static String WOMAN_TYPE = "woman"; // 静态变量 public static void main(String[] args)throws Exception { Teacher T1 = new Teacher(); T1.setName("Mark"); T1.setSexType(MAN_TYPE); T1.setAge(36); for(int i =0 ;i<15 ;i++){ System.gc();//主动触发GC 垃圾回收 15次--- T1存活 } Teacher T2 = new Teacher(); T2.setName("King"); T2.setSexType(MAN_TYPE); T2.setAge(18); Thread.sleep(Integer.MAX_VALUE);//线程休眠 } } class Teacher{ String name; String sexType; int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getSexType() { return sexType; } public void setSexType(String sexType) { this.sexType = sexType; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
-
-Xms30m:堆的起始大小是30M
-
-Xmx30m:堆的最大大小是30M
-
-XX:MaxMetaspaceSize=30m:元空间最大大小是30M
-
-XX:+UseConcMarkSweepGC:使用cms垃圾回收器
-
-XX:-UseCompressedOops:禁止压缩指针的使用,因为虚拟机有优化技术,对吧对象头进行指针压缩,不方便我们观察对象头
申请流程
-
1.JVM申请内存
- 上面输入的VM参数
-
2.初始化运行时数据区
- 方法区和堆就被创建出来了
-
3.类加载
-
JVMObject这个类被加载进来
-
JRE很多jar包也要加载进来
-
方法区主要存放的是class、静态变量、常量
对应上面的例子:JVMObject.class、Teacher.class、WOMAN_TYPE、MAN_TYPE
-
-
4.执行方法
-
运行main方
-
main方法
1.启动线程创建虚拟机栈,栈帧-main被压入虚拟机栈
2.Teacher T1 = new Teacher(),在堆里面创建一个Teacher对象,在栈里面的局部变量表里存放T1,代表Teacher对象的引用,指向Teacher对象
3.Teacher T2 = new Teacher(),在堆里面创建一个Teacher对象,在栈里面的局部变量表里存放T2,代表Teacher对象的引用,指向Teacher对象
-
-
5.创建对象
从底层深入理解运行时数据区
堆空间分代划分
-
新生代
-
Eden
Teacher1、Teacher2
-
From
-
To
-
-
老年代
- Tenured
GC概念
-
垃圾回收
-
在JVM中垃圾回收是自动的
-
System.gc(),主动触发gc,不推荐这样的代码,非常影响性能
JHSDB工具
准备工作
-
可视化和命令行工具,来查看JVM的运行信息
-
java写的jdk的一个工具
-
启动前保证/jdk/bin和/jre/bin目录下都有sawindbg.dll这个文件
-
然后在/jdk/lib目录下,打开命令行工具,输入java -cp .\sa-jdi.jar sun.jvm.hotspot.HSDB
-
启动G:\enjoy\JVM\ref-jvm3项目下的JVMObject类
-
打开命令行工具,输入jps,看到JVMObject类的进程号为xx,此时测试为10588
查看指定进程
- 在HSDB工具中,打开File->Attach to Hotspot process,就可以启动的进程,里面有很多的线程在运行
- 点击main线程,就可以该线程的具体运行情况
查看对象
- 所有类的信息,打开Tools->Object Histogram,
- 在输入栏中输入全路径包名,就可以找到上面定义的Teacher
- 当前测试数据,1380000000–King对象,142de950–Mark对象
查看堆的参数
-
打开Tools->Heap Parameters
-
Heap Parameters: Gen 0: eden [0x0000000013800000,0x00000000139a8080,0x0000000014000000) space capacity = 8388608, 20.70465087890625 used from [0x0000000014000000,0x0000000014000000,0x0000000014100000) space capacity = 1048576, 0.0 used to [0x0000000014100000,0x0000000014100000,0x0000000014200000) space capacity = 1048576, 0.0 usedInvocations: 0 Gen 1: concurrent mark-sweep generation free-list-space[ 0x0000000014200000 , 0x0000000015600000 ) space capacity = 20971520 used(4%)= 920488 free= 20051032 Invocations: 15
-
Eden:起始地址13800000~14000000
From:起始地址14000000~14100000
To :起始地址14100000~14200000
Old :起始地址14200000~15600000
-
King对象在Eden区,Mark对象在老年代
$为什么Mark对象在老年代?
(1).我们执行了15次System.gc(),进行了15次垃圾回收,但是Mark对象并不会被回收,是存活的,因为这个对象一直被T1引用,不能被回收
(2).T1首先在Eden区,经过一次垃圾回收进入From区,然后一直在From和To之间来回存在,会有一个分代年龄,达到15次以后就会进入老年代
-
JVM的堆是分代划分的,这种划分是连续的
-
所有对象都是优先在Eden区分配
-
查看栈
-
在线程列表信息中,点击一下main线程,然后再点击标签栏上的第二项Stack memory
-
┌ 类似于这样一个符号框住的部分就是一个栈帧,所以当前栈里面有两个栈帧
-
第一个栈帧-----当前栈帧,同时是一个native方法栈帧,此时这个栈里面栈顶是一个栈帧,是一个native方法,因为是调用的Thead.sleep方法,这就是一个native方法
-
第二个栈帧-----是一个java方法栈帧,也就是main方法的栈帧,
(1)黑色箭头是操作数栈
-
虚拟机优化技术
栈的优化技术
- 栈帧信息
- 操作数栈
- 局部变量表
实例
-
package ex2; /** * @author King老师 * VM参数 * JVM对栈帧空间的优化 * **/ public class JVMStack { public int work(int x) throws Exception{ int z =(x+5)*10;//局部变量表有, 32位 Thread.sleep(Integer.MAX_VALUE); return z; } public static void main(String[] args)throws Exception { JVMStack jvmStack = new JVMStack(); jvmStack.work(10);//10 放入main栈帧 10 ->操作数栈 } }
-
main方法执行时,10要被放到main栈帧的操作数栈中
-
在HSDB工具中,发现main栈帧和work栈帧中间有一个内存地址重合了,实现了数据的共享,也就是main栈帧的操作数栈和work栈帧的局部变量表共享了10这个基础类型的变量,节约了内存空间
深入辨析堆和栈
栈
存储内容
- 主要是虚拟机栈
- 存储的内容主要是基础的八大数据类型,以及对象的引用
- 方法执行完了,内存就会释放掉
与线程的关系
- 是线程私有的,对于其他线程不可见
空间大小
- 默认大小是1M,同时跑1000个线程才占1G
堆
存储内容
- 存储的是对象,几乎所有的对象都是存储在堆中的
- new一个对象时,类中的成员变量,如果基础类型,也是在堆中的
与线程的关系
- 线程共享,对其余线程可见
空间大小
- 远远大于栈的,性能调优和垃圾回收主要关注的是栈
内存溢出
- OOM,OutOfMemoryError
栈溢出
- 情况1:死递归,没有出口的递归,不断的创建栈帧,超过1M后报错
- 情况2:机器有2个g,堆占1个g,方法区占800M,我们跑200个线程,占200M,如果同时跑201个线程就超过计算机的内存了,但是这种情况模拟不了,计算机会死机,因为栈区整体不能被限制,只能限制单个虚拟机栈的大小
堆溢出
-
-XX:+PrintGCDetails
- 打印GC详细信息
-
情况1:假设堆设置最大30M,new String[35000000],创建一个35M的对象
-
package ex2.oom; /** * @author King老师 * VM Args:-Xms30m -Xmx30m -XX:+PrintGCDetails * 堆内存溢出(直接溢出) */ public class HeapOom { public static void main(String[] args) { String[] strings = new String[35*1000*1000]; //35m的数组(堆) } }
- OutOfMemoryError:Java heap space
- 这就是直接就溢出了
-
情况2:写一个死循环,不停的往list里面添加元素
-
package ex2.oom; import java.util.LinkedList; import java.util.List; /** * @author King老师 * VM Args:-Xms30m -Xmx30m 堆的大小30M * 造成一个堆内存溢出(分析下JVM的分代收集) * GC调优---生产服务器推荐开启(默认是关闭的) * -XX:+HeapDumpOnOutOfMemoryError */ public class HeapOom2 { public static void main(String[] args) throws Exception { List<Object> list = new LinkedList<>(); // list 当前虚拟机栈(局部变量表)中引用的对象 int i =0; while(true){ i++; if(i%1000==0) Thread.sleep(10); list.add(new Object());// 不能回收2, 优先回收再来抛出异常。 } } }
-
OutOfMemoryError:GC overhead limit exceeded
##如果垃圾回收占据98%的资源,回收率不足2%
$$发生这个错有可能是内存泄漏造成的
-
这种异常首先打印普通gc,然后一片片的Full gc,然后回收不动了
为什么不断的gc呢?
因为对象是在慢慢增长的,不是一次性把堆搞垮的,JVM认为这是正常的,所以会进行gc,但是gc很多次后,发现毫无作用,就会抛出这个异常
-
方法区溢出
-
使用cglib,写一个while循环,不断的去加载这个类
-
package ex2.oom; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * 方法区导致的内存溢出 * VM Args: -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M * */ public class MethodAreaOutOfMemory { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(TestObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object arg0, Method arg1, Object[] arg2, MethodProxy arg3) throws Throwable { return arg3.invokeSuper(arg0, arg2); } }); enhancer.create(); } } public static class TestObject { private double a = 34.53; private Integer b = 9999999; } }
-
-XX:MetaspaceSize=10M
- 方法区大小
-
-XX:MaxMetaspaceSize=10M
- 方法区最大
-
抛出异常OutOfMemoryError:metaspace
本机直接内存溢出
-
没有被JVM虚拟化的称为直接内存
-
package ex2.oom; import java.nio.ByteBuffer; /** * @author King老师 * VM Args:-XX:MaxDirectMemorySize=100m * 限制最大直接内存大小100m */ public class DirectOom { public static void main(String[] args) { //直接分配128M的直接内存 ByteBuffer bb = ByteBuffer.allocateDirect(128*1024*1204); } }
-
-XX:MaxDirectMemorySize=100m
- 限制直接内存最大100M
-
抛出错误OutOfMemoryError:Direct buffer memory
常量池
-
运行时常量池一定是放在方法区中间的,这是虚拟机规范里面定义的
-
class常量池,就是class对象里面的东西
-
javap -v JVMObject.class
-
Classfile /G:/enjoy/JVM/ref-jvm3/out/production/ref-jvm3/ex2/JVMObject.class Last modified 2021-3-11; size 1071 bytes MD5 checksum 630079e5e3c9aa2c35874d2e4c645eec Compiled from "JVMObject.java" public class ex2.JVMObject minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #18.#46 // java/lang/Object."<init>":()V #2 = Class #47 // ex2/Teacher #3 = Methodref #2.#46 // ex2/Teacher."<init>":()V #4 = String #48 // Mark #5 = Methodref #2.#49 // ex2/Teacher.setName:(Ljava/lang/String;)V #6 = Class #50 // ex2/JVMObject #7 = String #51 // man #8 = Methodref #2.#52 // ex2/Teacher.setSexType:(Ljava/lang/String;)V #9 = Methodref #2.#53 // ex2/Teacher.setAge:(I)V #10 = Methodref #54.#55 // java/lang/System.gc:()V #11 = String #56 // King #12 = Class #57 // java/lang/Integer #13 = Long 2147483647l #15 = Methodref #58.#59 // java/lang/Thread.sleep:(J)V #16 = String #60 // woman #17 = Fieldref #6.#61 // ex2/JVMObject.WOMAN_TYPE:Ljava/lang/String; #18 = Class #62 // java/lang/Object #19 = Utf8 MAN_TYPE #20 = Utf8 Ljava/lang/String; #21 = Utf8 ConstantValue #22 = Utf8 WOMAN_TYPE #23 = Utf8 <init> #24 = Utf8 ()V #25 = Utf8 Code #26 = Utf8 LineNumberTable #27 = Utf8 LocalVariableTable #28 = Utf8 this #29 = Utf8 Lex2/JVMObject; #30 = Utf8 main #31 = Utf8 ([Ljava/lang/String;)V #32 = Utf8 i #33 = Utf8 I #34 = Utf8 args #35 = Utf8 [Ljava/lang/String; #36 = Utf8 T1 #37 = Utf8 Lex2/Teacher; #38 = Utf8 T2 #39 = Utf8 StackMapTable #40 = Class #47 // ex2/Teacher #41 = Utf8 Exceptions #42 = Class #63 // java/lang/Exception #43 = Utf8 <clinit> #44 = Utf8 SourceFile #45 = Utf8 JVMObject.java #46 = NameAndType #23:#24 // "<init>":()V #47 = Utf8 ex2/Teacher #48 = Utf8 Mark #49 = NameAndType #64:#65 // setName:(Ljava/lang/String;)V #50 = Utf8 ex2/JVMObject #51 = Utf8 man #52 = NameAndType #66:#65 // setSexType:(Ljava/lang/String;)V #53 = NameAndType #67:#68 // setAge:(I)V #54 = Class #69 // java/lang/System #55 = NameAndType #70:#24 // gc:()V #56 = Utf8 King #57 = Utf8 java/lang/Integer #58 = Class #71 // java/lang/Thread #59 = NameAndType #72:#73 // sleep:(J)V #60 = Utf8 woman #61 = NameAndType #22:#20 // WOMAN_TYPE:Ljava/lang/String; #62 = Utf8 java/lang/Object #63 = Utf8 java/lang/Exception #64 = Utf8 setName #65 = Utf8 (Ljava/lang/String;)V #66 = Utf8 setSexType #67 = Utf8 setAge #68 = Utf8 (I)V #69 = Utf8 java/lang/System #70 = Utf8 gc #71 = Utf8 java/lang/Thread #72 = Utf8 sleep #73 = Utf8 (J)V { public static final java.lang.String MAN_TYPE; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String man public static java.lang.String WOMAN_TYPE; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC public ex2.JVMObject(); 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 10: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lex2/JVMObject; public static void main(java.lang.String[]) throws java.lang.Exception; descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: new #2 // class ex2/Teacher 3: dup 4: invokespecial #3 // Method ex2/Teacher."<init>":()V 7: astore_1 8: aload_1 9: ldc #4 // String Mark 11: invokevirtual #5 // Method ex2/Teacher.setName:(Ljava/lang/String;)V 14: aload_1 15: ldc #7 // String man 17: invokevirtual #8 // Method ex2/Teacher.setSexType:(Ljava/lang/String;)V 20: aload_1 21: bipush 36 23: invokevirtual #9 // Method ex2/Teacher.setAge:(I)V 26: iconst_0 27: istore_2 28: iload_2 29: bipush 15 31: if_icmpge 43 34: invokestatic #10 // Method java/lang/System.gc:()V 37: iinc 2, 1 40: goto 28 43: new #2 // class ex2/Teacher 46: dup 47: invokespecial #3 // Method ex2/Teacher."<init>":()V 50: astore_2 51: aload_2 52: ldc #11 // String King 54: invokevirtual #5 // Method ex2/Teacher.setName:(Ljava/lang/String;)V 57: aload_2 58: ldc #7 // String man 60: invokevirtual #8 // Method ex2/Teacher.setSexType:(Ljava/lang/String;)V 63: aload_2 64: bipush 18 66: invokevirtual #9 // Method ex2/Teacher.setAge:(I)V 69: ldc2_w #13 // long 2147483647l 72: invokestatic #15 // Method java/lang/Thread.sleep:(J)V 75: return LineNumberTable: line 14: 0 line 15: 8 line 16: 14 line 17: 20 line 18: 26 line 19: 34 line 18: 37 line 21: 43 line 22: 51 line 23: 57 line 24: 63 line 25: 69 line 26: 75 LocalVariableTable: Start Length Slot Name Signature 28 15 2 i I 0 76 0 args [Ljava/lang/String; 8 68 1 T1 Lex2/Teacher; 51 25 2 T2 Lex2/Teacher; StackMapTable: number_of_entries = 2 frame_type = 253 /* append */ offset_delta = 28 locals = [ class ex2/Teacher, int ] frame_type = 250 /* chop */ offset_delta = 14 Exceptions: throws java.lang.Exception static { }; descriptor: ()V flags: ACC_STATIC Code: stack=1, locals=0, args_size=0 0: ldc #16 // String woman 2: putstatic #17 // Field WOMAN_TYPE:Ljava/lang/String; 5: return LineNumberTable: line 12: 0 } SourceFile: "JVMObject.java"
-
这里面的Constant pool,这个属于class常量池,也可以叫静态常量池
-
class常量池(静态常量池)
-
类的版本
-
有哪些字段,有哪些方法
-
如果是接口的话,还有接口描述
-
编译时存放的两个东西
- 一个是字面量,String a = “b”,int a = 13,b和13就是字面量
- 另一个是符号引用,Person类里面引用了Tools类,编译的时候,不知道Tools的真实内存地址,只能用符号引用代替Org.king.Tools(全路径名),类加载的时候就可以根据符号引用找到实际的内存地址
运行时常量池
- 直接引用放在运行时常量池,jdk1.7以后运行时常量池的实现可以放在堆上,但是逻辑上还是属于方法区
字符串常量池
- 官方资料里面是没有字符串常量池这个概念的
- 核心还是String类,所以应该弄清String类
String类分析(final)
-
两个对象
- 主要是对char value[]的数组进行封装
- int hash(哈希值)
-
final char value[]数组;Stirng对象的不可变
-
被final修饰,不可更改
为什么这么设计?
-
1.可变就不安全,可能被恶意修改
-
2.hash值也不会轻易改变,hash值也是唯一的,可以确保hashmap key-value的稳定
-
3.这种设计可以去实现字符串常量池
-
String的创建方式
第一种
- String str = “abc”;
- 编译加载时,会在常量池中创建常量,如果有了就不会创建了,而是直接返回这个字符串的引用
- “abc”运行时,返回常量池中的字符串引用
第二种
- String str = new String(”abc“)
- 编译加载时,会在常量池中创建常量“abc”
- new方法,会在堆空间创建String对象,并引用常量池中的字符串对象char数组"abc",并返回String对象引用
- 所以直接用String str = “abc”更好,可以少一个String对象
第三种
-
public void mode3(){ Location location = new Location(); location.setCity("深圳"); location.setRegion("南山"); }
-
直接在堆里面,不会在常量池中创建
第四种
- String str = “ab” +“cd"+“ef”
- 会生成3个对象,“ab”、“abcd”、“abcdef”,效率最低
- 编译器会自动优化成“abcded"
第五种
-
String str = "abcdef"; for(int i=0; i<1000; i++) { str = str + i; }
-
对于这种大循环,编译器会优化出StringBuilder,进行append拼接,如果就几个,就跟第四种是一样的
-
String str = "abcdef"; for(int i=0; i<1000; i++) { str = (new StringBuilder(String.valueOf(str)).append(i).toString()); }
-
第六种
-
String str = new String(“king”).intern();
String str2 = new String(“king”).inter();
- intern()会在字符串常量池中去检查,有直接返回引用,没有则在字符串常量池中进行创建
- a==b 输出true(这里需要认为是对象),因为调用了intern()方法,不会创建新的对象了
- 推特公司优化内存,“深圳”、“北京“相同的字符串常量
-
对象是堆中间的,引用是栈、方法区中的
-
==是比较值,当是对象,比较的是地址,equal比较的是否同一个对象
总结
-
1.字符串常量池在哪个区 方法区
-
2.“abc"重复,统一放在字符串常量池
-
3.inter()方法
// 去字符串常量池找到是否有等于该字符串的对象,如果有,直接返回对象的引用。
-
4.虚拟栈的优化
参数10是被调用方法的局部变量
如果改成对象引用,此时传递的是引用,一样可以共享
-
5.字符串常量池不会被垃圾回收,垃圾回收的主要对象是堆里面的对象
-
6.值传递、引用传递、int缓存,需要一节课讲清楚
-
7.生产环境频繁gc怎么排查?需要体系化的jvm知识
-
8.String如果不加final,就不是常量了,就不需要字符串常量池了,如果不加,就是一个对象,就放在堆里面了