JVM 从入门到入坑~长文预警。

JVM 从入门到入坑。



从常见面试题引入。

请谈谈对 JVM 的理解。Java 8 的虚拟机有什么更新?
什么是 OOM?什么是 StackOverflowError?有哪些方法分析?
JVM 的常用参数调优你知道哪些?
谈谈 JVM 中对类加载器的理解。

  • JVM 体系结构概述。
  • 堆体系结构概述。
  • 堆参数调优入门。
  • 总结。

JVM 体系结构。

  • JVM 位置。

Java Virtual Machine
  ↓  ↑
操作系统(eg. WIndows, Linux 等)。
  ↓  ↑
硬件系统(eg. Intel 体系、SPAC 等)。

JVM 是运行在操作系统之上的,ta 与硬件没有直接的交互。

JNI —> Java Native Interface。


  • jvm 体系结构图。

在这里插入图片描述

在这里插入图片描述


类装载器 ClassLoader。

负责加载 .class 文件,.class 文件在文件开头有特定的文件标示。将 .class 文件字节码内容加载到内存中,并将这些内容转换成方法区中的运行时数据结构并且 ClassLoader 只负责 .class 文件的加载。至于 ta 是否可以运行,则由 Exception Engine 决定。

在这里插入图片描述

echo %JAVA_HOME%
echo %PATH%
echo %CLASSPATH%

  • java 相关文件说明。

*.class 文件用 Sublime Text 打开,可以看到 —> 开头都是:cafe babe
jvm 通过 cafe babe 识别 .class 文件)。

cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 154c 636f 6d2f 6765
656b 2f48 656c 6c6f 576f 726c 643b 0100
046d 6169 6e01 0016 285b 4c6a 6176 612f
6c61 6e67 2f53 7472 696e 673b 2956 0100
0461 7267 7301 0013 5b4c 6a61 7661 2f6c
616e 672f 5374 7269 6e67 3b01 000a 536f
7572 6365 4669 6c65 0100 0f48 656c 6c6f
576f 726c 642e 6a61 7661 0c00 0700 0807
001c 0c00 1d00 1e01 0005 6865 6c6c 6f07
001f 0c00 2000 2101 0013 636f 6d2f 6765
656b 2f48 656c 6c6f 576f 726c 6401 0010
6a61 7661 2f6c 616e 672f 4f62 6a65 6374
0100 106a 6176 612f 6c61 6e67 2f53 7973
7465 6d01 0003 6f75 7401 0015 4c6a 6176
612f 696f 2f50 7269 6e74 5374 7265 616d
3b01 0013 6a61 7661 2f69 6f2f 5072 696e
7453 7472 6561 6d01 0007 7072 696e 746c
6e01 0015 284c 6a61 7661 2f6c 616e 672f
5374 7269 6e67 3b29 5600 2100 0500 0600
0000 0000 0200 0100 0700 0800 0100 0900
0000 2f00 0100 0100 0000 052a b700 01b1
0000 0002 000a 0000 0006 0001 0000 0003
000b 0000 000c 0001 0000 0005 000c 000d
0000 0009 000e 000f 0001 0009 0000 0037
0002 0001 0000 0009 b200 0212 03b6 0004
b100 0000 0200 0a00 0000 0a00 0200 0000
0600 0800 0700 0b00 0000 0c00 0100 0000
0900 1000 1100 0000 0100 1200 0000 0200
13

在这里插入图片描述


方法区:放类的描述信息,放类的模板。

在这里插入图片描述


类装载器 ClassLoader 种类。

在这里插入图片描述

  • 虚拟机自带的加载器。
    启动类加载器(Bootstrap)C++ . . . . .(C++)-- = Java // java 底层 C++。
    扩展类加载器(Extension)Java
    应用程序类加载器(AppClassLoader) . . . . . . 一般自己 new 的使用 AppClassLoader。
    AppClassLoader 在 Java 中也叫系统类加载器,加载当前应用的 classpath 的所有类。

  • 用户自定义加载器。
    java.lang.ClassLoader 的子类,用户可以定制类的加载方式。

  • jdk 文件分析。

$JAVAHOME/jre/lib/rt.jar —> rt.jar(runtime)(1.0 版本)。Bootstrap 启动类加载器加载 ta。
rt.jar 中有 /java/lang/Object.class/java/lang/String.class/java/util/ArrayList.class

