JVM学习笔记(上)

简介: JVM学习笔记(上)

1、总体路线

2、程序计数器

Program Counter Register 程序计数器(寄存器)

作用:是记录下一条 jvm 指令的执行地址行号。

特点:

  • 是线程私有的
  • 不会存在内存溢出

解释器会解释指令为机器码交给 cpu 执行,程序计数器会记录下一条指令的地址行号,这样下一次解释器会从程序计数器拿到指令然后进行解释执行。
多线程的环境下,如果两个线程发生了上下文切换,那么程序计数器会记录线程下一行指令的地址行号,以便于接着往下执行。

3、栈

定义

每个线程运行需要的内存空间,称为虚拟机栈

每个栈由多个栈帧(Frame)组成,对应着每次调用方法时所占用的内存

每个线程只能有一个活动栈帧,对应着当前正在执行的方法

问题辨析:

  • 垃圾回收是否涉及栈内存?

不会。栈内存是方法调用产生的,方法调用结束后会弹出栈。

  • 栈内存分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

  • 方法内的局部变量是否线程安全

如果方法内部的变量没有逃离方法的作用范围,它是线程安全的

如果是局部变量引用了对象,并逃离了方法的作用范围,那就要考虑线程安全问题。

m1是线程安全的,m2,m3都不算线程安全的。

栈内存溢出

栈帧过大、过多、或者第三方类库操作,都有可能造成栈内存溢出 java.lang.stackOverflowError ,使用 -Xss256k 指定栈内存大小!

public class Demo1_19 {
    public static void main(String[] args) throws JsonProcessingException {
        Dept d = new Dept();
        d.setName("Market");
        Emp e1 = new Emp();
        e1.setName("zhang");
        e1.setDept(d);
        Emp e2 = new Emp();
        e2.setName("li");
        e2.setDept(d);
        d.setEmps(Arrays.asList(e1, e2));
        // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
        ObjectMapper mapper = new ObjectMapper();
        System.out.println(mapper.writeValueAsString(d));
    }
}
class Emp {
    private String name;
    @JsonIgnore
    private Dept dept;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Dept getDept() {
        return dept;
    }
    public void setDept(Dept dept) {
        this.dept = dept;
    }
}
class Dept {
    private String name;
    private List<Emp> emps;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<Emp> getEmps() {
        return emps;
    }
    public void setEmps(List<Emp> emps) {
        this.emps = emps;
    }
}

线程运行诊断

案例一:cpu 占用过多

解决方法:Linux 环境下运行某些程序的时候,可能导致 CPU 的占用过高,这时需要定位占用 CPU 过高的线程

  • top 命令,查看是哪个进程占用 CPU 过高
  • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过 top 查到的进程号 通过 ps 命令进一步查看是哪个线程占用 CPU 过高
  • jstack 进程 id 通过查看进程中的线程的 nid ,刚才通过 ps 命令看到的 tid 来对比定位,注意 jstack 查找出的线程 id 是 16 进制的,需要转换。

本地方法栈

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

4、堆

定义

Heap 堆

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

特点

  • 它是线程共享,堆内存中的对象都需要考虑线程安全问题
  • 有垃圾回收机制

堆内存溢出

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

可以使用 -Xmx8m 来指定堆内存大小。

堆内存诊断

jps 工具

查看当前系统中有哪些 java 进程

jmap 工具

查看堆内存占用情况 jmap - heap 进程id

jconsole 工具

图形界面的,多功能的监测工具,可以连续监测

5、方法区

定义

Java 虚拟机有一个在所有 Java 虚拟机线程之间共享的方法区域。方法区域类似于用于传统语言的编译代码的存储区域,或者类似于操作系统进程中的“文本”段。它存储每个类的结构,例如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括特殊方法,用于类和实例初始化以及接口初始化方法区域是在虚拟机启动时创建的。

组成

方法区内存溢出

1.8 之前会导致永久代内存溢出

使用 -XX:MaxPermSize=8m 指定永久代内存大小

1.8 之后会导致元空间内存溢出

使用 -XX:MaxMetaspaceSize=8m 指定元空间大小

运行时常量池

二进制字节码包含(类的基本信息,常量池,类方法定义,包含了虚拟机的指令)

首先看看常量池是什么,编译如下代码:

public class Test {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

然后使用 javap -v Test.class 命令反编译查看结果。

常量池:

就是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息

运行时常量池:

常量池是 *.class 文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

6、StringTable

