1、JVM概述

image.png
这里可以先举一个例子来了解JVM的实现过程

image.png
image.png
操作实现入栈出栈
image.png
通过 为1入栈 然后局部变量定义 然后1出栈赋值个a 操作数栈会将运算和其他操作实现

2、虚拟机栈

2.1、定义

2.2、栈内存溢出

2.3、线程运行诊断

1、CPU占用过高问题

定位

  • 用top定位哪个进程对cpu的占用过高
  • ps H -eo pid,tid,%cpu|grep 进程id (用ps命令进一步定位哪个线程引起的cpu占用过高)
  • jstack 进程id

    • 可以根据线程id找到有问题的线程,进一步定位到问题代码的源码行数。

    • 3、类加载器

      3.1、作用

      加载Class文件~
      每个编写的”.java”拓展名类文件都存储着需要执行的程序逻辑,这些”.java”文件经过Java编译器编译成拓展名为”.class”的文件,”.class”文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的”.class”文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:
      image.png
  • 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

  • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。

  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

image.png1.虚拟机自带的加载器
2.启动类(根)加载器
3.扩展类加载器
4.应用程序(系统类)加载器

  1. public class Car {
  2. public int age;
  3. public static void main(String[] args) {
  4. // 通过反射拿到Car类对象
  5. Car car1 = new Car();
  6. Car car2 = new Car();
  7. Car car3 = new Car();
  8. car1.age = 1;
  9. car2.age = 2;
  10. car3.age = 3;
  11. System.out.println(car1.hashCode());
  12. System.out.println(car2.hashCode());
  13. System.out.println(car3.hashCode());
  14. Class<? extends Car> aClass1 = car1.getClass();
  15. Class<? extends Car> aClass2 = car2.getClass();
  16. Class<? extends Car> aClass3 = car3.getClass();
  17. // Class 都是Car 所以是相等的
  18. System.out.println(aClass1.hashCode());
  19. System.out.println(aClass2.hashCode());
  20. System.out.println(aClass3.hashCode());
  21. }
  22. }

image.png

3.2、启动(Bootstrap)类加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

3.3、扩展(Extension)类加载器

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
源码加载器

  1. //ExtClassLoader类中获取路径的代码
  2. private static File[] getExtDirs() {
  3. //加载<JAVA_HOME>/lib/ext目录中的类库
  4. String s = System.getProperty("java.ext.dirs");
  5. File[] dirs;
  6. if (s != null) {
  7. StringTokenizer st =
  8. new StringTokenizer(s, File.pathSeparator);
  9. int count = st.countTokens();
  10. dirs = new File[count];
  11. for (int i = 0; i < count; i++) {
  12. dirs[i] = new File(st.nextToken());
  13. }
  14. } else {
  15. dirs = new File[0];
  16. }
  17. return dirs;
  18. }

3.4、系统(System)类加载器

也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
  在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

3.5、双亲委派模式

双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
image.png双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

3.5.1、双亲委派优势

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

3.6、类加载器之间关系

我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点

  • 启动类加载器:由C++实现,没有父类。
  • 拓展类加载器(ExtClassLoader):由Java语言实现,父类加载器为null
  • 系统类加载器(AppClassLoader):由Java语言实现,父类加载器为ExtClassLoader
  • 自定义类加载器:父类加载器肯定为AppClassLoader。