rt.jar 可以看作 Jdk 的第一代 1.0
后续发展了扩展包:$JAVAHOME/jre/lib/ext/*.jar。
ext —> extend —> javax.包。

在这里插入图片描述

详情见代码。

package com.geek;

public class MyObject {

    public static void main(String[] args) {

        Object object = new Object();

        System.out.println(object.getClass());
        // class java.lang.Object
        // 对象实例的模板。(Object 类)。

        System.out.println(object.getClass().getClassLoader());
        // null
        // 找那个快递员。
        // Object 祖宗,是 Bootstrap 启动类加载器加载进 Runtime Data Area。
        // Bootstrap 是 C++ 语言写的,java 得到 null。

//        System.out.println(object.getClass().getClassLoader().getParent());
        // Exception in thread "main" java.lang.NullPointerException
        //	at com.geek.MyObject.main(MyObject.java:23)。——> Bootstrap 已经是祖宗了。
//        System.out.println(object.getClass().getClassLoader().getParent().getParent());
        // Exception in thread "main" java.lang.NullPointerException
        //	at com.geek.MyObject.main(MyObject.java:26)

        System.out.println("~ ~ ~ ~ ~ ~ ~");

        MyObject myObject = new MyObject();
        System.out.println(myObject.getClass());
        // class com.geek.MyObject
        System.out.println(myObject.getClass().getClassLoader());
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        // 由`启动类加载器`。AppClassLoader 加载进 Runtime Data Area。

        // rt.jar/sun/misc/Launcher.class
        //  jvm 相关调用的入口程序。

        System.out.println(myObject.getClass().getClassLoader());
        // sun.misc.Launcher$AppClassLoader@18b4aac2
        System.out.println(myObject.getClass().getClassLoader().getParent());
        // sun.misc.Launcher$ExtClassLoader@74a14482
        System.out.println(myObject.getClass().getClassLoader().getParent().getParent());
        // null ——> Bootstrap。

    }
}

  • 用户自定义加载器。
public abstract class ClassLoader { ... }

ClassLoader 是一个抽象类。
Class xxx extends java.lang.ClassLoader {} 定制自己的加载器。


面试常问。

双亲委派机制。

我爸是李刚,有事找我爹。

我要用一个(A.java)的类,先去 Bootstrap Class Loader 找,找到就直接用;找不到就降一级,去 Extension Class Loader 中找,再找不到就报 ClassNotFoundException

在这里插入图片描述

案例:自己手动创建 java.lang.String。

package java.lang;

public class String {

    public static void main(String[] args) {
        System.out.println("hello");
    }

    /*
    Error: Main method not found in class java.lang.String, please define the main method as:
   public static void main(String[] args)
or a JavaFX application class must extend javafx.application.Application

Process finished with exit code 1

     */
}

根据双亲委派机制,jvm 会先去 Bootstrap 找 String 类(找到了 rt.jar/java/lang/String.class)。但 ta 没有 main() 方法。

=> 双亲委派机制 —> 保证用户写的代码不污染 java 的源代码。保证沙箱安全。保证加载的 class 都是同一个。

当一个类收到了类加载的请求,ta 首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次加载器都是如此。因此所有的加载请求都应该传送到启动类加载器中。只有当父类加载器反馈自己无法完成这个请求的时候(在 ta 的加载路径下没有找到所需加载的 class),子类加载器才会尝试自己去加载。
采用双亲委派机制的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

Execution Engine 执行引擎负责解释命令,提交操作系统执行。

  • 类装载器子系统包含的知识点。
    类加载器。
    有几种。
    双亲委派。
    沙箱安全。

沙箱安全机制。

本地接口。

多线程问题引入。

进程、线程问题和编程语言无关。

package com.geek.thread;

public class ThreadDemo {

    public static void main(String[] args) {
        Thread t1 = new Thread();

        t1.start();

    }
}

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

t1.start(); —> 实际是调用 start0();

public
class Thread implements Runnable {
    private native void start0();

}

class 中的 start0();只有方法的声明,没有方法的实现。
调用底层操作系统或 C 语言函数库。

native 方法 —> 本地方法栈(Native Method Stack)。
java 方法 —> Java 栈(Java Stack)。

Java 1995 年诞生。C 语言称霸。
要向 C 语言“交保护费”。
在 C 语言的淫威下,Java 可耻的屈服了。
——> native。