  • 常量池中的字符串仅是符号,只有在被用到时才会转化为对象
  • 利用串池的机制,来避免重复创建字符串对象
  • 字符串变量拼接的原理是StringBuilder
  • 字符串常量拼接的原理是编译器优化
  • 可以使用intern方法,主动将串池中还没有的字符串对象放入串池中

例1:

// StringTable [ "a", "b" ,"ab" ]  hashtable 结构,不能扩容
public class Demo1_22 {
    // 常量池中的信息,都会被加载到运行时常量池中, 这时 a b ab 都是常量池中的符号,还没有变为 java 字符串对象
    // ldc #2 会把 a 符号变为 "a" 字符串对象
    // ldc #3 会把 b 符号变为 "b" 字符串对象
    // ldc #4 会把 ab 符号变为 "ab" 字符串对象
    public static void main(String[] args) {
        String s1 = "a"; // 懒惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在编译期间的优化,结果已经在编译期确定为ab
        System.out.println(s3 == s5);//true
    System.out.println(s3 == s4);//flase
    }
}

intern方法 1.8

调用字符串对象的 intern 方法,会将该字符串对象尝试放入到串池中

  • 如果串池中没有该字符串对象,则放入成功
    如果有该字符串对象,则放入失败
  • 无论放入是否成功,都会返回串池中的字符串对象

注意:此时如果调用 intern 方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

public class Demo1_23 {
    //  ["ab", "a", "b"]
    public static void main(String[] args) {
        String x = "ab";
        String s = new String("a") + new String("b");
        // 堆  new String("a")   new String("b") new String("ab")
        String s2 = s.intern(); // 将这个字符串对象尝试放入串池,如果有则并不会放入,如果没有则放入串池, 会把串池中的对象返回
        System.out.println( s2 == x);//true
        System.out.println( s == x );//false
    }
}

面试题

/**
 * 演示字符串相关面试题
 */
public class Demo1_21 {
    public static void main(String[] args) {
        String s1 = "a";
        String s2 = "b";
        String s3 = "a" + "b"; // ab
        String s4 = s1 + s2;   // new String("ab")
        String s5 = "ab";
        String s6 = s4.intern();
// 问
        System.out.println(s3 == s4); // false
        System.out.println(s3 == s5); // true
        System.out.println(s3 == s6); // true
        String x2 = new String("c") + new String("d"); // new String("cd")
//        x2.intern();
        String x1 = "cd";
        x2.intern();
// 问,如果调换了【最后两行代码】的位置呢,如果是jdk1.6呢
        System.out.println(x1 == x2);//false
    }
}

理解intern方法:intern是将字符串放到常量池里,常量池如果没有,就把自己的地址放到常量池,如果有,就返回常量池里面的对象地址。

StringTable位置

jdk1.6 StringTable 位置是在永久代中,1.8 StringTable 位置是在堆中。

StringTable 垃圾回收

-Xmx10m 指定堆内存大小

-XX:+PrintStringTableStatistics 打印字符串常量池信息

-XX:+PrintGCDetails

-verbose:gc 打印 gc 的次数,耗费时间等信息

/**
 * 演示 StringTable 垃圾回收
 * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
 */
public class Code_05_StringTableTest {
    public static void main(String[] args) {
        int i = 0;
        try {
            for(int j = 0; j < 10000; j++) { // j = 100, j = 10000
                String.valueOf(j).intern();
                i++;
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            System.out.println(i);
        }
    }
}

StringTable调优

在介绍性能调优之前不得不说一说StringTable的底层实现,前面已经提到了StringTable底层是一个HashTable,HashTable长什么样呢?其实就是数组+链表,每个元素是一个key-value。当存入一个元素的时候,就会将其key通过hash函数计算得出数组的下标并存放在对应的位置。

比如现在有一个key-value,这个key通过hash函数计算结果为2,那么就把value存放在数组下标为2的位置。但是如果现在又有一个key通过hash函数计算出了相同的结果,比如也是2,但2的位置已经有值了,这种现象就叫做哈希冲突,怎么解决呢?这里采用了链表法:

链表法就是将下标一样的元素通过链表的形式串起来,如果数组容量很小但是元素很多,那么发生哈希冲突的概率就会提高。大家都知道,链表的效率远没有数组那么高,哈希冲突过多会影响性能。所以为了减少哈希冲突的概率,所以可以适当的增加数组的大小。数组的每一格在StringTable中叫做bucket,我们可以增加bucket的数量来提高性能,默认的数量为60013个,来看一个对比:

long startTime = System.nanoTime();
String str = "hello";
for(int i = 0;i < 500000;i++) {
    String s = str + i;
    s.intern();
}
long endTime = System.nanoTime();
System.out.println("花费的时间为:"+(endTime-startTime)/1000000 + "毫秒");

先通过一个虚拟机参数将bucket指定的小一点,来个2000吧:

-XX:StringTableSize=2000

运行一下:

一共花费了1.2秒。再来将bucket的数量增加一点,来个20000个:

-XX:StringTableSize=20000

运行一下:

可以看到,这次只花了0.19秒,性能有了明显的提升,说明这样确实可以优化StringTable。

7、直接内存

定义

Direct Memory

