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 栈帧过多导致内存溢出

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

// 配置栈空间参数:-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 环境里面运行一段代码,如下所示:

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 占用过高的场景。

nohup java cn.itcast.jvm.t1.stack.Demo1_16 & 

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

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

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

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

jstack 进程 id
[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]# 

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

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

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

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

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 环境下启动这段代码,用后台的方式运行,命令如下

[root@localhost src]# nohup java cn.itcast.jvm.t1.stack.Demo1_3 &
[1] 69772

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

[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 堆内存溢出

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

/* 配置堆空间参数:-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 堆内存诊断

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

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 命令查看堆内存的使用情况,不过查询的只是某一时刻的情况,语法如下:

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

# 当程序运行输出 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 工具

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

/**
 * 演示查看对象个数 堆转储 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 特点

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

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

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

5.3 方法区内存溢出

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

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

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

         执行下面的代码

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,然后执行下面的代码

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 的参数,命令如下:

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

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

5.4 运行时常量池

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

5.4.1 常量池

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

5.4.2 运行时常量池

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

5.5 StringTable 串池

5.5.1 编译分析

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

String s1 = "a";
String s2 = "b";
String s3 = "ab";

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

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

        StringTable hashtable 结构,不能扩容。

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

5.5.2 编译期自动优化 

        因为 s3 ”ab“ 值在 StringTable 中,s4 在堆内存中,所以不等。

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

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

5.5.3 jdk1.8 的 intern 方法

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

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
	}
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() 方法:将这个字符串对象尝试放入串池,如果有则不会放入,如果没有则拷贝一份放入串池,最会会把串池中的对象返回

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 引用的还是堆内存的对象
	}
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、考虑将字符串对象是否入池

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

快乐的小三菊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值