Native Interface 本地接口。

本地接口的作用是融合不同的编程语言为 Java 所用,ta 的初衷是融合 C / C++ 程序,Java 诞生的时候是 C / C++ 横行的时候,要想立足,必须有调用 C / C++ 程序。于是就在内存中专门开辟了一块区域处理标记为 native 的代码。ta 的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载 native libraries。
目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过 Java 程序驱动打印机或者 Java 系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用 Socket 通信,也可以使用 Web Service 等等。

Native Method Stack。

ta 的具体做法是 Native Method Stack 中登记 native 方法,在 Execution Engine 执行时加载本地方法库。

ps:t1.start(); 只能调用一次。

         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();


程序计数器。

Program Counter Register。

register
(in electronic devices) a location in a store of data, used for a specific purpose and with quick access time.

程序计数器是 CPU 的一部分。
—> 指针。—> 指向下一个要执行方法的地址。(课程表、值班表)。

每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的字节码(用来存储指向下一条指令的地址,也即将要执行的指令代码),由执行引擎读取下一条指令,是一个非常小的内存空间,几乎可以忽略不计。
这块内存区域很小,ta 是当前线程所执行的字节码的行号指示器,字节码解释器通过改变这个计数器来读取下一条需要执行的字节码指令。
如果执行的是一个 native 方法,那这个计数器是空的。
用以完成分支、循环、跳转异常处理、线程恢复等基础功能。不会发生内存溢出(OutOfMemory=OOM)错误。

在这里插入图片描述

Runtime Data Area 中,
灰色 —> 线程私有,内存占用非常少,几乎不存在 gc 垃圾回收机制。


summary.

  1. JVM 系统架构图。
  2. 类加载器。
    2.1. 加载器种类。
    2.2. 双亲委派。
    2.3. 沙箱安全机制。
  3. native。

.start() 以后,新建完成到就绪状态。待底层操作系统和 CPU 的调度。不一定立刻马上启动。
native 是一个关键字,有声明,无实现。
Native Method Stack。

  1. PC 寄存器。
    4.1. 记录了方法之间的调用和执行情况,类似排班值日表。

    用来存储指向下一条指令的地址,也即将要执行的指令代码,ta 是当前线程所执行的字节码的行号指示器。

  2. 方法区。

在这里插入图片描述


Method Area 方法区。

亮色。—> 所有线程共享。存在垃圾回收。

Method Area 方法区。

供各线程共享的运行时内存区域。ta 存储了每一个类的结构信息,例如运行时常量池(Runtime Constant Pool)、字段和方法数据、构造函数和普通方法的字节码内容。上面讲的是规范,在不同虚拟机里实现是不一样的,最典型的就是永久代(PermGen)和元空间(MetaSpace)。

实例变量存在堆内存中,和方法区无关。

在这里插入图片描述

Car Class —> 类的结构信息(模板)(变量、方法)。

空调 kt = new 格力();
LIst list = new ArrayList();
规范 <—> 不同实现。

天上飞的理念,必然有不同的落地实现。

汉堡 <—> 麦当劳,KFC,德克士、汉堡王。
空调 <—> 格力、海尔、小米。

方法区 f = new 永久带;(java 7 以前)。
方法区 f = new 元空间;(java 8).



Stack 栈。

  • 栈管运行,堆管存储。

e.printStackTrace();// 栈管运行,打印运行中的异常。

try {
    Thread.sleep(1);
} catch (InterruptedException e) {
    e.printStackTrace();
}

程序 = 算法 + 数据结构。—— 学院派。
程序 = 框架 + 业务逻辑。

栈。FIFO。
队列。FILO。

栈也叫占内存,主管 Java 程序的运行,是在线程创建时创建,ta 的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束该栈就 over,生命周期和线程一致,是线程私有的。8 种基本类型的变量 + 对象的引用变量 + 实例方法都是在函数的栈内存中分配的。

栈帧中主要保存 3 类数据。

  • 本地变量(Local Variable):输入参数和输出参数以及方法内的变量。
    栈操作(Operand Stack):记录出栈、入栈的操作。
    栈帧数据(Frame Data):包括类文件、方法等。

线程私有,不存在垃圾回收。

栈帧:Java 方法进入 JVM 中就叫栈帧。

