突然 系统卡顿CPU飙高? 莫名 OOM?强软弱虚引用咋用?垃圾回收参数咋设置?选哪个垃圾收集器? 宕机?如何写出优质代码?想要系统性了解JVM内存调优?JVM 就决定用你了

           

一、前言

1.1JVM是什么 

1.2JVM与JDK JRE的关系

1.3为啥要学JVM

 

1.4常见的JVM

1.5学习路线

二、内存结构

2.1程序计数器

记住 它是线程私有 是用来记录 某线程下一次执行字节码指令的地址

 

2.2栈

2.2.1 介绍

 记住:栈是先进后出的 如下图二 里面存放的是一个个栈帧,活动栈帧 只有一个 栈帧里面存放的是方法参数 局部变量 返回地址啥的 

2.2.2 代码演示

如下 图一图二三是 分别运行到 method1 ,method3 以及结束执行 method2,3 返回到method1时的栈内信息 可以看到图二 图中下方中间 运行到method3时的参数及局部变量信息

 

 

2.2.3 三个面试问题

1:不涉及,栈内存是方法执行完就回收了

2.不是 反而大了不好,栈内存设置大了意味着每调用一个方法就要使用这样大小的一块内存空间

如果原本支持200个线程并发,当你设置大了后就会浪费内存空间的同时并且线程并发数会减少

3. 是线程安全的   只有被线程共享的变量才有线程安全问题 

2.2.4下面三个方法哪些可能线程不安全

m1线程安全的因为里面局部变量始终在该方法内,

m2,m3都不是线程安全的,作为参数传进来或者作为返回值返回的变量都可能被其它线程共享 

2.2.5 线程安全问题小结

2.2.6栈内存溢出(stack over flow) 

调用方法太多时,正常不会 一般是因为你写递归,结束条件没写好,或者 你写的代码比如 

部门类里有员工,员工类里又有部门,使用时就 套娃似的  太多层了 

 

 就会报这错

2.2.7线程诊断与定位

比如某死循环代码或者如下这典型死锁代码  要怎样定位呢?

 jps

 然后jstack 进程id

信息如下图 如果死锁的话最小面就会有死锁线程的信息

 

 如果 进程内线程太多了 就可以使用如下指令 先大致看到是哪个线程的问题

 然后再到 jstack 信息中 根据 线程编号去找(记得转换为16进制),这样就方便多了

2.3本地方法栈

有些与操作系统打交道的方法 由 C或者C++实现 

比如 Object里的clone方法,本地方法栈的存在就是给他们提供运行的内存空间

2.4 堆

2.4.1 堆内存诊断 jmap -heap 线程id

使用如下代码示例看看 当一段代码运行时 堆内存的分布及使用吧 

package com.robert.concurrent.test;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "c.TestConcurrentHashmap")
public class TestConcurrentHashmap {


    public static void main(String[] args) {

        log.debug("初始状态...");
        TimeSleepUtil.sleep(30);

        byte[] data = new byte[1024*1024*20];
        log.debug("给对象分配内存空间后状态...");
        TimeSleepUtil.sleep(20);

        System.gc();
        log.debug("回收后状态...");
        TimeSleepUtil.sleep(30);

    }

}

 再每次log的时候 在 idea 下面的windows终端 内输入 jps查询当前运行进程 再使用

jmap -heap 线程id 去查看堆信息  

 下面三图分别为 初始化状态 给对象分配内存空间后状态 回收后状态 下的堆的配置信息及内存的信息我们主要看内存使用信息   可以看到图一比图二 新生代多出了20M占用内存  最后手动GC后

更是比初始时占用内存都要少很多,现在看不懂没关系,接着看

 

 

2.4.3 堆内存诊断 -jconsole

以上的代码运行时 终端 jconsole连接后 能看到对应的内存的实时变化

2.4.4 堆内存诊断 -jvisualvm

jvisualvm 这个工具 也是jdk自带的, 这个又比jconsole好点,比如如下 我可以看到 占用内存从大到下的前二十个对象的信息

 

 

堆转储命令 jmap -Dump命令使用转储堆内存快照到指定文件

2.5方法区

2.5.1介绍 

从官网的介绍可以看到 方法区 跟 堆一样 也是 线程的共享区域 它存储着 每个类的结构信息

比如 运行时常量池,属性,方法还有方法和构造器的代码 