下面我们通过程序来验证上述阐述的观点

  1. /**
  2. * Created by zejian on 2017/6/18.
  3. * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
  4. */
  5. //自定义ClassLoader,完整代码稍后分析
  6. class FileClassLoader extends ClassLoader{
  7. private String rootDir;
  8. public FileClassLoader(String rootDir) {
  9. this.rootDir = rootDir;
  10. }
  11. // 编写获取类的字节码并创建class对象的逻辑
  12. @Override
  13. protected Class<?> findClass(String name) throws ClassNotFoundException {
  14. //...省略逻辑代码
  15. }
  16. //编写读取字节流的方法
  17. private byte[] getClassData(String className) {
  18. // 读取类文件的字节
  19. //省略代码....
  20. }
  21. }
  22. public class ClassLoaderTest {
  23. public static void main(String[] args) throws ClassNotFoundException {
  24. FileClassLoader loader1 = new FileClassLoader(rootDir);
  25. System.out.println("自定义类加载器的父加载器: "+loader1.getParent());
  26. System.out.println("系统默认的AppClassLoader: "+ClassLoader.getSystemClassLoader());
  27. System.out.println("AppClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent());
  28. System.out.println("ExtClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
  29. /**
  30. 输出结果:
  31. 自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@29453f44
  32. 系统默认的AppClassLoader: sun.misc.Launcher$AppClassLoader@29453f44
  33. AppClassLoader的父类加载器: sun.misc.Launcher$ExtClassLoader@6f94fa3e
  34. ExtClassLoader的父类加载器: null
  35. */
  36. }
  37. }

而ExtClassLoader没有父类加载器。如果我们实现自己的类加载器,它的父加载器都只会是AppClassLoader。这里我们不妨看看Lancher的构造器源码

  1. public Launcher() {
  2. // 首先创建拓展类加载器
  3. ClassLoader extcl;
  4. try {
  5. extcl = ExtClassLoader.getExtClassLoader();
  6. } catch (IOException e) {
  7. throw new InternalError(
  8. "Could not create extension class loader");
  9. }
  10. // Now create the class loader to use to launch the application
  11. try {
  12. //再创建AppClassLoader并把extcl作为父加载器传递给AppClassLoader
  13. loader = AppClassLoader.getAppClassLoader(extcl);
  14. } catch (IOException e) {
  15. throw new InternalError(
  16. "Could not create application class loader");
  17. }
  18. //设置线程上下文类加载器,稍后分析
  19. Thread.currentThread().setContextClassLoader(loader);
  20. //省略其他没必要的代码......
  21. }
  22. }

3.7、沙箱安全机制

沙箱机制就是将 Java 代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问,通过这样的措施来保证对代码的有效隔离,防止对本地系统造成破坏。组成沙箱的基本组件是字节码校验器(确保Java类文件遵循Java语言规范)和类装载器(通过双亲委派机制保证安全)

4、程序计数器

4.1、定义

程序计数器也被称为PC寄存器(Program Counter Register)。用于标识当线程当前执行的代码,如果线程执行的是非native方法,则PC中保存的是当前需要执行的指令的地址;如果线程执行的是native方法,则PC中的值是undefined。

4.2、作用

image.png
记录下一条JVM指令的执行地址,来实现执行顺序。

4.3、特点

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

    5、Native关键字

    Java是一个跨平台的语言,既然是跨了平台,所付出的代价就是牺牲一些对底层的控制,而Java要实现对底层的控制,就需要借助一些其他语言的帮助,这个就是native的作用。
    Native method就是一个java调用非java代码的接口,例如Thread.start()方法为本地方法,凡是带了native关键字,说明Java权限已经达不到了,需要调用用C语言的库。具体体现为native方法会进入本地方法栈,会调用本地方法接口(JNI),然后进入本地方法库 ```java /**

    • native:凡是带了native关键字的,说明java的作用范围达不到了,会去调用底层c语言的库。
    • 会进入本地方法栈,调用本地方法的本地接口 JNI
    • JNI作用:扩展java的使用,融合不同的编程语言为Java所用
    • 它在内存区域中专门开辟了一块标记区域:Native Method Stack,登记native方法
    • 在最终执行的时候,加载本地方法库中的方法通过JNI *
    • 方法区:方法区是被所有线程共享,所有字段和方法字节码,以及一些特殊方法,如构造函数,
    • 接口代码也在此定义,简单说,所有定义的方法的信息都保存在该区域,此区域属于共享区间 *
    • 静态变量、常量、类信息(构造方法,接口定义)、运行时的常量池存在方法区中,但是实例变量
    • 存在堆内存中,和方法区无关。 static final Class 常量池 */ public class Native {

      private int a; private String name = “abc”;

      public static void main(String[] args) {

      1. Native aNative = new Native();
      2. new Thread(() -> {
      3. }, "my thread name").start();

      }

      private native void start0();//调用底层c语言的库

}

  1. <a name="xUdMK"></a>
  2. #### 5.2、JNI:Java Native Interface
  3. 在介绍 native 之前,我们先了解什么是 JNI。<br />一般情况下,我们完全可以使用 Java 语言编写程序,但某些情况下,Java 可能会不满足应用程序的需求,或者是不能更好的满足需求,比如:<br />①、标准的 Java 类库不支持应用程序平台所需的平台相关功能。<br />②、我们已经用另一种语言编写了一个类库,如何用Java代码调用?<br />③、某些运行次数特别多的方法代码,为了加快性能,我们需要用更接近硬件的语言(比如汇编)编写。 上面这三种需求,其实说到底就是如何用 Java 代码调用不同语言编写的代码。那么 JNI 应运而生了。<br />从Java 1.1开始,Java Native Interface (JNI)标准就成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计 的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。使用java与本地已编译的代码交互,通常会丧失平台可移植性。但是,有些情况下这样做是可以接受的,甚至是必须的,比如,使用一些旧的库,与硬件、操作系统进行交互,或者为了提高程序的性能。JNI标准至少保证本地代码能工作在任何Java 虚拟机实现下。
  4. 通过 JNI,我们就可以通过 Java 程序(代码)调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能;同时其他技术和系统也可以通过 JNI 提供的相应原生接口开调用 Java 应用系统内部实现的功能。<br />在windows系统上,一般可执行的应用程序都是基于 native 的PE结构,windows上的 JVM 也是基于native结构实现的。Java应用体系都是构建于 JVM 之上。
  5. <a name="A6A3J"></a>
  6. ### 6、方法区
  7. <a name="Kgbc4"></a>
  8. #### 6.1、方法区概述
  9. 主要存放字节码的相关信息,例如常量、静态变量、类元信息(即类的组成信息),如果静态变量是类类型,则保存的是该对象在堆内存中的地址引用。方法区被所有线程共享。**逻辑上存在,内存上不存在**<br />下图为一个对象的完整组成部分<br />方法区是一个概念,它包括常量池+ClassLoader+Class还有串常量(StringTable)。<br />在逻辑上,方法区算是堆内存的一部分使用的是堆的永久代的内存,但是在实际实现上,不一定用堆的内存。<br />而在1.8之后增加了元空间这种概念,将方法区的实现从堆内存改到了操作系统内存。<br />它的结构如下:<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25484710/1654350050725-4f879a50-221d-44a8-b87b-590219f629b3.png#clientId=ua39820b3-c8b2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=535&id=uf0306edf&margin=%5Bobject%20Object%5D&name=image.png&originHeight=802&originWidth=1136&originalType=binary&ratio=1&rotation=0&showTitle=false&size=250360&status=done&style=none&taskId=u5e637316-9480-459e-a833-ec16b42f2b6&title=&width=757.3333333333334)<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25484710/1648460087764-3f22e984-7043-453e-98ca-f3cab1756b96.png#clientId=ufddf3e21-3d62-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=190&id=u0964f10a&margin=%5Bobject%20Object%5D&name=image.png&originHeight=379&originWidth=1512&originalType=binary&ratio=1&rotation=0&showTitle=false&size=276392&status=done&style=none&taskId=u489750d4-a8b2-49e3-9c92-cd51c05ac52&title=&width=756)<br />由上图可知一个对象不仅有实例数据等,还有对象头,对象头有一个类型指针,它指向了类的元数据地址,即在堆中创建一个对象时,它也同样有一个指针指向方法区所保存的类元信息<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25484710/1648460160906-542145c7-8dae-46bf-a8af-b132896424fd.png#clientId=ufddf3e21-3d62-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=303&id=ua57c3e87&margin=%5Bobject%20Object%5D&name=image.png&originHeight=514&originWidth=1270&originalType=binary&ratio=1&rotation=0&showTitle=false&size=142784&status=done&style=none&taskId=ubc1214ec-67f3-4cbb-8f32-528d1deeb2e&title=&width=748)<br />大致存放:常量+静态变量+类信息<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/25484710/1650964155698-9f14980d-8ebf-4e0f-9856-885d0f2926e4.png#clientId=ueac345d9-77eb-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=389&id=ua6b9c90e&margin=%5Bobject%20Object%5D&name=image.png&originHeight=584&originWidth=1454&originalType=binary&ratio=1&rotation=0&showTitle=false&size=507998&status=done&style=none&taskId=uf6587a2e-9392-4a50-86cf-f26bb599057&title=&width=969.3333333333334)
  10. <a name="WAA7D"></a>
  11. #### 6.2、运行时常量池
  12. 1. 常量池就是一张表虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息。
  13. 1. 运行时常量池,常量池是*.class 文件中的,当该类被加载,他的常量池信息就会放到运行时常量池毛病将里面的符号改为真实地址。
  14. <a name="k70h8"></a>
  15. #### 6.3、内存溢出
  16. - 1.8以前会导致**永久代**内存溢出
  17. - 1.8以后会导致**元空间**内存溢出
  18. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25484710/1654350115847-4a34428b-a8c0-498a-9dfa-4f68a094ff44.png#clientId=ua39820b3-c8b2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=437&id=u6f17182b&margin=%5Bobject%20Object%5D&name=image.png&originHeight=656&originWidth=1388&originalType=binary&ratio=1&rotation=0&showTitle=false&size=320395&status=done&style=none&taskId=u66a40533-9874-466a-a363-25def736993&title=&width=925.3333333333334)
  19. <a name="Mtg2m"></a>
  20. #### 6.4、常量池
  21. 常量池在java用于保存在编译期已确定的,已编译的class文件中的一份数据。它包括了关于类,方法,接口等中的常量,也包括[字符串](https://so.csdn.net/so/search?q=%E5%AD%97%E7%AC%A6%E4%B8%B2&spm=1001.2101.3001.7020)常量,分为常量池和运行时常量池。
  22. <a name="ikf8k"></a>
  23. ##### 6.4.1、查看常量池的方法
  24. 通过反编译来查看字节码文件<br />获得对应类的.class文件
  25. - 在JDK对应的bin目录下运行cmd,**也可以在IDEA控制台输入**
  26. - ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25484710/1654350221193-d12f75a6-2411-46b3-bcc3-f9fe3b44f7da.png#clientId=ua39820b3-c8b2-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=69&id=u4f8225e8&margin=%5Bobject%20Object%5D&name=image.png&originHeight=104&originWidth=608&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11556&status=done&style=none&taskId=u7409724c-0964-41f9-887d-73627e0316e&title=&width=405.3333333333333)
  27. - 输入 **javac 对应类的绝对路径**(编译一次代码,可以用idea运行一次)
  28. ```java
  29. F:\JAVA\JDK8.0\bin>javac F:\Thread_study\src\com\nyima\JVM\day01\Main.javaCopy

输入完成后,对应的目录会出现.class文件

  • 控制台输入javap -v 类的绝对路径

    1. javap -v F:\Thread_study\src\com\nyima\JVM\day01\Main.classCopy
  • 常量池

image.png
image.png

  • 虚拟机中执行编译的方法(框内的是真正编译执行的内容,#号的内容需要在常量池中查找

image.png

6.4.2、运行时常量池和常量池区别
  • 常量池
    • 就是一张表(如上图中的constant pool),虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量信息
  • 运行时常量池
    • 常量池是.class文件中的,当该类被加载以后,它的常量池信息就会放入运行时常量池,并把里面的*符号地址变为真实地址

6.4.3、串池(StringTable)

常量池中的字符串仅是符号,只有在被用到时才会转化为对象储存到堆里或者串池里。
串池是什么?
image.png
image.png
串池指的是串池(StringTable),如上图所示是方法区的一员,1.6之前串池在常量池里储存在逻辑上的堆内存的永久代里。因为永久代需要重GC清理,所以1.8之后对串池的位置进行了更改,使他物理意义上脱离了大多数方法区成员的位置,不在元空间(本地内存)里。
串池是用来保存String对象的,当常量池的字符被String对象引用时,若串池里无重复的对象,将该对象加入到串池。以后若再遇到相同String对象引用相同的字符串则直接使用串池里的对象。(拼接的时候不用,用的是堆内存)

  1. public class StringTableStudy {
  2. public static void main(String[] args) {
  3. String a = "a";
  4. String b = "b";
  5. String ab = "ab";
  6. }
  7. }

常量池中的信息,都会被加载到运行时常量池中,但这是a b ab 仅是常量池中的符号还没有成为java字符串

  1. 0: ldc #2 // String a
  2. 2: astore_1
  3. 3: ldc #3 // String b
  4. 5: astore_2
  5. 6: ldc #4 // String ab
  6. 8: astore_3
  7. 9: returnCopy

当执行到 ldc #2 时,会把符号 a 变为 “a” 字符串对象,并放入串池中(hashtable结构 不可扩容)
当执行到 ldc #3 时,会把符号 b 变为 “b” 字符串对象,并放入串池中
当执行到 ldc #4 时,会把符号 ab 变为 “ab” 字符串对象,并放入串池中
最终StringTable [“a”, “b”, “ab”]
注意:字符串对象的创建都是懒惰的,只有当运行到那一行字符串且在串池中不存在的时候(如 ldc #2)时,该字符串才会被创建并放入串池中。
串池的存在避免了字符串对象的重复创建。
字符串变量拼接和字符串常量拼接

  • 字符串变量拼接的原理是StringBuilder,拼接后的对象放在堆内存里(拼接之后重新new一个String对象)。
  • 字符串常量拼接的原理是编译器优化,串池里如果有你拼接完的字符串则直接返回,没有则创建一个加入到串池(1.6之前是复制对象,如果当前对象需要串池中,只是复制了一个对象而已,本对象是没有进入池中的,1.8之后是直接引用对象,如果串池中不存在则加入串池,同时返回当前对象,如果存在则返回池中的,当前对象不入池)。

使用拼接字符串变量对象创建字符串的过程

  1. public class StringTableStudy {
  2. public static void main(String[] args) {
  3. String a = "a";
  4. String b = "b";
  5. String ab = "ab";
  6. //拼接字符串对象来创建新的字符串a
  7. String ab2 = a+b;
  8. }
  9. }

反编译后的结果

  1. Code:
  2. stack=2, locals=5, args_size=1
  3. 0: ldc #2 // String a
  4. 2: astore_1
  5. 3: ldc #3 // String b
  6. 5: astore_2
  7. 6: ldc #4 // String ab
  8. 8: astore_3
  9. 9: new #5 // class java/lang/StringBuilder
  10. 12: dup
  11. 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
  12. 16: aload_1
  13. 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  14. 20: aload_2
  15. 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  16. 24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
  17. 27: astore 4
  18. 29: returnCopy
  • 使用拼接字符串常量的方法来创建新的字符串时,因为内容是常量,javac在编译期会进行优化(如果是String x = “a” + “b” JVM默认优化为”ab”字符串),结果已在编译期确定为ab,而创建ab的时候已经在串池中放入了“ab”,所以ab3直接从串池中获取值,所以进行的操作和 ab = “ab” 一致。
  • 使用拼接字符串变量的方法来创建新的字符串时,因为内容是变量,只能在运行期确定它的值,所以需要使用StringBuilder来创建

intern方法

  1. jdk1.8

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

  • 如果串池中没有该字符串对象,则放入成功,返回引用的对象
  • 如果有该字符串对象,则放入失败,返回字符串里有的该对象

无论放入是否成功,都会返回串池中的字符串对象。
注意:此时如果调用intern方法成功,堆内存与串池中的字符串对象是同一个对象;如果失败,则不是同一个对象

  1. jdk1.6

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

  • 如果串池中没有该字符串对象,会将该字符串对象复制一份,再放入到串池中,返回的是复制的对象
  • 如果有该字符串对象,则放入失败,返回串池原有的该字符串的对象

注意:此时无论调用intern方法成功与否,串池中的字符串对象和堆内存中的字符串对象都不是同一个对象
测试

  • 原来串池中存在该字符串

此时,无论1.6还是1.8 x都不等于s
image.png

  • 原来串池中不存在该字符串

1.6 因为是复制一份新的堆的对象,所以和原来的对象s不同
image.png
1.8.因为和原来的对象s相同所以 true
image.png
串池垃圾回收
很多人感觉串池不能垃圾回收,但实际上,串池也是可以进行垃圾回收的。
image.png

image.png
注意:要加上-XX:+PrintStringTableStatistics底下才会显示详细信息。

  1. -Xmx10m 指定堆内存大小
  2. -XX:+PrintStringTableStatistics 打印字符串常量池信息
  3. -XX:+PrintGCDetails
  4. -verbose:gc 打印 gc 的次数,耗费时间等信息

垃圾回收的演示代码:

  1. /**
  2. * 演示 StringTable 垃圾回收
  3. * -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
  4. */
  5. public class Code_05_StringTableTest {
  6. public static void main(String[] args) {
  7. int i = 0;
  8. try {
  9. for(int j = 0; j < 100; j++) { // j = 100, j = 10000
  10. String.valueOf(j).intern();
  11. i++;
  12. }
  13. }catch (Exception e) {
  14. e.printStackTrace();
  15. }finally {
  16. System.out.println(i);
  17. }
  18. }
  19. }

image.png
image.png

6.5、StringTable性能调优
  • 增加桶的个数
  • 尽量不使用堆进行字符串储存

因为StringTable(串池)底层是hashmap,实现了去重的功能,所以它的性能跟桶的个数(链表的节点数息息相关),桶数越多,性能越强,所以,当我们数据量较大的时候,适当增加桶的个数,能有效的提高效率。

  1. -XX:StringTableSize=桶个数(最少设置为 1009 以上)

image.png
image.png
image.png
image.png
同时,用堆内存储存字符串相对于用StringTable储存字符串,因为不能去重,占用的内存资源较大,所以我们尽量用intern()函数将字符串加入到串池。去掉重复的。
堆储存和串池储存做对比
在48万数据存在重复的情况下
image.png
image.png
image.png
image.png
image.png

7、栈

image.png

  1. Java虚拟机栈也是线程私有的,它的生命周期与线程相同(随线程而生,随线程而灭)

  2. 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;

如果虚拟机栈可以动态扩展,如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常;

(当前大部分JVM都可以动态扩展,只不过JVM规范也允许固定长度的虚拟机栈)

  1. Java虚拟机栈描述的是Java方法执行的内存模型:每个方法执行的同时会创建一个栈帧。

对于我们来说,主要关注的stack栈内存,就是虚拟机栈中局部变量表部分。

栈和方法区和常量池的交互
image.png

7.2、栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。
注意:

在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。 因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

image.png
注意:
在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。
执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

7.3、局部变量表

1.局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。
2.局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,

对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址)
注意:
很多人说:基本数据和对象引用存储在栈中。
当然这种说法虽然是正确的,但是很不严谨,只能说这种说法针对的是局部变量。
局部变量存储在局部变量表中,随着线程而生,线程而灭。并且线程间数据不共享。
但是,如果是成员变量,或者定义在方法外对象的引用,它们存储在堆中。
因为在堆中,是线程共享数据的,并且栈帧里的命名就已经清楚的划分了界限 : 局部变量表!

7.4、reference 对象实例应用

我的理解是:一个超链接
一般来说,虚拟机都能从引用中直接或者间接的查找到对象的以下两点 :
a.在Java堆中的数据存放的起始地址索引。
b.所属数据类型在方法区中的存储类型。
例如:我们在创建一个Student对象时的数据存储结构:
image.png

7.5、动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,
持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。
在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。
另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。
这里简单提一下动态连接的概念,后面在详细讲解.

7.6、方法出口

当一个方法开始执行后,只有2种方式可以退出这个方法 :
方法返回指令 : 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。
无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。
一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。
而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

8、HotSpot和堆

8.1、堆概述

一个JVM只有一个堆内存,堆内存大小是可以调节的。
类加载器读取了类文件周,一般会把什么放到堆内存中,类、方法、常量、变量~、保存我们所有引用类型的真实对象;
堆内存中分为三个区域:

  • 新生区(Eden)
  • 养老区()
  • 永久区

image.png

GC垃圾回收主要在伊甸园区(新生区)和养老区~
假如内存满了,OOM,堆内存溢出

8.2、永久区

这个区域常驻内存的,用来存放JDK自身携带的Class对象,Interface元数据,储存的是Java运行时的一些环境或类信息,这个区域不存在垃圾回收,关闭VM虚拟就会释放当前区域的内存
一个启动类加载了大量第三方的jar包,tomcat就部署太多应用,大量动态生成反射类,不断加载。直到内存满!

  • jdk1.6之前 :永久代,常量池在方法区中
  • jdk1.7 :永久代,但是慢慢退化,去永久代,常量池在堆中
  • jdk1.8之后 :无永久代,常量池在元空间中

image.png
image.png
逻辑上存在:物理上不存在!

8.3、堆内存溢出

8.4、堆内存诊断

  1. jps工具
  • 查看当前系统有哪些java进程
  1. jmap工具
  • 查看堆内存占用情况 jmap - heap 进程id
  1. jconsole工具
  • 图形界面的,多功能的检测工具,可以连续检测

自带视图 可视化 jvisualvm

9、GC介绍

9.1、内存图

image.png
image.png
JVM进行GC时,并不是对这三个统一回收,大部分时候都在新生代gc

  • 新生代(Eden Space)
  • 幸存区(to)
  • 老年区(from)

GC两种类型:轻GC(普通GC侧重于新生代)minor gc,重GC(全局GC)full gc

题目:

  • JVM的内存模型和分区~详细到每个分区放什么?
  • 堆里面的分区有哪些?Eden,form,to,老年区,说说他们的特点!
  • GC的算法有哪些?标记清除法,标记压缩,复制算法,引用计数器,怎么用?
  • 轻GC和 重GC分别在上面时候发生?

解答:

9.2、引用计数器算法

对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象的引用计数器的值为0,即表示对象A不能在被使用,可进行回收。
image.png

  • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
  • 缺点:他需要单独的字段存储计数器,这样的做法增加了存储空间的开销。每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。引用计数器还有一个严重的问题,即无法处理循环引用的问题,这是一条致命的缺陷,导致在Java回收的垃圾回收

    9.3、复制算法

    当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址。此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。
    image.png
    周期
    image.png
    image.png

  • 好处:没有内存的碎片

  • 坏处:浪费一个幸存区的空间(To区永远为空)。假设对象100%存活(极端情况)

复制算法最佳使用场景:对象存活度较低的时候;新生区;

9.4 标记算法

它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
在标记阶段,collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
而在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。
image.png

  • 优点:不需要额外的空间!
  • 缺点:两次扫描,严重浪费时间,会产生内存碎片

标记压缩(标记-整理)
再优化
再次扫描,将全部标记排序,向前移动存活的对象。
先标记清除几次然后再进行压缩 标记清除压缩

image.pngimage.png

11、总结:

内存效率:复制算法 > 标记清除算法 > 标记压缩算法(时间复杂度)
内存整齐度:复制算法 = 标记压缩算法 >标记清除算法
内存利用率:标记压缩算法 = 标记清除算法 > 复制算法

年轻代:

  • 存活率低
  • 复制算法!

老年代:

  • 区域大:存活率高
  • 标记清除(内存碎片较少就停止清除进行压缩)+标记压缩混合 实现(JVM多少次清除再进行压缩)

12、JMM:Java Memory Model

Java内存模型

  1. 什么是JMM?

【JMM】(Java Memory Model的缩写)允许编译器和缓存以数据在处理器特定的缓存(或寄存器)和主存之间移动的次序拥有重要的特权,除非程序员使用了volatile或synchronized明确请求了某些可见性的保证。

  1. JMM干嘛的?

作用:缓存一致性协议,用于定义数据读写的规则(遵守规则,找到规则)
用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。
image.pngimage.pngimage.png
解决共享对象可见性这个问题:volilate

  1. JMM该如何学习?

JMM:抽象概念
volilate:关键字 保证遍历的原子性,立刻刷入到主内存中
JMM对这八种指令的使用,制定了如下规则:

  • 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
  • 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
  • 不允许一个线程将没有assign的数据从工作内存同步回主内存
  • 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
  • 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
  • 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
  • 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
  • 对一个变量进行unlock操作之前,必须把此变量同步回主内存

image.png

13、直接内存

13.1、什么是直接内存?

直接内存指的就是Direct Memory,常见于Nio操作,区别于io,在读写操作时有着更高的效率。他实际上不属于jvm,应该算是一种开辟更快的读写内存的机制。
特点:

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