package com.geek;

public class StackDemo {

    private static void m1() {
        System.out.println("222");
        System.out.println("~~~1");
        System.out.println("333");
    }

    public static void main(String[] args) {
        System.out.println("111");
        m1();
        System.out.println("444");
    }
}

~~~

111
222
~~~1
333
444

Process finished with exit code 0

package com.geek;

public class StackDemo {

    private static void m1() throws InterruptedException {
        System.out.println("222");
        System.out.println("~~~1");
        System.out.println("333");

        Thread.sleep(Integer.MAX_VALUE);
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("111");
        m1();
        System.out.println("444");
    }
}


~~~

111
222
~~~1
333

...
...

  • 栈运行原理。

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集,当一个方法 A 被调用时就产生了一个栈帧 F,并被压入到栈中。
A 方法又调用了 B 方法,于是产生 栈帧 F2 也被压入栈,
B 方法又调用了 C 方法,于是产生 栈帧 F3 也被压入栈,

执行完毕后,先弹出 F3 栈帧,再弹出 F2 栈帧,再弹出 F1 栈帧…
遵循先进后出 / 后进先出原则。
每个方法执行的同事都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中入栈到出栈的过程。栈的大小和具体 JVM 的实现有关,通常在 256K ~ 756K 之间,约等于 1 MB 左右。

在这里插入图片描述

package com.geek;

public class StackDemo {

    private static void m1() throws InterruptedException {
        m1();
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println("111");
        m1();
        System.out.println("444");
    }
}

~~~

Exception in thread "main" java.lang.StackOverflowError

StackOverflowError 算错误,不是异常。
public class StackOverflowError extends VirtualMachineError {
abstract public class VirtualMachineError extends Error {
public class Error extends Throwable {
public class Throwable( implements Serializable { )


堆、栈、方法区的交互。

在这里插入图片描述

geek@geek-PC:~$ java -version
openjdk version "1.8.0_181"
OpenJDK Runtime Environment (build 1.8.0_181-8u181-b13-2~deb9u1-b13)
OpenJDK 64-Bit Server VM (build 25.181-b13, mixed mode)

geek@geek-PC:~/geek/tools_my/jdk1.8.0_241/bin$ ./java -version
java version "1.8.0_241"
Java(TM) SE Runtime Environment (build 1.8.0_241-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.241-b07, mixed mode)

HotSpot —> jdk 的名字。
Goslin 提出了 jdk 的规范,只要把接口都实现,就可以自成一派。
Sun 公司 —> HotSpot。
BEA 公司 —> JRocket。
Oracle 收购了 Sun 和 BEA 全部软件、硬件。(两套 JVM 规范)。
合二为一 —> HotSpot。

还有 IBM 的 J9 Virtual Machine


Heap 堆。

物理上:新生 + 养老。
逻辑上:新生 + 养老 + 永久。

在这里插入图片描述

java 7 —> 永久代。
java 8 —> 元空间。

在这里插入图片描述

引用类型 ~ 对象和 String 的区别。

对象和 String 的区别。

对象、String 的引用都在栈内存中。
但对象的实例在堆内存中,
而 String 的实例在常量池中。如果有就直接复用,如果没有则新建。

在这里插入图片描述

package com.geek.transferValue;

public class TransferValueDemo {

    public void changeValue01(int age) {
        age = 30;
    }

    public void changeValue02(Person person) {
        person.setName("xxx");
    }

    public void changeValue03(String string) {
        string = "xxx";
    }


    public static void main(String[] args) {

        TransferValueDemo test = new TransferValueDemo();

        int age = 20;
        test.changeValue01(age);
        System.out.println("age = " + age);

        Person person = new Person("Geek");
        test.changeValue02(person);
        System.out.println("person _ name = " + person.getName());

        String string = "geek";
        test.changeValue03(string);
        System.out.println("string = " + string);
    }
}

~~~

age = 20
person _ name = xxx
string = geek

Process finished with exit code 0


对象生命周期和 GC。

gc 之后有交换,谁空谁是 to。

在这里插入图片描述


MinorGC 的过程。(复制 —> 清空 —> 互换)。
  • eden, survivorFrom 复制到 SurviorTo,年龄 + 1。

首先,当 Eden 区満的时候会触发第一次 GC,把还活着的对象拷贝到 SurvivorFrom 区,当 Eden 区再次触发 GC 的时候会扫描 Eden 区和 From 区,对这两个区域进行垃圾回收,经过这次回收后还存活的对象,则直接复制到 To 区域(如果有对象已经达到老年的标准,则复制到老年代区),同时把这些对象的年龄 + 1。

  • 清空 Eden, SurvivorFrom。

然后,清空 Eden 和 Survivor 中的对象,也即复制之后有交换,谁空谁是 To。

  • SurvivorrTo 和 SurvivorFrom 互换。

最后,SurvivorTo 和 SurvivorFrom 互换,原 SurvivorTo 成为下一次 GC 时的 SurvivorFrom 区。部分对象会在 From 和 To 区域复制来复制去,如此交换 15 次(由 JVM 参数 MaxTenuringThreshold 决定,这个参数默认是 15),最终如果还是存活就进入老年代。

在这里插入图片描述


永久带。

永久存储区是一个常驻内存区域,用于存放 JDK 自身所携带的 Class,Interface 的元数据,也就是说 ta 存储的是运行环境必须的类的信息,被装载进此区域的数据是不会被垃圾回收器回收的,关闭 JVM 才会释放此区域所占用的内存。

eg.
使用 Spring 时加的 Spring jar 包,
使用 SpringMVC 时加的 SpringMVC jar 包。


堆参数调优。

在 Java 8 中,永久代已经被消除,被一个称为元空间的区域所取代。元空间的本质和永久代类似。
元空间永久代最大区别:

永久带使用的是 JVM 堆内存,但是 java 8 以后的元空间并不在虚拟机中而是使用本机物理内存

因此,默认情况下,元空间的大小受本地内存限制。类的元数据放入 native memory,字符串池和类的静态变量放入 Java 堆中,这样可以加载多少类的元数据就不再由 MaxPermSize 控制,而由系统的实际可用空间来控制。

在这里插入图片描述

常量池元空间
JVM 默认使用物理内存的 1 / 4


堆内存调优。
-Xms 设置初始分配大小。默认为物理内存的 1 / 64
-Xmx 最大分配内存。默认为物理内存的 1 / 4
-XX:+PrintGCDetails 输出详细的 GC 处理日志。
package com.geek;

public class JVMDemo {

    public static void main(String[] args) {

        // 返回 Java 虚拟机试图使用的最大内存。
        long maxMemory = Runtime.getRuntime().maxMemory();

        System.out.println("(-Xmx)maxMemory = " + maxMemory + "(字节)。" + (maxMemory / (double) 1024 / 1024) + " MB");

        // 返回 Java 虚拟机中的内存总量。
        long totalMemory = Runtime.getRuntime().totalMemory();

        System.out.println("(-Xmx)totalMemory = " + totalMemory + "(字节)。" + (totalMemory / (double) 1024 / 1024) + " MB");

        System.out.println("~ ~ ~ ~ ~ ~ ~");

        System.out.println(Runtime.getRuntime().availableProcessors());
    }
}


~~~

(-Xmx)maxMemory = 1832910848(字节)。1748.0 MB
(-Xmx)totalMemory = 124780544(字节)。119.0 MB
~ ~ ~ ~ ~ ~ ~
4

Process finished with exit code 0


调整。

一般要将 JVM 初始内存和最大内存调整为一样。避免怱高怱低。

在这里插入图片描述

VM Options 填写:

-Xms1024m -Xmx1024m -XX:+PrintGCDetails

在这里插入图片描述

再次运行,可以发现 maxMemorytotalMemory 已变化。

(-Xmx)maxMemory = 1029177344(字节)。981.5 MB
(-Xmx)totalMemory = 1029177344(字节)。981.5 MB
~ ~ ~ ~ ~ ~ ~
4
Heap
 PSYoungGen      total 305664K, used 20971K [0x00000000eab00000, 0x0000000100000000, 0x0000000100000000)
  eden space 262144K, 8% used [0x00000000eab00000,0x00000000ebf7afb8,0x00000000fab00000)
  from space 43520K, 0% used [0x00000000fd580000,0x00000000fd580000,0x0000000100000000)
  to   space 43520K, 0% used [0x00000000fab00000,0x00000000fab00000,0x00000000fd580000)
 ParOldGen       total 699392K, used 0K [0x00000000c0000000, 0x00000000eab00000, 0x00000000eab00000)
  object space 699392K, 0% used [0x00000000c0000000,0x00000000c0000000,0x00000000eab00000)
 Metaspace       used 3081K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 335K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 0

物理上,JVM 内存由 PSYoungGen(total 305664K)和 ParOldGen(total 699392K)组成。

305664 K + 699392 K = 1,005,056 K = 981.5 M
(-Xmx)maxMemory = 1029177344(字节)。981.5 MB
(-Xmx)totalMemory = 1029177344(字节)。981.5 MB

Metaspace 是逻辑上的。


OOM。堆内存溢出。

Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

模拟演示堆内存溢出

将 JVM 内存调整为 10M。

-Xms10m -Xmx10m -XX:+PrintGCDetails

测试代码。

package com.geek;

import java.util.Random;

public class OOMTest {

    public static void main(String[] args) {

        String str = "geek。";

        while (true) {
            str += str + new Random().nextInt(77777777) + new Random().nextInt(999999999);
        }
    }
}

GC … GC … GC … FULLGC … GC … FULLGC … GC … GC … FULLGC …
Exception in thread “main” java.lang.OutOfMemoryError: Java heap space

[GC (Allocation Failure) [PSYoungGen: 2013K->507K(2560K)] 2013K->627K(9728K), 0.0030688 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1953K->511K(2560K)] 2073K->916K(9728K), 0.0024636 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1958K->496K(2560K)] 2362K->1652K(9728K), 0.0014568 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 1943K->144K(2560K)] 4506K->3411K(9728K), 0.0035471 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 887K->144K(2560K)] 5563K->4819K(9728K), 0.0014242 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 144K->144K(1536K)] 4819K->4819K(8704K), 0.0016892 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 144K->0K(1536K)] [ParOldGen: 4675K->2470K(7168K)] 4819K->2470K(8704K), [Metaspace: 3035K->3035K(1056768K)], 0.0117442 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 40K->32K(2048K)] 6735K->6726K(9216K), 0.0008385 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Ergonomics) [PSYoungGen: 32K->0K(2048K)] [ParOldGen: 6694K->1766K(7168K)] 6726K->1766K(9216K), [Metaspace: 3035K->3035K(1056768K)], 0.0100738 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 19K->0K(2048K)] 6010K->5990K(9216K), 0.0006650 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 5990K->5990K(9216K), 0.0005609 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 5990K->4582K(7168K)] 5990K->4582K(9216K), [Metaspace: 3035K->3035K(1056768K)], 0.0038578 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] 4582K->4582K(9216K), 0.0034333 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2048K)] [ParOldGen: 4582K->4563K(7168K)] 4582K->4563K(9216K), [Metaspace: 3035K->3035K(1056768K)], 0.0075858 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3332)
	at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
	at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
	at java.lang.StringBuilder.append(StringBuilder.java:208)
	at com.geek.OOMTest.main(OOMTest.java:12)