2.5.2方法区的实现

 方法区只是个规范 jdk1.8之前它的实现是叫永久代 之后是原空间

大致结构如下

2.5.3方法区的内存溢出 

前面不是说方法区是用来存放类信息的嘛,下面这块代码 就是加载类的信息 1.8后 方法区是存在系统的内存中的 我的电脑内存是16G 需要加载类次数太多

所以调整 方法区 内存大小 -XX:MaxMetaspaceSize=8m  修改为8m

运行时发现果然报错

 如果是Jdk1.8之前报的错就是下面这样 永久代内存溢出 

 一般是使用第三方框架或者写底层代码时 才会出现的错误 比如 spring mybatis会动态加载些类

正常 自己不会写这种加载很多次类的代码 

2.5.4 方法区记录的信息查看

使用如下 jdk提供的反编译的工具 javap  反编译 class文件

C:\study\workspace\target\classes\com\robert\concurrent\test>javap -v TestConcurrentHashmap.class

 可以看到 反编译后的字节码信息 类文件信息如下  这是被工具 翻译成了人能看懂的

第一部分:文件相关信息 比如 修改日期 文件大小,文件路径,权限,版本啥的

Classfile /C:/study/workspace/target/classes/com/robert/concurrent/test/TestConcurrentHashmap.class
  Last modified 2021-10-15; size 714 bytes
  MD5 checksum 189bce0860301ad4051a45b5b748fee3
  Compiled from "TestConcurrentHashmap.java"
public class com.robert.concurrent.test.TestConcurrentHashmap
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER

第二部分: 常量池  就相当于 类中常量的一张表通过编号就能找到   现在是一些 编号 在运行中时

会被替换为相应内存地址 

Constant pool:
   #1 = Methodref          #6.#24         // java/lang/Object."<init>":()V
   #2 = String             #25            // c.TestConcurrentHashmap
   #3 = Methodref          #26.#27        // org/slf4j/LoggerFactory.getLogger:(Ljava/lang/String;)Lorg/slf4j/Logger;
   #4 = Fieldref           #5.#28         // com/robert/concurrent/test/TestConcurrentHashmap.log:Lorg/slf4j/Logger;
   #5 = Class              #29            // com/robert/concurrent/test/TestConcurrentHashmap
   #6 = Class              #30            // java/lang/Object
   #7 = Utf8               log
   #8 = Utf8               Lorg/slf4j/Logger;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/robert/concurrent/test/TestConcurrentHashmap;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               args
  #19 = Utf8               [Ljava/lang/String;
  #20 = Utf8               MethodParameters
  #21 = Utf8               <clinit>
  #22 = Utf8               SourceFile
  #23 = Utf8               TestConcurrentHashmap.java
  #24 = NameAndType        #9:#10         // "<init>":()V
  #25 = Utf8               c.TestConcurrentHashmap
  #26 = Class              #31            // org/slf4j/LoggerFactory
  #27 = NameAndType        #32:#33        // getLogger:(Ljava/lang/String;)Lorg/slf4j/Logger;
  #28 = NameAndType        #7:#8          // log:Lorg/slf4j/Logger;
  #29 = Utf8               com/robert/concurrent/test/TestConcurrentHashmap
  #30 = Utf8               java/lang/Object
  #31 = Utf8               org/slf4j/LoggerFactory
  #32 = Utf8               getLogger
  #33 = Utf8               (Ljava/lang/String;)Lorg/slf4j/Logger;

 第三部分:成员属性,方法还有方法和构造器的代码 

下面这些代码里的 #1 #3 这种都会在运行时去常量池中获取对应常量然后执行