  • 常见于 NIO 操作时,用于数据缓冲区
  • 分配回收成本较高,但读写性能高
  • 不受 JVM 内存回收管理

使用直接内存的好处

文件读写流程:

因为 java 不能直接操作文件管理,需要切换到内核态,使用本地方法进行操作,然后读取磁盘文件,会在系统内存中创建一个缓冲区,将数据读到系统缓冲区, 然后在将系统缓冲区数据,复制到 java 堆内存中。缺点是数据存储了两份,在系统内存中有一份,java 堆中有一份,造成了不必要的复制。

使用了 DirectBuffer 文件读取流程

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

使用ByteBuffer.allocateDirect

/**
 *  IO:阻塞式                  NIO (New IO / Non-Blocking IO):非阻塞式
 *  byte[] / char[]     Buffer
 *  Stream              Channel
 *
 * 查看直接内存的占用与释放
 */
public class BufferTest {
    private static final int BUFFER = 1024 * 1024 * 1024; //1GB
    public static void main(String[] args){
        //直接分配本地内存空间
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
        System.out.println("直接内存分配完毕,请求指示!");
        Scanner scanner = new Scanner(System.in);
        scanner.next();
        System.out.println("直接内存开始释放!");
        byteBuffer = null;
        System.gc();
        scanner.next();
    }
}

使用unsafe类

/**
 * 直接内存分配的底层原理:Unsafe
 */
public class Demo1_27 {
    static int _1Gb = 1024 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        Unsafe unsafe = getUnsafe();
        // 分配内存
        long base = unsafe.allocateMemory(_1Gb);
        unsafe.setMemory(base, _1Gb, (byte) 0);
        System.in.read();
        // 释放内存
        unsafe.freeMemory(base);
        System.in.read();
    }
    public static Unsafe getUnsafe() {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            return unsafe;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

8、垃圾回收

如何判断对象可以回收

引用计数法

当一个对象被引用时,引用对象的值加一,当一个对象不被引用时,引用对象的值减一,当值为 0 时,就表示该对象不被引用,可以被垃圾收集器回收。

这个引用计数法听起来不错,但是有一个弊端,如下图所示,循环引用时,两个对象的计数都为1,导致两个对象都无法被释放,造成内存泄露。

可达性分析算法

  • JVM 中的垃圾回收器通过可达性分析来探索所有存活的对象
  • 扫描堆中的对象,看能否沿着 GC Root 对象为起点的引用链找到该对象,如果找不到,则表示可以回收
  • 可以作为 GC Root 的对象
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中 JNI(即一般说的Native方法)引用的对象
  • 已启动且未停止的 Java 线程
public static void main(String[] args) throws IOException {
        ArrayList<Object> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add(1);
        System.out.println(1);
        System.in.read();
        list = null;
        System.out.println(2);
        System.in.read();
        System.out.println("end");
    }

对于以上代码,可以使用如下命令将堆内存信息转储成一个文件,然后使用

Eclipse Memory Analyzer 工具进行分析。

第一步:

使用 jps 命令,查看程序的进程

第二步

使用 jmap -dump:format=b,live,file=1.bin 16104 命令转储文件

dump:转储文件

format=b:二进制文件

file:文件名

16104:进程的id

第三步:打开 Eclipse Memory Analyzer 对 1.bin 文件进行分析。

分析的 gc root,找到了 ArrayList 对象,然后将 list 置为null,再次转储,那么 list 对象就会被回收。

java定义常量是在方法区还是堆区

在Java中,常量通常被定义为静态final字段,它们被存储在方法区中的运行时常量池中。这意味着它们在程序运行期间只被分配一次,并且可以被所有对象共享。堆区是用于存储对象实例的区域,而不是常量。

四种引用

强引用

只有所有 GC Roots 对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

软引用(SoftReference)

仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象可以配合引用队列来释放软引用自身

弱引用(WeakReference)

仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象

可以配合引用队列来释放弱引用自身

虚引用(PhantomReference)

必须配合引用队列使用,主要配合 ByteBuffer 使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存

终结器引用(FinalReference)

无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有被回收),再由 Finalizer 线程通过终结器引用找到被引用对象并调用它的 finalize 方法,第二次 GC 时才能回收被引用对象。

演示软引用

/**
 * 演示 软引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Code_08_SoftReferenceTest {
    public static int _4MB = 4 * 1024 * 1024;
    public static void main(String[] args) throws IOException {
        method2();
    }
    // 设置 -Xmx20m , 演示堆内存不足,
    public static void method1() throws IOException {
        ArrayList<byte[]> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            list.add(new byte[_4MB]);
        }
        System.in.read();
    }
    // 演示 软引用
    public static void method2() throws IOException {
        ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
        for(int i = 0; i < 5; i++) {
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());
        for(SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }
}

method1 方法解析:

首先会设置一个堆内存的大小为 20m,然后运行 mehtod1 方法,会抛异常,堆内存不足,因为 mehtod1 中的 list 都是强引用。

method2 方法解析:

在 list 集合中存放了 软引用对象,当内存不足时,会触发 full gc,将软引用的对象回收。细节如图:

上面的代码中,当软引用引用的对象被回收了,但是软引用还存在,所以,一般软引用需要搭配一个引用队列一起使用。

修改 method2 如下:

// 演示 软引用 搭配引用队列

public static void method3() throws IOException {
        ArrayList<SoftReference<byte[]>> list = new ArrayList<>();
        // 引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for(int i = 0; i < 5; i++) {
            // 关联了引用队列,当软引用所关联的 byte[] 被回收时,软引用自己会加入到 queue 中去
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        // 从队列中获取无用的 软引用对象,并移除
        Reference<? extends byte[]> poll = queue.poll();
        while(poll != null) {
            list.remove(poll);
            poll = queue.poll();
        }
        System.out.println("=====================");
        for(SoftReference<byte[]> ref : list) {
            System.out.println(ref.get());
        }
    }

弱引用

/**
 * 演示弱引用
 * -Xmx20m -XX:+PrintGCDetails -verbose:gc
 */
public class Demo2_5 {
    private static final int _4MB = 4 * 1024 * 1024;
    public static void main(String[] args) {
        //  list --> WeakReference --> byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            list.add(ref);
            for (WeakReference<byte[]> w : list) {
                System.out.print(w.get()+" ");
            }
            System.out.println();
        }
        System.out.println("循环结束:" + list.size());
    }
}
相关文章
|
12月前
|
存储 前端开发 安全
JVM学习笔记(完结)
JVM学习笔记(完结)
JVM学习笔记(一)------基本结构
JVM学习笔记(一)------基本结构
|
开发框架 前端开发 Java
JVM学习笔记(二)------Java代码编译和执行的整个过程
JVM学习笔记(二)------Java代码编译和执行的整个过程
|
存储 Java
JVM学习笔记(一)—基本结构
JVM学习笔记(一)—基本结构
|
6月前
|
安全 Java
《深入理解java虚拟机》学习笔记-----郑雨迪
《深入理解java虚拟机》学习笔记-----郑雨迪
71 0
|
12月前
|
缓存 监控 算法
JVM学习笔记(中)
JVM学习笔记(中)
|
Java
JVM学习笔记-如何在IDEA打印JVM的GC日志信息
若要在Idea上打印JVM相应GC日志,其实只需在Run/Debug Configurations上进行设置即可。
113 0
|
Arthas Java 测试技术
JVM学习笔记(5)——JVM线上问题排查
JVM学习笔记(5)——JVM线上问题排查
112 0
|
Java
JVM学习笔记(4)——JVM调优
JVM学习笔记(4)——JVM调优
94 0
|
算法 Java
JVM学习笔记(3)——垃圾回收器
JVM学习笔记(3)——垃圾回收器
84 0