JVM内存结构
程序计数器
用于记住下一条执行指令的地址, 线程之间切换,获取到了时间片,将获取程序计数器中的指令进行执行。
并且线程私有
不会有内存溢出问题
虚拟机栈(栈)
先进后出, 存储非native的Java方法(栈帧),执行新方法时会将方法放入栈中,并且执行完reutrn或者抛出异常,将会弹栈
设置大小 -Xss2024k
本地方法栈
与虚拟机栈最大区别是存入的是native方法
内存溢出
栈内存溢出: 递归调用方法,栈帧过大(栈帧容量大于栈内存)
堆内存溢出:
线程运行诊断
用top定位那个进程对cpu的占用过高或死锁
ps H -eo pid,tid,%cpu | grep 进程id (来定位那个线程导致的)
jstack 进程id (其中的线程id是16进制的,ps出来的是10进制)
最终可定位到代码行数
堆
通过new关键字,创建的对象
线程共享,存在线程安全问题
存在垃圾回收机制
设置大小 -Xmx 1024k
堆内存溢出
大量新建对象存在堆中无法得到回收
1 2 3 4 5 6 List list = new ArrayList <>();String str = "hello" ;for (;;) { str += "hello" ; list.add(str); }
堆内存诊断
jps工具
jmap工具 jmap -heap 进程id
jconsole工具
jvisualvm 工具(推荐)
方法区(元空间)
方法区是规范,永久代和元空间是实现
主要存储类的信息
方法区内存溢出
类信息加载过多会出现方法区溢出,但是1.8JDK方法区使用的本地内存,需要设置元空间大小。就能出现元方法区溢出问题
-XX:MaxMetaspaceSize=N 1.8
-XX:MaxPermSize=N 1.8之前
运行时常量池
Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有用于存放编译期生成的各种字面量(Literal)和符号引用(Symbolic Reference)的 常量池表(Constant Pool Table)
通过这个常量池表,指令就可以去和这个表对应匹配执行指令
字符串常量池(Stringtable)
JDK1.6中StringTable是放在永久代中
1.8、1.7是放在堆中的
并且 StringTable的大小会影响性能, -XX: StringTableSize = n(因为StringTable本质是一个HashTable)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 String s1 = "a" ;String s2 = "b" ;String s3 = "ab" ;String s4 = s1 + s2;String s5 = "a" + "b" ;System.out.println(s3 == s4); System.out.println(s3 == s5); String s1 = new String ("a" ) + new String ("b" );System.out.println(s1 == "ab" ); String intern = s1.intern();System.out.println(intern == "ab" ); String s1 = new String ("a" ) + new String ("b" );String s2 = s1.intern();System.out.println(s1 == "ab" ); System.out.println(s2 == "ab" ); String s1 = new String ("a" ) + new String ("b" );System.out.println(s1 == "ab" ); String s2 = s1.intern(); System.out.println(s1 == "ab" );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 String s1 = new String("a") + new String("b");// 反编译 PS F:\idea\code\jvm\target\classes\heapoverflow> javap -v .\Test.class Classfile /F:/idea/code/jvm/target/classes/heapoverflow/Test.class Last modified 2023-9-10; size 671 bytes MD5 checksum 7b08566e21524c96238adc5e4e5c9627 Compiled from "Test.java" public class heapoverflow.Test minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: # 1 = Methodref # 2 = Class # 3 = Methodref # 4 = Class # 5 = String # 6 = Methodref # 7 = Methodref # 8 = String # 9 = Methodref # 10 = Class # 11 = Class # 12 = Utf8 <init> # 13 = Utf8 ()V # 14 = Utf8 Code # 15 = Utf8 LineNumberTable # 16 = Utf8 LocalVariableTable # 17 = Utf8 this # 18 = Utf8 Lheapoverflow/Test; # 19 = Utf8 main # 20 = Utf8 ([Ljava/lang/String;)V # 21 = Utf8 args # 22 = Utf8 [Ljava/lang/String; # 23 = Utf8 s1 # 24 = Utf8 Ljava/lang/String; # 25 = Utf8 SourceFile # 26 = Utf8 Test.java # 27 = NameAndType # 28 = Utf8 java/lang/StringBuilder # 29 = Utf8 java/lang/String # 30 = Utf8 a # 31 = NameAndType # 32 = NameAndType # 33 = Utf8 b # 34 = NameAndType # 35 = Utf8 heapoverflow/Test # 36 = Utf8 java/lang/Object # 37 = Utf8 (Ljava/lang/String;)V # 38 = Utf8 append # 39 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; # 40 = Utf8 toString # 41 = Utf8 ()Ljava/lang/String; { public heapoverflow.Test(); 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 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lheapoverflow/Test; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=4, locals=2, args_size=1 0: new #2 // class java/lang/StringBuilder 3: dup 4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V 7: new #4 // class java/lang/String 10: dup 11: ldc #5 // String a 13: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 19: new #4 // class java/lang/String 22: dup 23: ldc #8 // String b 25: invokespecial #6 // Method java/lang/String."<init>":(Ljava/lang/String;)V 28: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 31: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; // 最终还是new String("ab") 34: astore_1 35: return LineNumberTable: line 10: 0 line 11: 35 LocalVariableTable: Start Length Slot Name Signature 0 36 0 args [Ljava/lang/String; 35 1 1 s1 Ljava/lang/String; }
直接内存
不受JVM管理
会有泄露问题
多用于文件传输NIO
需要手动去释放分配的内存(借用了虚引用)
直接内存因为不需要通过系统内存,和Java堆内存而是另分配一段内存来提升速度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ByteBuffer allocate = ByteBuffer.allocateDirect(1024 );public static ByteBuffer allocateDirect (int capacity) { return new DirectByteBuffer (capacity); } DirectByteBuffer(int cap) { super (-1 , 0 , cap, cap); boolean pa = VM.isDirectMemoryPageAligned(); int ps = Bits.pageSize(); long size = Math.max(1L , (long )cap + (pa ? ps : 0 )); Bits.reserveMemory(size, cap); long base = 0 ; try { base = unsafe.allocateMemory(size); } catch (OutOfMemoryError x) { Bits.unreserveMemory(size, cap); throw x; } unsafe.setMemory(base, size, (byte ) 0 ); if (pa && (base % ps != 0 )) { address = base + ps - (base & (ps - 1 )); } else { address = base; } cleaner = Cleaner.create(this , new Deallocator (base, size, cap)); att = null ; }
垃圾回收 如何判定对象是否能回收
引用计数法(A、B对象互相引用会导致无法回收掉;)
对象每被引用一次计数就加1,否则减1
可达性分析法
分析对象之间的引用关系来确定哪些对象可以被程序访问到(即可达),哪些对象不再可达,从而判断哪些对象可以被回收
GC Roots 是一组根对象,它们是程序中的起始点,通过它们可以追踪到所有的可达对象
从 GC Roots 开始,通过对象之间的引用链进行遍历,找到所有可以从 GC Roots 访问到的对象,这些对象被认为是可达的。如果一个对象不可达(即没有引用链可以连接到它),那么它被认为是不可达的,可以被回收。
五种引用 强引用
能通过GC Root引用链找到对象,就说明不会被垃圾回收
弱引用
没有强引用,并且不管内存还够不够,都会去回收,最终弱引用将加入引用队列等待被回收
软引用
强引用都没有,并且内存不够时才会去回收,最终软引用被加入引用队列等待回收
虚引用
须配合引用队列,将虚引用Cleaner放入引用队列, 然后Reference Handler线程会去看引用队列中是否有Cleaner,如果发现有,就会去调用Cleaner的clean方法将分配的直接内存通过Unsafe.freeMemory方法释放掉。最终将虚引用Cleaner也释放掉
终结器引用
须配合引用队列,终接器引用被方法引用队列,当它被释放时,会调用对象的finallize方法,当下一次GC时这个对象就会被GC回收掉
软引用-引用队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main (String[] args) { List<SoftReference<String>> list = new ArrayList <>(); ReferenceQueue<String> queue = new ReferenceQueue <>(); SoftReference softReference = new SoftReference <String>(new String ("" ), queue); list.add(softReference); Reference<?> poll = queue.poll(); while (poll != null ) { list.remove(poll); poll = queue.poll(); } }
弱引用 1 2 3 4 5 6 7 8 9 10 List<WeakReference<String>> list = new ArrayList <>(); ReferenceQueue<String> queue = new ReferenceQueue <>(); WeakReference WeakReference = new WeakReference <String>(new String ("" ), queue);list.add(WeakReference); Reference<?> poll = queue.poll(); while (poll != null ) { list.remove(poll); poll = queue.poll(); }
垃圾回收算法 标记-清除
通过GC Root引用链找到需要清除的对象进行标记,然后再清除
速度快,但是清除的空间不连续,会产生内存碎片, 如果再放入一个数组就放不下了
标记-整理
不会产生内存碎片,但是整理需要时间,故效率较低(当整理存在的对象的地址时,会花更多的时间去处理对象)
复制
不会产生内存碎片,但是需要双倍的内存空间来复制存活的对象
将FROM存活的对象复制到TO上,然后TO再和FROM交换
分代算法
刚开始对象会放在新生代中的伊甸园区
如果新生代容量满了,会触发minor GC,GC的同时会将所有的非GC线程停止住, 此时会对伊甸园区中进行标记,并使用复制算法将存活的对象复制到幸存区To中,且将年龄加1,此时会将幸存区To与幸存区From进行交换
如果幸存区和伊甸园都满了,并且对象的年龄到达阈值(15)。会将对象加入到老年代中。
如果新生代和老年代都满了,会触发Full GC。如果GC后还是装不下新对象就会触发OOM了
如果特别大的对象,会直接到老年代还是装不下就OOM,其中也会触发GC
相关VM参数
垃圾回收器 串行
单线程
堆内存较小
GC线程开始回收, 会导致其他用户线程阻塞,直到GC线程回收完成后才能继续运行
吞吐量优先
多线程,多核cpu
区间内STW时间最短, 发生GC次数少,堆内存较大、
发生垃圾回收的时候,会创建多个GC线程同时回收垃圾,并且回收的同时CPU会迅速跑到100%
响应时间优先 CMS
多线程,多核cpu
单位时间STW最短, 频繁发生GC,堆内存较大
垃圾回收的时候,GC线程会先进行标记,此时其他用户线程也会进行阻塞,然后其他用户线程就可以与GC线程并行执行,GC线程继续标记,重新标记的时候又会使用户线程进行阻塞,最后用户线程又可以并行执行,GC线程开始回收垃圾。GC线程回收垃圾的同时,其他用户线程也会生产垃圾。回收失败也会造成CPU急速增加到100%
G1
Garbage First
回收阶段
先是新生代收集,然后是新生代收集 + 并发标记,然后混合收集。随后循环执行
Young Collection G1垃圾回收器会将堆内存划分为多个区域,每个区域可以是伊甸园、幸存区和老年代
当伊甸园被占满了,会触发新生代的垃圾回收,并且引起STW。将伊甸园中的幸存的对象复制到幸存区。
当幸存区内存紧张的时候,会将幸存区中的年龄大于一定数的幸存对象放入到老年代中。
Young Collection + CM 当老年代内存占用达到阈值时,会进行并发标记,不会STW
Mixed Collection 对伊甸园、幸存区、老年代进行垃圾回收,并且在混合收集中会优先收集那些垃圾最多的区域。故优先收集老年代
最终标记,会引起STW
拷贝存活,会引起STW
Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题
新生代回收时会去遍历GC ROOT,但是老年代中也会有引用了新生代的对象,此时通过脏卡来标记新生代对象。以便回收时不用全遍历老年代,提高效率
黑色处理完,存活
灰色正在处理,被黑色引用,存活
白色未处理,被灰色引用,存活
对于多线程操作,会对引用添加一个写屏障,并且将这个对象加入到队列中,等到并发标记结束后,将队列中的对象取出来进行重新标记
JDK 8u20 字符串去重
优点:节省大量内存
缺点:会造成轻微的cpu运算,新生代回收时间轻微增加
字符串底层使用的是char数组,G1会使新建的字符串如果内存相同就会指向相同的char数组引用
–
String的intern方法注重的是String对象(一个String对象)
而G1则是char数组对象(还是两个String对象)
1 2 String s1 = new String ("abc" )String s2 = new String ("abc" )
JDK 8u40 并发标记卸载
所有对象都经过并发标记后,就能知道那些类不再使用,当一个类加载器的所有类都不在使用,则卸载它所有加载的类
JDK 8u60 回收巨型对象
一个对象大于region的一半时,称为巨型对象
G1 不会对巨型对象进行拷贝
回收时被优先考虑
G1会跟踪老年代所有incoming引用,当老年代的incomeing引用为0的巨型对象就可以在新生代回收的时候被处理掉
垃圾回收调优 查看当前配置信息
java -XX:+PrintFlagsFinal -version | grep GC
java -XX:+PrintFlagsFinal -version | findstr GC
相关工具
jmap, jconsole….
确定目标
低延迟还是高吞吐量
CMS, G1, ZGC 低延迟
parallelGC 高吞吐量
最快的GC是不发生GC
查看FullGC前后内存的占用,然后考虑一下问题
数据太多
数据表示太臃肿
对象图
对象大小
是否内存泄露
新生代调优
TLAB thread-local allocation buffer
死亡对的回收代价是零
大部分用过就死
推荐修改新生代堆内存大小(25%-50% 堆内存)
并发数 * (请求数据 + 相应数据)
晋升阈值配置得当,让长时间存活的对象尽快到老年代
老年代调优 观察再查看是否是老年代造成的;如果是再调大老年代内存
JVM规范类文件结构
多态原理步骤
当执行 invokevirtual指令时
通过栈找到具体对象地址
分析对象头,找到对象Class
根据Class中的vtable(虚方法表, 加载类链接阶段就处理好了), 找到具体方法的地址
执行方法
类加载 1、加载
将类的字节码载入到方法区中,内部采用C++的instanceKlass描述java类,它的重要field有:
_java_mirror即java的类镜像,例如对String来说,就是String.class,作用是把klass暴露给java使用
_super即父类
_fileds 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
如果这个类还有父类没有加载,就先加载父类
加载和链接可能是交替运行的
2、链接 1、验证
对类型文件进行验证,是否是Java文件,例如魔数验证
2、准备
static变量在JDK7之前存储在instanceKlass后, JDK7开始存储与class对象后
static变量分配空间和赋值是两个步骤,分配空间在准备阶段,赋值在初始化阶段
如果static变量是final的基本类型,以及字符串常量,那么在编译阶段就确定了,赋值在准备阶段完成
如果static变量是final的,但属于引用类型,那么赋值会在初始化阶段完成
3、解析
将常量池中的符号引用解析为直接引用
3、初始化 发生时机
类加载器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null ) { long t0 = System.nanoTime(); try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { long t1 = System.nanoTime(); c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }