ToB企服应用市场:ToB评测及商务社交产业平台

标题: JVM [打印本页]

作者: 九天猎人    时间: 2022-8-28 07:50
标题: JVM
JVM

一、什么是JVM

定义

Java Virtual Machine,JAVA程序的运行环境(JAVA二进制字节码的运行环境)
好处

比较

JVM JRE JDK的区别

二、内存结构

整体架构


1、程序计数器

作用

用于保存JVM中下一条所要执行的指令的地址
特点

2、虚拟机栈

定义

演示

代码
  1. public class Main {
  2.     public static void main(String[] args) {
  3.         method1();
  4.     }
  5.     private static void method1() {
  6.         method2(1, 2);
  7.     }
  8.     private static int method2(int a, int b) {
  9.         int c = a + b;
  10.         return c;
  11.     }
  12. }Copy
复制代码

在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点
问题辨析

内存溢出

Java.lang.stackOverflowError 栈内存溢出
发生原因
线程运行诊断

CPU占用过高
3、本地方法栈

一些带有native关键字的方法就是需要JAVA去调用本地的C或者C++方法,因为JAVA有时候没法直接和操作系统底层交互,所以需要用到本地方法
4、堆

定义

通过new关键字创建的对象都会被放在堆内存
特点

堆内存溢出

java.lang.OutofMemoryError :java heap space. 堆内存溢出
堆内存诊断

jps
jmap
jconsole
jvirsalvm
5、方法区

结构


内存溢出

常量池

二进制字节码的组成:类的基本信息、常量池、类的方法定义(包含了虚拟机指令)
通过反编译来查看类的信息
运行时常量池

常量池与串池的关系

串池StringTable

特征
用来放字符串对象且里面的元素不重复
  1. public class StringTableStudy {
  2.     public static void main(String[] args) {
  3.         String a = "a";
  4.         String b = "b";
  5.         String ab = "ab";
  6.     }
  7. }
复制代码
 
常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号,还没有成为java字符串
  1. 0: ldc           #2                  // String a
  2. 2: astore_1
  3. 3: ldc           #3                  // String b
  4. 5: astore_2
  5. 6: ldc           #4                  // String ab
  6. 8: astore_3
  7. 9: returnCopy
复制代码
 
当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)
当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中
当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中
最终StringTable [“a”, “b”, “ab”]
注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。
使用拼接字符串变量对象创建字符串的过程
  1. public class StringTableStudy {
  2.     public static void main(String[] args) {
  3.         String a = "a";
  4.         String b = "b";
  5.         String ab = "ab";
  6.         //拼接字符串对象来创建新的字符串
  7.         String ab2 = a+b;
  8.     }
  9. }
复制代码
 
  1. [/code]通过拼接的方式来创建字符串的过程是:StringBuilder().append(“a”).append(“b”).toString()
  2. 最后的toString方法的返回值是一个新的字符串,但字符串的值和拼接的字符串一致,但是两个不同的字符串,一个存在于串池之中,一个存在于堆内存之中
  3. [code]
复制代码
intern方法 1.8

调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
无论放入是否成功,都会返回串池中的字符串对象
注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象
  1. [/code][size=2]intern方法 1.6[/size]
  2. 调用字符串对象的intern方法,会将该字符串对象尝试放入到串池中
  3. [list]
  4. [*]如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中
  5. [*]如果有该字符串对象,则放入失败
  6. [/list]无论放入是否成功,都会返回串池中的字符串对象
  7. 注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
  8. [size=3]StringTable 垃圾回收[/size]
  9. StringTable在内存紧张时,会发生垃圾回收
  10. [size=3]StringTable调优[/size]
  11. [list]
  12. [*]因为StringTable是由HashTable实现的,所以可以适当增加HashTable桶的个数,来减少字符串放入串池所需要的时间
  13. [code]-XX:StringTableSize=xxxxCopy