Heap
 PSYoungGen      total 2048K, used 60K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 1024K, 5% used [0x00000000ffd00000,0x00000000ffd0f248,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 7168K, used 4563K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 63% used [0x00000000ff600000,0x00000000ffa74da0,0x00000000ffd00000)
 Metaspace       used 3067K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 334K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 1
package com.geek;

public class OOMTest {

    public static void main(String[] args) {

/*        String str = "geek。";

        while (true) {
            str += str + new Random().nextInt(77777777) + new Random().nextInt(999999999);
        }*/

        byte[] bytes = new byte[30 * 1024 * 1024];
    }
}

[GC (Allocation Failure) [PSYoungGen: 1257K->496K(2560K)] 1257K->504K(9728K), 0.0023745 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 496K->368K(2560K)] 504K->376K(9728K), 0.0052104 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 368K->0K(2560K)] [ParOldGen: 8K->336K(7168K)] 376K->336K(9728K), [Metaspace: 2993K->2993K(1056768K)], 0.0085719 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 336K->336K(9728K), 0.0025985 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 336K->318K(7168K)] 336K->318K(9728K), [Metaspace: 2993K->2993K(1056768K)], 0.0100414 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.geek.OOMTest.main(OOMTest.java:13)
Heap
 PSYoungGen      total 2560K, used 145K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)
  eden space 2048K, 7% used [0x00000000ffd00000,0x00000000ffd247a0,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 7168K, used 318K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)
  object space 7168K, 4% used [0x00000000ff600000,0x00000000ff64fb70,0x00000000ffd00000)
 Metaspace       used 3051K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 333K, capacity 388K, committed 512K, reserved 1048576K

