JVM 内存结构

一、程序计数器

1.1 定义

当前线程所执行的字节码的行号指示器,用于记住下一条 jvm的执行地址。

1.2 特点

1、线程私有

2、不存在内存溢出

二、虚拟机栈

2.1 定义

每个线程运行时所需要的内存,称为虚拟机栈。

2.2 特点

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

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

3、线程私有

2.3 问题辨析

2.3.1 垃圾回收是否涉及栈内存

不涉及,因为方法执行完毕后,内存就被回收了。

2.3.2 栈内存分配是否越大越好

不是,它和物理内存有关,假设当前物理内存为 500M ,栈的内存为 1M ,那么他就可以存放 500 个线程,如果此时增加栈内存为 2M ,那么此时就只能存放250个线程了,所以不是越大越好。

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

看情况,如果方法内局部变量没有逃离方法的作用访问,它是线程安全的。如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全。

2.4 栈内存溢出

2.4.1 栈帧过多导致内存溢出

方法的连续调用且无法释放资源,即栈帧都被加载到虚拟机栈里面,就会出现虚拟机栈的栈帧过多导致内存溢出的情况。比如下面的代码就会导致内存溢出

java 复制代码
// 配置栈空间参数:-Xss16k
// 递归调用,方法一直调用自己就会导致栈内存溢出,打印 count 来记录到底递归了多少次
public class Demo1_2 {
    private static int count;

    public static void main(String[] args) {
        try {
            method1();
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(count);
        }
    }
    private static void method1() {
        count++;
        method1();
    }
}

2.4.2 栈帧过大导致内存溢出

如果栈帧特别的大,把我的虚拟机栈给撑满了也会导致内存溢出, 不过这种情况很少见。

2.5 线程运行诊断

2.5.1 cpu 占用过高

首先在 linux环境里面运行一段代码,如下所示:

java 复制代码
public class Demo1_16 {