复制代码
  • 考虑是否需要将字符串对象入池
    可以通过intern方法减少重复入池
    6、直接内存

    文件读写流程


    使用了DirectBuffer

    直接内存是操作系统和Java代码都可以访问的一块区域,无需将代码从系统内存复制到Java堆内存,从而提高了效率
    释放原理

    直接内存的回收不是通过JVM的垃圾回收来释放的,而是通过unsafe.freeMemory来手动释放
    通过
    1. //通过ByteBuffer申请1M的直接内存
    2. ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M);Copy
    复制代码
    申请直接内存,但JVM并不能回收直接内存中的内容,它是如何实现回收的呢?
    allocateDirect返回一个DirectByteBuffer对象,里面申请内存,通过虚引用对象释放内存
    DirectByteBuffer类
    1. DirectByteBuffer(int cap) {   // package-private
    2.    
    3.     super(-1, 0, cap, cap);
    4.     boolean pa = VM.isDirectMemoryPageAligned();
    5.     int ps = Bits.pageSize();
    6.     long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    7.     Bits.reserveMemory(size, cap);
    8.     long base = 0;
    9.     try {
    10.         base = unsafe.allocateMemory(size); //申请内存
    11.     } catch (OutOfMemoryError x) {
    12.         Bits.unreserveMemory(size, cap);
    13.         throw x;
    14.     }
    15.     unsafe.setMemory(base, size, (byte) 0);
    16.     if (pa && (base % ps != 0)) {
    17.         // Round up to page boundary
    18.         address = base + ps - (base & (ps - 1));
    19.     } else {
    20.         address = base;
    21.     }
    22.     cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); //通过虚引用,来实现直接内存的释放,this为虚引用的实际对象
    23.     att = null;
    24. }Copy
    复制代码
    这里调用了一个Cleaner的create方法,且后台线程还会对虚引用的对象监测,如果虚引用的实际对象(这里是DirectByteBuffer)被回收以后,就会调用Cleaner的clean方法,来清除直接内存中占用的内存
    1. public void clean() {
    2.        if (remove(this)) {
    3.            try {
    4.                this.thunk.run(); //调用run方法
    5.            } catch (final Throwable var2) {
    6.                AccessController.doPrivileged(new PrivilegedAction<Void>() {
    7.                    public Void run() {
    8.                        if (System.err != null) {
    9.                            (new Error("Cleaner terminated abnormally", var2)).printStackTrace();
    10.                        }
    11.                        System.exit(1);
    12.                        return null;
    13.                    }
    14.                });
    15.            }Copy
    复制代码
    对应对象的run方法
    1. public void run() {
    2.     if (address == 0) {
    3.         // Paranoia
    4.         return;
    5.     }
    6.     unsafe.freeMemory(address); //释放直接内存中占用的内存
    7.     address = 0;
    8.     Bits.unreserveMemory(size, capacity);
    9. }Copy
    复制代码
    直接内存的回收机制总结

    三、垃圾回收

    1、如何判断对象可以回收

    引用计数法

    弊端:循环引用时,两个对象的计数都为1,导致两个对象都无法被释放

    可达性分析算法

    五种引用


    强引用

    只有GC Root都不引用该对象时,才会回收强引用对象
    软引用

    当GC Root指向软引用对象时,在内存不足时,会回收软引用所引用的对象
    软引用的使用
    1. public class Demo1 {
    2.     public static void main(String[] args) {
    3.         final int _4M = 4*1024*1024;
    4.         //使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
    5.         List<SoftReference<byte[]>> list = new ArrayList<>();
    6.         SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
    7.     }
    8. }Copy
    复制代码
    如果在垃圾回收时发现内存不足,在回收软引用所指向的对象时,软引用本身不会被清理
    如果想要清理软引用,需要使用引用队列
    1. public class Demo1 {
    2.     public static void main(String[] args) {
    3.         final int _4M = 4*1024*1024;
    4.         //使用引用队列,用于移除引用为空的软引用对象
    5.         ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
    6.         //使用软引用对象 list和SoftReference是强引用,而SoftReference和byte数组则是软引用
    7.         List<SoftReference<byte[]>> list = new ArrayList<>();
    8.         SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);
    9.         //遍历引用队列,如果有元素,则移除
    10.         Reference<? extends byte[]> poll = queue.poll();
    11.         while(poll != null) {
    12.             //引用队列不为空,则从集合中移除该元素
    13.             list.remove(poll);
    14.             //移动到引用队列中的下一个元素
    15.             poll = queue.poll();
    16.         }
    17.     }
    18. }Copy
    复制代码
     
    自定义类加载器

    使用场景

    步骤

    破坏双亲委派模式

    6、运行期优化

    分层编译

    JVM 将执行状态分成了 5 个层次:
    profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等
    即时编译器(JIT)与解释器的区别

    对于大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。 执行效率上简单比较一下 Interpreter < C1 < C2,总的目标是发现热点代码(hotspot名称的由 来),并优化这些热点代码
    逃逸分析

    逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术
    逃逸分析的 JVM 参数如下:
    逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数
    对象逃逸状态
    全局逃逸(GlobalEscape)
    参数逃逸(ArgEscape)
    没有逃逸
    逃逸分析优化
    针对上面第三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化
    锁消除
    我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有当前线程使用,那么就会移除该对象的同步锁
    例如,StringBuffer 和 Vector 都是用 synchronized 修饰线程安全的,但大部分情况下,它们都只是在当前线程中用到,这样编译器就会优化移除掉这些锁操作
    锁消除的 JVM 参数如下:
    锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上
    标量替换
    首先要明白标量和聚合量,基础类型和对象的引用可以理解为标量,它们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象
    对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量,这就叫做标量替换。
    这样,如果一个对象没有发生逃逸,那压根就不用创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能
    标量替换的 JVM 参数如下:
    标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上
    栈上分配
    当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能
    方法内联

    内联函数

    内联函数就是在程序编译时,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体来直接进行替换
    JVM内联函数

    C++是否为内联函数由自己决定,Java由编译器决定。Java不支持直接声明为内联函数的,如果想让他内联,你只能够向编译器提出请求: 关键字final修饰 用来指明那个函数是希望被JVM内联的,如
    1. "F:\JAVA\JDK8.0\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"Copy
    复制代码
    总的来说,一般的函数都不会被当做内联函数,只有声明了final后,编译器才会考虑是不是要把你的函数变成内联函数
    JVM内建有许多运行时优化。首先短方法更利于JVM推断。流程更明显,作用域更短,副作用也更明显。如果是长方法JVM可能直接就跪了。
    第二个原因则更重要:方法内联
    如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身,如:
    1. 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
    2. 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07
    3. 0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
    4. 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
    5. 0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
    6. 0000120 61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65 01
    7. 0000140 00 04 74 68 69 73 01 00 1d 4c 63 6e 2f 69 74 63
    8. 0000160 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c 6f
    9. 0000200 57 6f 72 6c 64 3b 01 00 04 6d 61 69 6e 01 00 16
    10. 0000220 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72
    11. 0000240 69 6e 67 3b 29 56 01 00 04 61 72 67 73 01 00 13
    12. 0000260 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69
    13. 0000300 6e 67 3b 01 00 10 4d 65 74 68 6f 64 50 61 72 61
    14. 0000320 6d 65 74 65 72 73 01 00 0a 53 6f 75 72 63 65 46
    15. 0000340 69 6c 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64
    16. 0000360 2e 6a 61 76 61 0c 00 07 00 08 07 00 1d 0c 00 1e
    17. 0000400 00 1f 01 00 0b 68 65 6c 6c 6f 20 77 6f 72 6c 64
    18. 0000420 07 00 20 0c 00 21 00 22 01 00 1b 63 6e 2f 69 74
    19. 0000440 63 61 73 74 2f 6a 76 6d 2f 74 35 2f 48 65 6c 6c
    20. 0000460 6f 57 6f 72 6c 64 01 00 10 6a 61 76 61 2f 6c 61
    21. 0000500 6e 67 2f 4f 62 6a 65 63 74 01 00 10 6a 61 76 61
    22. 0000520 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 01 00 03 6f
    23. 0000540 75 74 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72
    24. 0000560 69 6e 74 53 74 72 65 61 6d 3b 01 00 13 6a 61 76
    25. 0000600 61 2f 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d
    26. 0000620 01 00 07 70 72 69 6e 74 6c 6e 01 00 15 28 4c 6a
    27. 0000640 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b
    28. 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
    29. 0000700 00 07 00 08 00 01 00 09 00 00 00 2f 00 01 00 01
    30. 0000720 00 00 00 05 2a b7 00 01 b1 00 00 00 02 00 0a 00
    31. 0000740 00 00 06 00 01 00 00 00 04 00 0b 00 00 00 0c 00
    32. 0000760 01 00 00 00 05 00 0c 00 0d 00 00 00 09 00 0e 00
    33. 0001000 0f 00 02 00 09 00 00 00 37 00 02 00 01 00 00 00
    34. 0001020 09 b2 00 02 12 03 b6 00 04 b1 00 00 00 02 00 0a
    35. 0001040 00 00 00 0a 00 02 00 00 00 06 00 08 00 07 00 0b
    36. 0001060 00 00 00 0c 00 01 00 00 00 09 00 10 00 11 00 00
    37. 0001100 00 12 00 00 00 05 01 00 10 00 00 00 01 00 13 00
    38. 0001120 00 00 02 00 14Copy
    复制代码
    方法调用被替换后
    1. u4              magic
    2. u2             minor_version;   
    3. u2             major_version;   
    4. u2             constant_pool_count;   
    5. cp_info        constant_pool[constant_pool_count-1];   
    6. u2             access_flags;   
    7. u2             this_class;   
    8. u2             super_class;   
    9. u2             interfaces_count;   
    10. u2             interfaces[interfaces_count];   
    11. u2             fields_count;   
    12. field_info     fields[fields_count];   
    13. u2             methods_count;   
    14. method_info    methods[methods_count];   
    15. u2             attributes_count;   
    16. attribute_info attributes[attributes_count];Copy
    复制代码
    反射优化

    [code]public class Reflect1 {   public static void foo() {      System.out.println("foo...");   }   public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {      Method foo = Demo3.class.getMethod("foo");      for(int i = 0; i




    欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4