{
  public com.robert.concurrent.test.TestConcurrentHashmap();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/robert/concurrent/test/TestConcurrentHashmap;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  args   [Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      args

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc           #2                  // String c.TestConcurrentHashmap
         2: invokestatic  #3                  // Method org/slf4j/LoggerFactory.getLogger:(Ljava/lang/String;)Lorg/slf4j/Logger;
         5: putstatic     #4                  // Field log:Lorg/slf4j/Logger;
         8: return
      LineNumberTable:
        line 5: 0
}

2.6 StringTable

2.6.1 StringTable 的几种特性

先看看,后面反编译用字节码能看到

2.6.2 字符串相关面试题

先看看,后面说答案

2.6.3字符串使用时才被创建为对象 

从下面两图 与 idea中memory(右下角)可以查看某类字符串对象个数,第一遍打印 时字符串对象增加,想想也正常 用到才放到常量池很合理不然浪费内存不是?

因为串池StringTable没有所以新增然后放进去,第二遍打印时 因为串池中有了所以直接取串池中的 然后如图三有局部变量引用就放到图四局部变量表

2.6.4 字符串引用 相加 其实是StringBuilder相拼接?

没错,从字节码可以看出 s1+s2 经过编译器优化后 就等于下图一 右侧注释代码 因为他们是可变的

 2.6.5 字符串常量 相加 其实就是一个字符串?

是的,编译器优化了 因为都是常量不可变,编译期就可以直接把代码改了然后优化 不过这种代码正常没人写

2.6.6 intern方法 是个啥?

调用intern方法  就是 去到串池中找如果存在 那么 就返回 串池中字符串 不存在就放进去后再返回

串池中的字符串  所以不管存不存在调用intern的返回值都是串池中的字符串常量,但是 不存在时

会把调用者的也改成 串池 中的字符串 所以    s.intern   正常s是放在堆中的new出来的对象,如果

调用intern方法时 串池StringTable中没有该串 那么 s = “ab”  而不再是原本new出来的放在堆中的String对象了

2.6.7StringTable存放的位置

 串池1.8是放在堆中的,minor GC时就可以回收无用的字符串   之前版本好像不是 这里不管了

2.6.8String相关面试题答案公布

答案全在注释中,上面看懂了的话应该可以分析出来了,我这再稍微分析下

2.6.9 StringTable垃圾回收

StringTable中的字符串也是会垃圾回收的,下三图 图一蓝色部分是程序初始状态时  串池的字符串的数量 图二图三 分别是 10个及1000个字符串往串池中放后的 串池中字符串的数量,当 存放的数量 大于串池中新增的字符串数量时 说明 你存放的字符串对象中 有值是重复的 或者 超出了新生代的垃圾回收的阈值 产生了垃圾回收 

 

 

2.6.10 StringTable调优

前面我们直到 StringTable 串池 的作用是存放 着一堆的字符串 他们是不重复的,所以我们如果一个项目中有大量字符串,然后内存不是很多的话,可以在使用时将这些字符串使用intern方法入池,也就达到了节省内存的效果,因为重复的字符串被剔除了 然后串池内的存储方式比正常的String对象内存小很多 没有啥对象头markword那些信息    同时 因为 StringTable存储的数据结构是类似HashTable的形式

有个bukect数组 然后 他下面就挂着存放字符串的链表   链表中的字符串都是有hash冲突(就是他们的hash值对数组长度取模)的 值就是  某bukect下标 所以 当项目中发生频繁入池的操作时 为了提升效率 可以添加如下VM参数 -XX: StringTableSize=20000 这种来减少hash碰撞的次数从而,提升入池效率

三、直接内存

3.1定义

简单说一下,如图二 原本我们读取 某磁盘文件中的内容时 需要先读到系统内存 然后从系统内存拷贝一份到java内存 然后代码内才能获取到    这玩意相当于 系统内存与java内存共享的一块内存,我们可以直接把磁盘中的内容读到直接内存 然后java代码中就可以读取了   所以大大提升了效率

 

3.2使用方式

如下图 我们可以使用 ByteBuffer.allocateDirect(_1GB) 这种形式来分配一块直接内存,然后用就完事了

package com.robert.concurrent.test;


import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.ByteBuffer;

@Slf4j(topic = "c.TestStringTable")
public class TestStringTable {

    private static final int _1GB = 1024 * 1024 * 1024;

    public static void main(String[] args) throws IOException {

        // 使用java内存存储
        byte[] content = new byte[_1GB];
        System.in.read(content);

        // 使用直接内存存储
        ByteBuffer direct = ByteBuffer.allocateDirect(_1GB);

    }
}

3.3 java中直接内存分配与回收原理

这里只简单说下,因为涉及到一个unsafe对象,与虚引用的概念 

unsafe这个对象是针对系统层面的一些操作暴露出来的一个算是接口类的玩意  前面多线程中 CAS就是调用它的一些方法,这里也是用它来分配直接内存

 直接使用unsafe对象分配直接内存的示例代码如下

 直接内存释放的原理就是创建了一个虚引用this对象 绑定的对象是 一个 实现了Runnable接口的Deallocator对象 当 虚引用对象释放时 调用 Deallocator对象的run方法然后执行 直接内存的释放

 释放直接内存的代码如下

3.4禁用显示回收对直接内存的影响

命令如下 在vm参数中新增这个后 代码中写的System.gc() 就无效了 为了防止新手乱写代码,因为System.gc()是fullGC  而系统的GC是 先youngGC再 full 

x

但是这样的话我们就无法通过回收直接内存的引用对象来回收直接内存了,那要怎样回收直接内存呢?

如图 反射获取到 unsafe对象然后调用其 freeMemory方法来释放直接内存 

四、垃圾回收

4.1 判断垃圾

4.1.1 如下为引用计数法

如果使用此方法 当 出现循环引用时将无法回收

4.1.2 可达性分析算法

如下 使用一些节点作为根节点 不是这些根节点的字节点的对象被标注为垃圾 可以进行垃圾回收,JVM使用的就是这种

4.1.3 哪些对象会被作为根节点

有如下有四大类 系统类对象, 本地方法栈中对象, 线程中的(比如 图四中倒数那两个就是一个作为main方法参数的string数组,另一个是main方法内用到的arraylist对象),锁对象

 

五、四种引用

5.1 简介

先看下概览,下面会详细说明 前面三种 

5.2强引用

我们一般正常创建出来的对象就是强引用关联的,这里不多说

5.3软引用

5.3.1 软引用使用案例

上面有说到,当第一次垃圾回收后 内存还是不足 此时就会回收软引用关联的对象,这样做的好处是什么呢? 当我们使用大内存对象的时候 如果程序运行的足够久 那么内存将被一直占用着 ,很有可能造成OOM 此时就可以使用软引用 去关联 该类大内存对象 当内存进行一轮垃圾回收(youngGC+fullGC)后还不足时 就直接回收掉它们

我先把vm参数中的堆内存参数置为了80M 这个看你机器具体情况,我是在保证能放下四个4M字节数组对象的情况下调整的当调到80M的时候新生代刚好够放一个4m对象而不发生垃圾回收

     -Xmx80m -XX:+PrintGCDetails -verbose:gc

package com.robert.concurrent.test;

import lombok.extern.slf4j.Slf4j;

import java.lang.ref.SoftReference;
import java.util.ArrayList;

@Slf4j(topic = "c.TestSoftReference")
public class TestSoftReference {

    private static final int _4MB = 1024 * 1024 * 4;

    public static void main(String[] args) {
        ArrayList<SoftReference<byte[]>> arrayList = new ArrayList();
        for (int i = 0; i < 5; i++) {

            System.out.println();
            SoftReference softReference = new SoftReference(new Byte[_4MB]);
            log.debug(softReference.get().toString());
            arrayList.add(softReference);
            log.debug("size:{}",arrayList.size());

            for (SoftReference<byte[]> softReference1 : arrayList) {
                System.out.println(softReference1.get());
            }
        }

        log.debug("软引用对象添加完后 list的size为:{}",arrayList.size());
        System.out.println();
        for (SoftReference<byte[]> softReference : arrayList) {
            System.out.println(softReference.get());
        }

    }

}

打印日志如下,我分析下

可以看到 当我存放前三个4M对象时没啥事,当存放第四个时发生了一次young GC 但是 没有回收这四个对象中的一个 说明回收了其它优先级更高的垃圾 比如 虚引用关联的

当存放到第五个4M字节数组对象时 这时候 young GC 与 full GC都进行了两轮 第一轮发现清除无用对象后 还是放不下第五个对象 于是第二次 fullGC 直接把 前面那四个软引用关联的对象给回收了

23:18:38.977 c.TestSoftReference [main] - [Ljava.lang.Byte;@7ff2a664
23:18:38.979 c.TestSoftReference [main] - size:1
[Ljava.lang.Byte;@7ff2a664

23:18:38.984 c.TestSoftReference [main] - [Ljava.lang.Byte;@2b4a2ec7
23:18:38.984 c.TestSoftReference [main] - size:2
[Ljava.lang.Byte;@7ff2a664
[Ljava.lang.Byte;@2b4a2ec7

23:18:38.988 c.TestSoftReference [main] - [Ljava.lang.Byte;@564718df
23:18:38.988 c.TestSoftReference [main] - size:3
[Ljava.lang.Byte;@7ff2a664
[Ljava.lang.Byte;@2b4a2ec7
[Ljava.lang.Byte;@564718df

[GC (Allocation Failure) [PSYoungGen: 17450K->2754K(24064K)] 66603K->51978K(78848K), 0.0017831 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
23:18:38.991 c.TestSoftReference [main] - [Ljava.lang.Byte;@51b7e5df
23:18:38.991 c.TestSoftReference [main] - size:4
[Ljava.lang.Byte;@7ff2a664
[Ljava.lang.Byte;@2b4a2ec7
[Ljava.lang.Byte;@564718df
[Ljava.lang.Byte;@51b7e5df

[GC (Allocation Failure) --[PSYoungGen: 19978K->19978K(24064K)] 69202K->69210K(78848K), 0.0046974 secs] [Times: user=0.14 sys=0.02, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 19978K->17899K(24064K)] [ParOldGen: 49232K->49220K(54784K)] 69210K->67120K(78848K), [Metaspace: 7160K->7160K(1056768K)], 0.0141763 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) --[PSYoungGen: 17899K->17899K(24064K)] 67120K->67120K(78848K), 0.0047847 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 17899K->0K(24064K)] [ParOldGen: 49220K->1471K(33280K)] 67120K->1471K(57344K), [Metaspace: 7160K->7160K(1056768K)], 0.0072875 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
23:18:39.023 c.TestSoftReference [main] - [Ljava.lang.Byte;@18a70f16
23:18:39.023 c.TestSoftReference [main] - size:5
null
null
null
null
[Ljava.lang.Byte;@18a70f16
23:18:39.023 c.TestSoftReference [main] - 软引用对象添加完后 list的size为:5

null
null
null
null
[Ljava.lang.Byte;@18a70f16
Heap
 PSYoungGen      total 24064K, used 17219K [0x00000000fe580000, 0x0000000100000000, 0x0000000100000000)
  eden space 20992K, 82% used [0x00000000fe580000,0x00000000ff650d00,0x00000000ffa00000)
  from space 3072K, 0% used [0x00000000ffa00000,0x00000000ffa00000,0x00000000ffd00000)
  to   space 3072K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x0000000100000000)
 ParOldGen       total 33280K, used 1471K [0x00000000fb000000, 0x00000000fd080000, 0x00000000fe580000)
  object space 33280K, 4% used [0x00000000fb000000,0x00000000fb16fe48,0x00000000fd080000)
 Metaspace       used 7160K, capacity 7364K, committed 7552K, reserved 1056768K
  class space    used 839K, capacity 884K, committed 896K, reserved 1048576K
Disconnected from the target VM, address: '127.0.0.1:13467', transport: 'socket'

Process finished with exit code 0

5.3.2引用队列的使用

如上例 当 引用对象关联的对象为null时 那这个引用对象如果没必要存在我们的list中时 我们可以

使用引用队列 在适当时机去将他们从list中移除从而将 关联对象为null的引用对象给回收

package com.robert.concurrent.test;

import lombok.extern.slf4j.Slf4j;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;

@Slf4j(topic = "c.TestSoftReference")
public class TestSoftReference {

    private static final int _4MB = 1024 * 1024 * 4;

    public static void main(String[] args) {
        ArrayList<SoftReference<byte[]>> arrayList = new ArrayList();
        ReferenceQueue referenceQueue = new ReferenceQueue();

        for (int i = 0; i < 5; i++) {

            System.out.println();
            SoftReference softReference = new SoftReference(new Byte[_4MB],referenceQueue);
            log.debug(softReference.get().toString());
            arrayList.add(softReference);
            log.debug("size:{}",arrayList.size());

            for (SoftReference<byte[]> softReference1 : arrayList) {
                System.out.println(softReference1.get());
            }
        }

        Reference reference = referenceQueue.poll();
        while(reference != null && reference.get() == null){
            arrayList.remove(reference);
            reference = referenceQueue.poll();
        }


        log.debug("软引用对象添加完后 list的size为:{}",arrayList.size());
        System.out.println();
        for (SoftReference<byte[]> softReference : arrayList) {
            System.out.println(softReference.get());
        }

    }

}

5.4 弱引用

软引用关联对象是在当内存进行一轮垃圾回收(youngGC+fullGC)后还不足时 第二轮就考虑回收掉它们

弱引用关联对象则是 在内存不足时第一轮垃圾回收 就可能回收掉

六、垃圾回收算法

6.1标记清除

根据可达性分析算法 将不被根节点引用的 对象做标记 然后标记了的内存区间的起始终止地址记录到一个空闲内存地址列表里 

优势:速度快

缺点:产生许多内存碎片没有充分利用内存 内存碎片可能有 5M 3M 2M 但是此时来个6M的对象就放不下 

6.2标记整理

如下,标记好垃圾后 把有用对象往前挪 

优点:没有内存碎片

缺点:因为整理要挪动对象存放的内存中的位置 比较耗时

6.3复制

如图 一块内存拆成两块 先标记 然后 把有用对象放到to区然后清空from区 然后 from与to交换位置

优点:没有内存碎片

缺点:占用双倍内存

七、分代回收

八、垃圾回收器

分为如下三类,根据业务场景选择

 

九、G1 

9.1简介

主要是看下面图中的适用场景

9.2G1回收的过程

9.2.1 Young collection(新生代回收)

如图 还是跟以前差不多,只是现在伊甸园区被分成了多块

9.2.2 Young collection+Concurrent Mark(新生代回收+并发标记)

并发标记是在老年代中对象内存达到阈值时发生

 

 9.2.3 Mixed Collection

9.3FullGC

前两种收集是在老年代内存不足时就进行full GC 后两者会先进行并发标记,看垃圾清理速度是否大于其它工作线程的垃圾产生速度

9.4跨代引用

老年代中被新生代对象所引用的话 会被做标记 ——》脏对象

9.5Remark

由于标记过程中引用可能会被修改,所以在当前标记对象引用被修改时加入一个标记队列并加上写屏障,然后后面重标记时再次取出来判断是否有引用

 

9.6字符串去重

这个跟前面说到的intern去重不同,那个是串池中字符串不重复,这个是

JDK8中的优化 如果 两个字符串对象的值相同,那么在新生代垃圾回收时 他们底层的值也就是字符数组最终会变成同一个

9.7类卸载

除了jdk底层外,有时候我们会手动加载一些类,那么这些类对象没被引用时给他卸载 这个jdk8中默认开启了

9.8巨型对象

如下解释,巨型对象是指大于一半region的对象,它不会在survivor区复制 它在ygc时如果没有引用那么优先被回收,毕竟占内存大,早消灭好

9.9JDK9并发标记起始条件调整

前面说过老年代对象内存占用堆内存45%时进行并发标记 JDK9中进行了调整,这个初始值可以设置,后面还会有算法采样数据后进行动态调整

9.10GC优化官方文档

 java12垃圾回收优化

十、 GC调优

10.1 简介

前言:常见的调优一般在图一这几个方面根据业务需求选择合适的回收器,,GC产生时就必然影响性能,从sql与代码层面 做到减少ygc 避免 ogc

 

 

10.2 垃圾回收参数查看

这玩意是个经验活 入门阶段 要了解前面的堆内存分布,垃圾回收器,回收算法 然后 掌握图中说的这些

 JVM的参数大全官网链接

使用如下命令查看GC的所有默认参数

"C:\work\jdk\jdk1.8.0_101\bin\java" -XX:+PrintFlagsFinal -version | findstr "GC"

在终端输入就会显示如下默认参数,图中蓝色选中就是 GC时间占比相关参数 99就是 (99-1)/100

垃圾回收单次时间只能占1%的时间

10.3 调优——新生代

 新生代因为 使用的标记复制算法 所以死亡对象回收代价为0毕竟不复制它们就行,并且消耗时间远低于FGC 

 ​​​​​​当然也不是说新生代内存越大越好 如下这曲线与介绍 太小肯定不行,太小的话因为新生代内存不足可能一些没达到生存年龄的对象都直接被放到老年代了,太大也不行,太大的话一次ygc消耗的时间也会相对长点 推荐是 25%~50%

10.4 调优——幸存区

 默认晋升老年代阈值是15 可以通过配置修改

10.5 调优——老年代

10.6 调优案例

如下案例图

案例一:是因为新生代内存小了,所以频繁MinorGC 然后 因为新生代内存小的原因,许多对象没有存活到年龄阈值15就被放到老年代去了 导致fullGC

案例二:是因为重新标记的影响 重新标记会扫描整个堆 ,性能影响较大,所以在重新标记前新生代再做次垃圾回收 如下图左上方参数配置

 案例三:这个是因为元空间小了 设置大点就好了

十一、类文件结构

11.1简介

包含了如下这些部分 

魔术,版本信息,常量池信息,本类与父类信息,接口信息,属性,方法,成员变量信息,后面会以一个最简单的类来展示它的类文件中的信息 看看就好 如想要深入了解访问如下官网链接

Java Language and Virtual Machine Specifications

11.2 类文件结构之——魔术

11.3 类文件结构之——版本

11.4 类文件结构之——常量池

如下可以看到 常量池中的东西包含了很多 什么方法名 类名 文件名 变量名 参数名 参数长度 等等

11.5 类文件结构之——access flags(访问标识与继承信息)

11.6 类文件结构之——field

11.7 类文件结构之——method-init

 构造方法

11.8 类文件结构之——method-main

 

11.9 a++ 与 ++a字节码分析

如下图一为源码,a++ 是把变量a的值入操作数栈 再在局部变量表中将a加一 而++a恰好相反

11.10条件判断指令

有英语基础的这里应该很容易看懂 if 如果   eq等于 ne不等于 cpm比较 lt 小于等于 都是英文单词首字母的缩写

11.11if的字节码指令是?

11.12while的字节码指令是?

 

11.13for的字节码指令是?

 

 

11.14字节码指令——cinit

 cinit 简单来说就是把 static的变量与代码块 放到一个方法中 这个方法 就是cinit 类加载时就被执行

11.15字节码指令——init

就是把 非静态成员 都放到一个init方法中执行 然后构造方法放最后

11.16字节码指令——方法调用

图一为代码,图二为反编译后的字节码指令 可以看到 可以明确调用对象的 都是 invokespecial

不能明确即有可能方法被子类重写的 则是 invokevirtual 而static方法统一都是 invokestatic

package com.robert.concurrent.test;

public class TestHelloWorld {

    public static void main(String[] args) {

        TestHelloWorld testHelloWorld = new TestHelloWorld();

        testHelloWorld.a2();
        testHelloWorld.a3();
        testHelloWorld.a4();

        TestHelloWorld.a1();
    }

    static void a1(){}

    private void a2(){}

    private final void a3(){}

    public void a4(){}

}

11.17字节码指令规律 小节

看完上面这么多字节码指令 想必你已经看出来一些规律

方法运行前 局部变量表就有了 比如有a,b,c三个局部变量那么槽位1,2,3就被填充了

运算之前必须 有load指令 进操作数栈  运算之前必须要load俩操作数进操作数栈 最后再进行iadd之类的指令进行运算   赋值操作 就是 istore  就是改变局部变量表的变量的值 ,i++ 是先load指令进操作数栈再改变局部变量表的值 ++i则相反 记住操作数栈的操作数一旦入栈就只能 被运算而不能改变,唯一有点陌生的指令就是 ldc   ld:load  c:constant   就是从常量池中加载常量 x 到操作

十二、多态的原理

 这里是通过类加载的链接阶段生成了一个虚方法表的玩意 然后具体运行的时候就到 虚方法表中找

调用对象中没找到该方法就找父类的

十二、try catch finally

12.1 简介

异常的捕捉是靠 一个exceptiontable的玩意 

finally的原理就是 把 finally中的指令往try后添加一份 所以一定会执行,另外finally中不能加return 虽然语法没错 但是可能导致 异常不会被捕捉

 

 

12.2 两道面试题 

以下返回 20 很明显 但是这类finally中写return 的做法可能使得异常不会被catch住

 

 

 下面打印 10 return 的变量会被放在一个槽位中 临时存储 除非finally中也有return 否则返回值不会被修改

十三、syncronized

 对代码块加syncronized锁字节码如下 可以看到是通过 monitorenter与 monitorexit 俩命令完成加锁与锁释放的 当异常时 也会调用 monitorexit释放锁

十三、语法糖

语法糖是啥呢 就是 一个优化 比如默认的无参构造 我们不写 编译器就会帮我们加上

13.1默认构造

13.2 自动拆装箱

13.3 泛型擦除

从下图可以看到 字节码 是 调用 add方法都是add的 Object类型  然后get时使用了一个checkcast进行强转 以此来做到泛型擦除

13.4 泛型反射

参数和返回值的具体类型 因为可能通过子类重写而改变 所以 反射时  也是先用父类获取 然后获取到后返回 rawtype

13.5 可变参数

13.6 foreach

 

13.7 switch string

13.8 switch enum

13.9 枚举

13.10 twr1

13.11 twr2

13.12 重写桥接

13.13 匿名内部类

十四、类加载

14.1加载

14.2连接

14.2.1验证

如下,我把字节码改了 执行就报错 魔术不对 

14.2.2准备

14.2.3解析

14.3初始化

 如下为验证代码

package com.robert.concurrent;

public class LoadClass {

    public static void main(String[] args) throws ClassNotFoundException {
//        //静态常量不会触发初始化
//        System.out.println(B.b);
        //类对象.class 不会触发初始化
//        System.out.println(B.class);
//        //创建该类的数组 不会触发初始化
//        System.out.println(new B[]{});
//        //不会初始化类B 但会加载B A
//        ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
//        contextClassLoader.loadClass("com.robert.concurrent.B");
//        //类对象.class 不会触发初始化
//        ClassLoader contextClassLoader2 = Thread.currentThread().getContextClassLoader();
//        Class.forName("com.robert.concurrent.B",false,contextClassLoader2);
//
//        //上面是不会触发类初始化的请况  下面会
//
//        //首次访问类的静态变量或者方法时
//        System.out.println(A.a);
//        //子类初始化如果父类还没初始化会引发
//        System.out.println(B.c);
//        //子类访问父类静态变量 只会触发父类初始化
//        System.out.println(B.a);
//        //会初始化类B并先初始化类A
//        Class.forName("com.robert.concurrent.B");

    }

}

class A {
    static int a = 0;

    static{
        System.out.println("a init");
    }

}

class B extends A{
    final static double b = 5.0;

    static boolean c = false;

    static{
        System.out.println("b init");
    }

}

14.4练习题

14.4.1 以下打印结果是?

14.4.2 了解了类加载?写个懒加载的单例吧 

十五、类加载器

 15.1 分类

15.2 启动类加载器

15.3 扩展类加载器

15.4 双亲委派的源码分析

跟代码可以发现 是 大致逻辑是 如果某个用户写的应用类从没被加载过那么

检查应用程序类加载器加载类缓存 是否存在 存在就返回 不存在的话看父类(扩展程序类加载器) 加载过的类缓存 中是否存在 存在就返回 不存在   继续找最上层父类(启动类加载器)加载过的类缓存是否存在 不存在 则 启动类加载器进行加载 没找到 则返回到子类 扩展程序类加载器进行加载

没找到 都是 抛ClassNotFoundExeption异常 被捕捉后 啥也不干 继续运行应用程序最终返回空 直到应用程序类加载器进行加载 找到了 就加载 加载是调的native方法 也就是操作系统实现的 然后返回咯

15.5 线程上下文类加载器

15.6 自定义类加载器 示例

15.6.1 创建 java类并生成 class文件

15.6.2 编写 自定义类加载器并测试

package com.robert.concurrent.test;

        import lombok.val;

        import java.io.ByteArrayOutputStream;
        import java.io.IOException;
        import java.nio.file.Files;
        import java.nio.file.Paths;

public class MyClassLoaderTest {


    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> person1 = myClassLoader.loadClass("Person");
        val person2 = myClassLoader.loadClass("Person");
        System.out.println(person1 == person2);

        MyClassLoader myClassLoader2 = new MyClassLoader();
        Class<?> person3 = myClassLoader2.loadClass("Person");
        System.out.println(person1 == person3);

        person3.newInstance();

    }


}

class MyClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String  classPath = "c:\\study\\myclasspathForTest\\"+name+".class";
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            Files.copy(Paths.get(classPath),out);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return defineClass(name, out.toByteArray(), 0, out.toByteArray().length);
    }
}

 打印结果如下 可以知道 加载器的不同 == 也会不同 

十六、运行期优化

16.1逃逸分析

就是热点代码会被缓存起来   hotpot虚拟机也是 热点的意思

 打印结果

16.2方法内联

解释如图一 简单逻辑的方法 内联就是不进入方法了 直接 与调用者代码合体 

 可以看到由于方法内联的开启 后面运行时间直接为0了

16.3字段优化

比如循环的时候 判断循环次数 小于 集合长度 集合长度不会一直获取 第一次获取后会被缓存起来

 

16.4反射优化

简单来说就是反射调用某方法 底层代码是如果超过一定次数 就变为 直接调用该方法 

 

 

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我才是真的封不觉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值