    public static void main(String[] args) {
        new Thread(null, () -> {
            System.out.println("1...");
            while(true) {

            }
        }, "thread1").start();


        new Thread(null, () -> {
            System.out.println("2...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread2").start();

        new Thread(null, () -> {
            System.out.println("3...");
            try {
                Thread.sleep(1000000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thread3").start();
    }
}

然后在 linux 环境下启动这段代码,用后台的方式运行,命令如下,此时我们就模拟了生产环境 cpu占用过高的场景。

bash 复制代码
nohup java cn.itcast.jvm.t1.stack.Demo1_16 & 

第一步:输入 top 命令查看当前 linux 进程的 cpu 占用情况,如下所示,我们发现我们的 java 进程的 cpu几乎快要满了。

第二步:使用 ps 命令来查看进程里面到底是哪个线程引起的 cpu占用过高,命令如下:

bash 复制代码
# H 参数表示输出进程树
# -eo 参数表示后面需要输出的东西,比如 pid,tid,%cpu 表示对 cpu 的占用情况
# grep 表示筛选指定的进程 id
ps H -eo pid,tid,%cpu | grep 进程id

第三步:使用 jstack 工具,他会把我们这个进程中的所有线程 id给我们列出来

bash 复制代码
jstack 进程 id
bash 复制代码
[root@localhost src]# jstack 67951
2023-09-06 01:58:54
Full thread dump OpenJDK 64-Bit Server VM (25.262-b10 mixed mode):

"Attach Listener" #13 daemon prio=9 os_prio=0 tid=0x00007f3424001000 nid=0x10c2f waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #12 prio=5 os_prio=0 tid=0x00007f346804b800 nid=0x10970 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"thread3" #11 prio=5 os_prio=0 tid=0x00007f34681ee800 nid=0x10980 waiting on condition [0x00007f344fad1000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.jvm.t1.stack.Demo1_16.lambda$main$2(Demo1_16.java:29)
	at cn.itcast.jvm.t1.stack.Demo1_16$$Lambda$3/135721597.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"thread2" #10 prio=5 os_prio=0 tid=0x00007f34681ec800 nid=0x1097f waiting on condition [0x00007f344fbd2000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
	at java.lang.Thread.sleep(Native Method)
	at cn.itcast.jvm.t1.stack.Demo1_16.lambda$main$1(Demo1_16.java:20)
	at cn.itcast.jvm.t1.stack.Demo1_16$$Lambda$2/303563356.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"thread1" #9 prio=5 os_prio=0 tid=0x00007f34681ea800 nid=0x1097e runnable [0x00007f34541ab000]
   java.lang.Thread.State: RUNNABLE
	at cn.itcast.jvm.t1.stack.Demo1_16.lambda$main$0(Demo1_16.java:11)
	at cn.itcast.jvm.t1.stack.Demo1_16$$Lambda$1/471910020.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007f3468136800 nid=0x1097c runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007f3468123800 nid=0x1097b waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f3468121800 nid=0x1097a waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f3468114000 nid=0x10979 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f3468112000 nid=0x10978 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f34680e4000 nid=0x10977 in Object.wait() [0x00007f34548e3000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000edc08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
	- locked <0x00000000edc08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f34680df800 nid=0x10976 in Object.wait() [0x00007f34549e4000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000edc06c00> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x00000000edc06c00> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=0 tid=0x00007f34680d5800 nid=0x10975 runnable 

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f346805e000 nid=0x10971 runnable 

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f3468060000 nid=0x10972 runnable 

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f3468062000 nid=0x10973 runnable 

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f3468063800 nid=0x10974 runnable 

"VM Periodic Task Thread" os_prio=0 tid=0x00007f346814b800 nid=0x1097d waiting on condition 

JNI global references: 311

[root@localhost src]# 

第四步:根据我们在第二步找到的线程 id67966 ,这个是十进制的,而我们第三步输出的线程 id 为十六进制的,需要将 67966 转换为十六进制的形式为:1097e ;然后查看查看第三步的线程 id1097e 的线程,如下所示:

此时我们就定位到了问题出现了 Thread-1 的这个线程这里,他的线程状态是 Runnable ,处于一直运行的状态,就是他导致的 cpu 占用过高的问题,从它的下面就可以看出来是第11行的代码出现了问题,接下来就可以解决了。

2.5.2 程序运行长时间没有结果

想象一个场景,比如点击新增按钮长时间没有返回,或者其他的那种长时间服务器没有反应的情况,该怎么办呢?我们用代码来模拟下这种场景,代码如下所示:

java 复制代码
package cn.itcast.jvm.t1.stack;

/**
 * 演示线程死锁
 */
class A{};
class B{};
public class Demo1_3 {
    static A a = new A();
    static B b = new B();


    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
        Thread.sleep(1000);
        new Thread(()->{
            synchronized (b) {
                synchronized (a) {
                    System.out.println("我获得了 a 和 b");
                }
            }
        }).start();
    }

}

然后在 linux环境下启动这段代码,用后台的方式运行,命令如下

bash 复制代码
[root@localhost src]# nohup java cn.itcast.jvm.t1.stack.Demo1_3 &
[1] 69772

第一步:使用 jstack命令查看当前进程里面的线程情况,我们可以观察下最后的几行代码

bash 复制代码
[root@localhost src]# jstack 69772
2023-09-06 02:15:27
Full thread dump OpenJDK 64-Bit Server VM (25.262-b10 mixed mode):

"Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f513c001000 nid=0x110b8 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f517c04b800 nid=0x1108d waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #10 prio=5 os_prio=0 tid=0x00007f517c20c800 nid=0x1109d waiting for monitor entry [0x00007f5166fc6000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at cn.itcast.jvm.t1.stack.Demo1_3.lambda$main$1(Demo1_3.java:30)
	- waiting to lock <0x00000000edc64ab8> (a cn.itcast.jvm.t1.stack.A)
	- locked <0x00000000edc65dc0> (a cn.itcast.jvm.t1.stack.B)
	at cn.itcast.jvm.t1.stack.Demo1_3$$Lambda$2/303563356.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"Thread-0" #9 prio=5 os_prio=0 tid=0x00007f517c20a800 nid=0x1109c waiting for monitor entry [0x00007f51670c7000]
   java.lang.Thread.State: BLOCKED (on object monitor)
	at cn.itcast.jvm.t1.stack.Demo1_3.lambda$main$0(Demo1_3.java:22)
	- waiting to lock <0x00000000edc65dc0> (a cn.itcast.jvm.t1.stack.B)
	- locked <0x00000000edc64ab8> (a cn.itcast.jvm.t1.stack.A)
	at cn.itcast.jvm.t1.stack.Demo1_3$$Lambda$1/471910020.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007f517c136800 nid=0x1109a runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007f517c123800 nid=0x11099 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f517c121800 nid=0x11098 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f517c114000 nid=0x11097 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f517c112000 nid=0x11096 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f517c0e4000 nid=0x11095 in Object.wait() [0x00007f5167bfa000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000edc08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
	- locked <0x00000000edc08ee0> (a java.lang.ref.ReferenceQueue$Lock)
	at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
	at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f517c0df800 nid=0x11094 in Object.wait() [0x00007f5167cfb000]
   java.lang.Thread.State: WAITING (on object monitor)
	at java.lang.Object.wait(Native Method)
	- waiting on <0x00000000edc06c00> (a java.lang.ref.Reference$Lock)
	at java.lang.Object.wait(Object.java:502)
	at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
	- locked <0x00000000edc06c00> (a java.lang.ref.Reference$Lock)
	at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=0 tid=0x00007f517c0d5800 nid=0x11093 runnable 

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f517c05e000 nid=0x1108f runnable 

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f517c060000 nid=0x11090 runnable 

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f517c062000 nid=0x11091 runnable 

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f517c063800 nid=0x11092 runnable 

"VM Periodic Task Thread" os_prio=0 tid=0x00007f517c13b800 nid=0x1109b waiting on condition 

JNI global references: 310


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f5148002178 (object 0x00000000edc64ab8, a cn.itcast.jvm.t1.stack.A),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f5148006218 (object 0x00000000edc65dc0, a cn.itcast.jvm.t1.stack.B),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
	at cn.itcast.jvm.t1.stack.Demo1_3.lambda$main$1(Demo1_3.java:30)
	- waiting to lock <0x00000000edc64ab8> (a cn.itcast.jvm.t1.stack.A)
	- locked <0x00000000edc65dc0> (a cn.itcast.jvm.t1.stack.B)
	at cn.itcast.jvm.t1.stack.Demo1_3$$Lambda$2/303563356.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
"Thread-0":
	at cn.itcast.jvm.t1.stack.Demo1_3.lambda$main$0(Demo1_3.java:22)
	- waiting to lock <0x00000000edc65dc0> (a cn.itcast.jvm.t1.stack.B)
	- locked <0x00000000edc64ab8> (a cn.itcast.jvm.t1.stack.A)
	at cn.itcast.jvm.t1.stack.Demo1_3$$Lambda$1/471910020.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.

第二步:从下面看就可以发现了死锁的说明,下面就是具体哪两个线程发生了死锁,然后解决就可以了。

三、本地方法栈

3.1 定义

jvm 为使用 native方法提供所需要的内存,称为本地方法栈。

3.2 特点

线程私有

四、堆

4.1 定义

通过 new关键字创建的对象都会使用堆内存

4.2 特点

1、线程共享的,堆中的对象需要考虑线程安全的问题

2、有垃圾回收机制。

4.3 堆内存溢出

下面的代码就可以模拟堆内存溢出的场景,如下所示:

java 复制代码
/* 配置堆空间参数:-Xms8m  */
public class Demo1_5 {

    public static void main(String[] args) {
        int i = 0;
        try {
            List<String> list = new ArrayList<>();
            String a = "hello";
            while (true) {
                list.add(a);
                a = a + a;  
                i++;
            }
        } catch (Throwable e) {
            e.printStackTrace();
            System.out.println(i);
        }
    }
}

4.4 堆内存诊断

首先运行下面的代码用于模拟堆内存特别大的场景

java 复制代码
public class Demo1_4 {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("1...");
        Thread.sleep(30000);
        byte[] array = new byte[1024 * 1024 * 10]; // 10 Mb
        System.out.println("2...");
        Thread.sleep(20000);
        array = null;
        // 手动触发 gc 操作
        System.gc();
        System.out.println("3...");
        Thread.sleep(1000000L);
    }
}

4.4.1 jmap 工具

首先查看当前系统有哪些 java 进程,并将他们的进程编号显示出来,使用命令 jps ,如下所示,找到我们正在运行的 java 进程编号 24764

然后使用 jmap命令查看堆内存的使用情况,不过查询的只是某一时刻的情况,语法如下:

bash 复制代码
jmap -heap 进程id
bash 复制代码
# 命令一共需要运行三次,对应程序三种不同的状态

# 当程序运行输出 1... 的时候第一次执行
# 此时的状态还没有向堆内存中存放对象
C:\Users\Administrator\Desktop\资料 解密JVM\代码\jvm\jvm>jmap -heap 24764
Attaching to process ID 24764, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4083154944 (3894.0MB)
   NewSize                  = 84934656 (81.0MB)
   MaxNewSize               = 1361051648 (1298.0MB)
   OldSize                  = 170917888 (163.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 63963136 (61.0MB)
   used     = 6434872 (6.136772155761719MB)
   free     = 57528264 (54.86322784423828MB)
   10.060282222560195% used
From Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
To Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
PS Old Generation
   capacity = 170917888 (163.0MB)
   used     = 0 (0.0MB)
   free     = 170917888 (163.0MB)
   0.0% used

3179 interned Strings occupying 281368 bytes.

# 当程序运行输出 2... 的时候第二次执行
# 此时向堆内存中放了一个对象,占用了新生代的10M的内存
C:\Users\Administrator\Desktop\资料 解密JVM\代码\jvm\jvm>jmap -heap 24764
Attaching to process ID 24764, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4083154944 (3894.0MB)
   NewSize                  = 84934656 (81.0MB)
   MaxNewSize               = 1361051648 (1298.0MB)
   OldSize                  = 170917888 (163.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 63963136 (61.0MB)
   used     = 16920648 (16.13678741455078MB)
   free     = 47042488 (44.86321258544922MB)
   26.453749859919313% used
From Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
To Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
PS Old Generation
   capacity = 170917888 (163.0MB)
   used     = 0 (0.0MB)
   free     = 170917888 (163.0MB)
   0.0% used

3180 interned Strings occupying 281416 bytes.

# 当程序运行输出 3... 的时候第三次执行
# 由于对象被赋值为 null 且手动触发了一次 GC,导致内存又都被回收起来了
C:\Users\Administrator\Desktop\资料 解密JVM\代码\jvm\jvm>jmap -heap 24764
Attaching to process ID 24764, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.131-b11

using thread-local object allocation.
Parallel GC with 8 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4083154944 (3894.0MB)
   NewSize                  = 84934656 (81.0MB)
   MaxNewSize               = 1361051648 (1298.0MB)
   OldSize                  = 170917888 (163.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 63963136 (61.0MB)
   used     = 1279280 (1.2200164794921875MB)
   free     = 62683856 (59.77998352050781MB)
   2.000027015560963% used
From Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
To Space:
   capacity = 10485760 (10.0MB)
   used     = 0 (0.0MB)
   free     = 10485760 (10.0MB)
   0.0% used
PS Old Generation
   capacity = 170917888 (163.0MB)
   used     = 1076400 (1.0265350341796875MB)
   free     = 169841488 (161.9734649658203MB)
   0.6297760945887654% used

3166 interned Strings occupying 280424 bytes.

4.4.2 jconsole 工具

图形界面的,多功能的检测工具,可以连续监测,重新启动工程,并在控制台输入命令:jconsole,如下所示:

接下来就等于启动了 jconsole软件 ,就可以查看堆内存的实时变化信息了。

4.4.3 jvisualvm 工具

首先模拟一个场景,垃圾回收后,内存占用仍然很高,代码如下:

java 复制代码
/**
 * 演示查看对象个数 堆转储 dump
 */
public class Demo1_13 {

    public static void main(String[] args) throws InterruptedException {
        List<Student> students = new ArrayList<>();
        for (int i = 0; i < 200; i++) {
            students.add(new Student());
//            Student student = new Student();
        }
        Thread.sleep(1000000000L);
    }
}
class Student {
    private byte[] big = new byte[1024*1024];
}

将代码运行起来,启动 jvisualvm工具,如下所示:

五、方法区

5.1 定义

方法区在 jvm 启动的时候被创建,逻辑上是堆的一个组成部分,但是不同的 jvm厂商在实现的时候不一定会遵从这个逻辑上的定义。

方法区存储了跟类的结构有关的信息,包括运行时常量池、成员变量、方法数据、成员方法和构造器方法代码部分,还有一些特殊方法(类的构造器),并且是线程共享的。

方法区在申请内存时发现不足了,也会报 outOfMemoryError。

5.2 特点

Hotspotjdk1.6jdk1.8的内存结构为例,方法区只是一个概念上的东西。

jdk1.6里面方法区是使用永久代来实现的,这个永久代里面包含了运行时常量池、类的信息、类加载器等。

到了 jdk1.8 以后,永久代的这个实现就被废弃了,变成了使用元空间,他存储了类的信息、类加载器、运行时常量池,但是他已经不占用堆内存了,换句话说就是不由我们的 jvm 来管理了,它被移出到我们的本地内存(操作系统内存)当中,但**StringTable(串池)**被移动到了堆里面。

5.3 方法区内存溢出

5.3.1 jdk1.8 以前会导致永久代内存溢出

下面的代码可以复现永久代内存溢出的情况,首先我们需要将 jdk 调成 1.6 ,然后修改下 jvm的参数,命令如下:

bash 复制代码
# 限制永久代最大的内存,为了更好的复现问题
-XX:MaxPermSize=8m

执行下面的代码

java 复制代码
import com.sun.xml.internal.ws.org.objectweb.asm.ClassWriter;
import com.sun.xml.internal.ws.org.objectweb.asm.Opcodes;

/**
 * 演示永久代内存溢出  java.lang.OutOfMemoryError: PermGen space
 * -XX:MaxPermSize=8m
 */
public class Demo1_6 extends ClassLoader {
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_6 test = new Demo1_6();
            for (int i = 0; i < 100000; i++, j++) {
                ClassWriter cw = new ClassWriter(0);
                cw.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                byte[] code = cw.toByteArray();
                test.defineClass("Class" + i, code, 0, code.length);
            }
        } finally {
            System.out.println(j);
        }
    }
}

根据提示的错误信息我们可以看出来,提示永久代的内存溢出了

5.3.2 jdk1.8 以后会导致元空间内存溢出

首先将 jdk 调成 **1.8,**然后执行下面的代码

java 复制代码
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Opcodes;

/**
 * 演示元空间内存溢出 java.lang.OutOfMemoryError: Metaspace
 * -XX:MaxMetaspaceSize=8m
 */
public class Demo1_8 extends ClassLoader { // 可以用来加载类的二进制字节码
    public static void main(String[] args) {
        int j = 0;
        try {
            Demo1_8 test = new Demo1_8();
            for (int i = 0; i < 10000; i++, j++) {
                // ClassWriter 作用是生成类的二进制字节码
                ClassWriter cw = new ClassWriter(0);
                // 版本号, public, 类名, 包名, 父类, 接口
                cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
                // 返回 byte[]
                byte[] code = cw.toByteArray();
                // 执行了类的加载
                test.defineClass("Class" + i, code, 0, code.length); // Class 对象
            }
        } finally {
            System.out.println(j);
        }
    }
}

输出结果如下,并没有报错,因为元空间默认情况下使用的是系统的内存,而且没有设置它的上限。

我们修改下 jvm的参数,命令如下:

bash 复制代码
# 限制元空间最大的内存,为了更好的复现问题
-XX:MaxMetaspaceSize=8m

再次运行程序,发现提示元空间的内存溢出了。

5.4 运行时常量池

一个 java 类想要运行,就需要先将其编译成**.class** 文件(二进制字节码),那这个**.class**文件有哪些部分组成呢?一般来说由三部分组成:类的基本信息、常量池、类方法定义包含了虚拟机指令。

5.4.1 常量池

常量池就是一张常量的表,虚拟机的指令根据这张常量表找到要执行的类名、方法名、参数类型和字面量等信息。常量池是存储在 *.class文件中的。

5.4.2 运行时常量池

当我们所有的类被加载到虚拟机中以后,每一个的类的常量池信息就都会被放入运行时常量池,并把里面的符号地址变为真实地址。运行时常量池是在内存当中存储的。

5.5 StringTable 串池

5.5.1 编译分析

先看一段代码,然后我们分析下它的执行逻辑

java 复制代码
String s1 = "a";
String s2 = "b";
String s3 = "ab";

刚才我们说过,常量池中的信息在运行的时候都会被加载到运行时常量池中,这时 abab 都是常量池中的符号 还没有成为 java字符串对象,需要等到代码执行到那一行才会变为对象。

当执行到 String s1 = "a" 的时候,首先先去 StringTable 里面看看有没有 a 对象,如果有就直接使用,如果没有,则把**"a"**放进去,然后再返回,执行完一条语句判断一次。

StringTablehashtable结构,不能扩容。

java 复制代码
// s1 和s2 是变量,引用的值有可能在执行的使用发生变化
// 所以在堆内存中新创建的对象
String s4 = s1 + s2;  
System.out.println(s3 == s4); //false

5.5.2 编译期自动优化

因为 s3 的**"ab"** 值在 StringTable 中,s4在堆内存中,所以不等。

java 复制代码
// javac 在编译期的优化
String s5 = "a" + "b";  
System.out.println(s3 == s5); // true

编译器认为:"a""b" 是常量,内容不会变了,所以他俩拼接的结果是固定的,那我就可以在编译期间得到结果为 "ab"。

5.5.3 jdk1.8 的 intern 方法

**intern()**方法:将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,最会会把串池中的对象返回。

java 复制代码
public static void main(String[] args) {
		String s = new String("a") + new String("b");
		// 执行完词条语句的存储状态是为:
		// stringtable里面:["a","b"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		
		String s2 = s.intern();
        // 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,最会会把串池中的对象返回
		// 执行完此条语句的存储状态是:
		// stringtable里面:["a","b","ab"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		// 并且 s2 返回的对象是串池中的 "ab" 对象
		
		System.out.println(s2 =="ab"); // true
		System.out.println(s =="ab"); // true
	}
java 复制代码
public static void main(String[] args) {
		String x= "ab";
		String s = new String("a") + new String("b");
		// 执行完词条语句的存储状态是为:
		// stringtable里面:["ab","a","b"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		
		String s2 = s.intern();
        // 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则放入串池,最会会把串池中的对象返回
		// 执行完此条语句的存储状态是:
		// stringtable里面:["ab","a","b"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		// 并且 s2 返回的对象是串池中的 "ab" 对象
		
		System.out.println(s2 == x); // true
		System.out.println(s == x); // false ,因为没放进去,s引用的还是堆内存的对象
	}

5.5.4 jdk1.6 的 intern 方法

**intern()**方法:将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则拷贝一份放入串池,最会会把串池中的对象返回

java 复制代码
public static void main(String[] args) {
		String s = new String("a") + new String("b");
		// 执行完词条语句的存储状态是为:
		// stringtable里面:["a","b"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		
		String s2 = s.intern();
        // 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则拷贝一份放入串池,最会会把串池中的对象返回
		// 执行完此条语句的存储状态是:
		// stringtable里面:["a","b","ab"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		// 并且 s2 返回的对象是串池中的 "ab" 对象
		
		System.out.println(s2 =="ab"); // true
		System.out.println(s =="ab"); // false,因为是复制了一份放入串池,所以 s 引用的还是堆内存的对象
	}
java 复制代码
public static void main(String[] args) {
		String x= "ab";
		String s = new String("a") + new String("b");
		// 执行完词条语句的存储状态是为:
		// stringtable里面:["ab","a","b"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		
		String s2 = s.intern();
        // 将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则拷贝一份放入串池,最会会把串池中的对象返回
		// 执行完此条语句的存储状态是:
		// stringtable里面:["ab","a","b"]
		// 堆内存里面:[new String("a")、new String("b")、new String("ab")]
		// 并且 s2 返回的对象是串池中的 "ab" 对象
		
		System.out.println(s2 == x); // true
		System.out.println(s == x); // false ,因为没放进去,s 引用的还是堆内存的对象
	}

5.6 StringTable 位置

jdk1.6 常量出是存储在永久代里面,而从 jdk1.7 开始就转移到了堆中,因为永久代的内存回收效率很低,只有 full gc 的时候才会触发永久代的垃圾回收,就会导致 StringTable 的回收效率不高。到了 jdk1.7 之后,放在堆里面只需要 minor gc 就可以触发垃圾回收。

5.7 StringTable 性能调优

1、调整 -XX:StringTableSize=桶个数

2、考虑将字符串对象是否入池

相关推荐
学点东西吧.13 小时前
JVM(一、基础知识)
java·jvm
XMYX-014 小时前
JVM 参数配置入门与优化案例
jvm
努力进修15 小时前
“高级Java编程复习指南:深入理解并发编程、JVM优化与分布式系统架构“
java·jvm·架构
秋の花16 小时前
【JAVA基础】JVM双亲委派
java·开发语言·jvm
customer0819 小时前
【开源免费】基于SpringBoot+Vue.JS医疗病历交互系统(JAVA毕业设计)
java·jvm·vue.js·spring boot·后端·spring cloud·kafka
Enoch88819 小时前
Day09 C++ 存储类
java·jvm·c++
YYYYYY020201 天前
C++中级学习笔记
jvm·笔记·学习
花心蝴蝶.1 天前
并发编程中常见的锁策略
java·jvm·windows
白总Server1 天前
JVM 处理多线程并发执行
jvm·后端·spring cloud·微服务·ribbon·架构·数据库架构
菜菜-plus1 天前
监控JVM的内存使用情况分析
jvm