Process finished with exit code 1

GC 收集日志信息。
[GC (Allocation Failure)  // 分配失败。

[PSYoungGen: 1257K->496K(2560K)] 1257K->504K(9728K), 0.0023745 secs] 

[Times: user=0.01 sys=0.00, real=0.00 secs] 

在这里插入图片描述

在这里插入图片描述


GC 是什么(分代收集算法)。

次数上频繁收集 Young 区。
次数上较少收集 Old 区。
基本不动元空间。

GC 四大算法。
总体概述 ~ Minor GC 和 Full GC 的区别。
  • Minor GC(普通 GC)。

只针对新生代区域的 GC,指发生在新生代的垃圾收集动作,因为大多数 Java 对象存货率都不高,所以 Minor GC 非常频繁,一般回收速度也比较快。

  • Major GC or Full GC(全局 GC)。

指发生在老年代的垃圾收集动作,出现了 Major GC,经常会伴随至少一次的 Minor GC(但并不是绝对 )。Major GC 的速度一般要比 Minor GC 慢 10 倍以上。


引用计数法。

应用:微软的 COM / ActionScript 3 / Python。
JVM 的实现基本不用。

缺点。

  • 每次对对象赋值时都要维护引用计数器,且计数器本身也有一定的消耗。
  • 较难处理循环引用。
package com.geek.gc;

public class RefCountGC {

    Object instance = null;
    private byte[] bigSize = new byte[2 * 1024 * 1024];// 这个成员属性的唯一作用就是占用一点内存。

    public static void main(String[] args) {
        RefCountGC objectA = new RefCountGC();
        RefCountGC objectB = new RefCountGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
        objectA = null;
        objectB = null;

        System.gc();// 手动挡,手动唤起 GC。
        // 并不是立刻执行。
        // 开发中一般不要使用。
    }
}


补充。(HelloWorld 有几个线程?——> 2 个。main 线程和 GC)。
public class HelloWorld {
	
	public static void main(String[] args) {

		System.out.println("Hello World,!");
	}
}

复制算法(Copying)。

“把伊甸园区的和 From 区的对象拷贝,到 To 区”。此时用的就是 Copying 算法。

年轻代中使用的是 Minor GC —> 复制算法。

-XX:MaxTenuringThreshold ——> 设置对象在新生代中存活的次数。
// java 8 不能超过 15。

不想自己画图了,参考:https://www.jianshu.com/p/9e6841a895b4

优点。

无碎片。

缺点。

  • 浪费了一半的内存 ——> 致命。
  • 如果对象的存活率很高,(稍微极端一点,假设 100% 存活),那么所有的对象都要复制一遍,并将所有的引用地址重置一遍,复制这一工作所花费的时间,在对象存活率达到一定程度时,将会变得不可忽略。——> 复制算法要想使用,最起码对象的存活率要非常低才行,more important, 我们必须要克服 50% 的内存浪费。

标记清除(Mark-Sweep)。

复制算法(Copying)[浪费空间 ——> ] 标记清除(Mark-Sweep)。

老年代一般是由标记清除或者是标记清除和标记整理的混合实现。

算法分成标记清除两个阶段。

先标记出要回收的对象,然后统一回收这些对象。

  • 优点。

不需要额外空间。

  • 缺点。

两次扫描,耗时严重。
会产生内存碎片。


标记压缩(Mark-Compact)。

解决碎片问题。

标记清除 + 整理。

慢工出细活。(耗时)。

  • 优点。

没有内存碎片,可以利用 bump-the-pointer。

  • 缺点。

需要移动对象的成本。

引申:标记 + 清除 + 压缩。(Mark-Sweep-compact)。

Mark-Sweep 和 Mark-Compact 的结合。
和 Mark-Sweep 一样,当进行多次 GC 后才 Compact。

没有最好的方法,分代收集算法。——> 根据不同代的特性选择。
内存效率:

复制算法 > 标记清除算法 > 标记整理算法。(简单对比时间复杂度,实际情况不一定)。

内存整齐度:

复制算法 = 标记清除算法 > 标记整理算法

内存利用率:

标记清除算法 = 标记整理算法 > 复制算法


JMM。Java memory model.

Java 内存模型。

volatile 是 Java 虚拟机提供的轻量级的同步机制。(低配版 sychronized)。

保证可见性。
保证原子性。
禁止指令重排。

  • JMM.

JMM (Java 内存模型,Java Memory Model,简称 JMM)本身是一种抽象的概念并不真实存在,ta 描述的是一组规则或规范,通过这组规范定义了程序中各个变量包括实例字段,静态字段和构成数组对象的元素)的访问方式。

由于 JVM 运行程序的实体是线程,而每个线程创建对 JVM 都会为其创建一个工作内存(有些地方称其为栈空间)。工作内存是每个线程的私有数据区域,而 Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程堆变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存中拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如图。

在这里插入图片描述

  • 可见性。

A B 两线程。有一瓶水,B 把水换成了可乐,有一种机制可以让 A 知道”水已经被换成了可乐“。

  • 原子性。

事务要么一起成功,要么一起失败。
A 给 B 转账 100,A 账户已经 -100,中途出现 bug,B 账户没有 +100,那么 A 账户也不会 -100。

  • VolatileDemo 代码演示可见性 + 原子性。
package com.geek.JMMDemo;


class MyNumber {
    int number = 10;

    public void addto1024() {
        this.number = 1024;
    }
}

/**
 * JMM ——> 可见性(通知机制)。
 */
public class JMMDemo {

    public static void main(String[] args) {

        MyNumber myNumber = new MyNumber();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "~ ~ ~ comes in.");
            // 暂停一会儿线程。
            try {
                Thread.sleep(3000);

                myNumber.addto1024();// 将 10 修改为 1024。
                System.out.println(Thread.currentThread().getName() + "\tupdate number, number value: " + myNumber.number);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "A").start();
        // 产生共享变量 number = 10。

        // main 线程直接运行至此。
        while (myNumber.number == 10) {
            // 需要有一种机制告诉 main 线程,number 已经修改为 1024,跳出 while。

        }

        System.out.println(Thread.currentThread().getName() + "\t mission is over");
    }
}

~~~

A~ ~ ~ comes in.
A	update number, number value: 1024

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

// 此时程序永不会结束。
// A 线程已经把 number 值改为 1024。(A	update number, number value: 1024)
// 但 main 线程并不知道。

给 int number = 10; 加上关键字 volatile
volatile 使变量天然对其他线程具有可见性。

volatile int number = 10;

A~ ~ ~ comes in.
A	update number, number value: 1024
main	 mission is over

Process finished with exit code 0

  • 有序性。

按照代码的顺序执行。


构造方法、静态代码块、静态方法。

package com.geek;


class CodeGeek {
    static {
        System.out.println("code 的静态代码块 333。");
    }

    {
        System.out.println("code 的构造方法 222。");
    }

    public CodeGeek() {
        System.out.println("code 的构造方法 111。");
    }
}


public class CodeBlockDemo {

    static {
        System.out.println("CodeBlockDemo 的静态代码块 555。");
    }

    {
        System.out.println("CodeBlockDemo 的构造块 444。");
    }

    public CodeBlockDemo() {
        System.out.println("CodeBlockDemo 的构造方法 666。");
    }

    public static void main(String[] args) {
        System.out.println("~ ~ ~ ~ ~ ~ ~ CodeBlockDemo 的 main() 方法 777。");

        new CodeGeek();
        System.out.println("~ ~ ~ ~ ~ ~ ~");
        new CodeGeek();
        System.out.println("~ ~ ~ ~ ~ ~ ~");
        new CodeBlockDemo();
    }

}

CodeBlockDemo 的静态代码块 555。
~ ~ ~ ~ ~ ~ ~ CodeBlockDemo 的 main() 方法 777。
code 的静态代码块 333。
code 的构造方法 222。
code 的构造方法 111。
~ ~ ~ ~ ~ ~ ~
code 的构造方法 222。
code 的构造方法 111。
~ ~ ~ ~ ~ ~ ~
CodeBlockDemo 的构造块 444。
CodeBlockDemo 的构造方法 666。

静态块 > 构造块 > 构造方法。
发布了47 篇原创文章 · 获赞 1 · 访问量 1180

猜你喜欢

转载自blog.csdn.net/lyfGeek/article/details/104513951