JVM

Java虚拟机

谈谈你对Java的理解

  • 平台无关性
  • GC
  • 语言特性
  • 面向对象
  • 类库
  • 异常处理

Java的平台无关性

  • (javap -c 对代码进行反编译,生成class文件里面的内容)

  • Java源代码首先被编译为字节码(.class),再由不同平台的JVM进行解析,Java语言在不同的平台上运行时不需要进行重新编译,Java虚拟机在执行字节码的时候,把字节码转换成具体平台上的机器指令。

为什么JVM不直接将源码解析为机器码:

  • 准备工作:每次工作都需要语法等检查
  • 兼容性:也可以将其他语言解析成字节码进行运行,兼容性强

JVM如何加载.class文件

Java虚拟机(在内存中)

  • Class Loader:依据特定格式,加载class文件到内存
  • Execution Engine(执行引擎):对命令进行解析
  • Native Interface(本地库接口):融合不同开发语言的原生库为Java所用
  • Runtime Data Area(运行时数据区):JVM内存空间结构模型

注:运行时常量池在JDK1.7由方法去转移进堆中。

JVM内存图

图片来源:《深入理解Java虚拟机(第二版)》

Java内存模型(Java Memory Model)(重要)

  • 程序计数器
    • 线程私有
    • 当前线程所执行的字节码行号指示器
    • 改变计数器的值来选取下一条需要执行的字节码指令
    • 如果执行Native方法,计数器的值为Undefined
    • 不会发生内存泄露
  • Java虚拟机栈
    • 线程私有
    • Java方法执行的内存模型
    • 包含多个栈帧
      • 局部变量表:用于存放方法参数和方法内部定义的局部变量
      • 操作数栈:入栈、出栈、复制、交换、产生消费变量
      • 动态链接
      • 返回地址
  • 本地方法栈
    • 线程私有
    • 与虚拟机栈类似,主要作用于native方法
  • 元空间(MetaSpace)与永久代(PermGen)区别——均是方法区的实现
    • 元空间使用本地内存,永久代使用jvm内存
    • MetaSapce相比PermGen(已被去掉)的优势
      • 字符串常量池存在永久代中,容易出现性能问题和溢出
      • 类和方法的信息大小难以确定,给永久代的大小指定带来困难
      • 永久代会为GC带来不必要的复杂性
    • 方法区会记录类星系(方法表等)
  • Java堆(Heap)
    • 存放对象实例的分配区域
    • GC管理的主要区域

JVM三大性能调优参数 -Xss -Xmx -Xms含义

  • -Xss:规定了每个线程虚拟机栈(堆栈)的大小
  • -Xms:堆的初始值
  • -Xmx:堆能达到的最大值

Java内存模型中堆和栈的区别——内存分配策略

  • 静态存储:编译时确定每个数据目标在运行时的存储空间需求
  • 栈式存储:数据区需要在编译时位置,运行时模块入口前确定数据区大小
  • 堆式存储:编译时或运行时模块入口都无法确定,动态分配
  • 联系:引用对象、数组实,栈里定义变量保存堆中目标的首地址(保证栈式存储在运行时能确定大小,堆式存储可以动态分配内存)
  • 区别:
    • 管理方式:栈自动释放,堆需要GC
    • 空间大小:栈比堆小
    • 碎片相关:栈产生的碎片远小于堆
    • 分配方式:栈支持静态和动态分配,而堆仅支持动态分配
    • 效率:栈的效率比堆高

内存实例代码

内存实例2

图片来源:慕课网

不同JDK版本之间intern()方法的区别——JDK6 vs JDK6+

  • JDK6:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,将此字符串对象添加到字符串常量池中,并且返回该字符串对象的引用。
  • JDK6+:当调用intern方法时,如果字符串常量池先前已创建出该字符串对象,则返回池中的该字符串的引用。否则,如果该字符串对象已经存在于Java堆中,则将堆中对此对象的引用添加到字符串常量池中,并且返回该引用;如果堆中不存在,则在池中创建该字符串并返回其引用。

类的加载方式

  • 隐式加载:new
  • 显示加载:loadClass,forName等(需要手动调用newInstance()方法)

类的装载过程

  • 加载:通过ClassLoader加载class文件字节码,生成Class对象
  • 链接:
    • 校验:检验加载的class的正确性和安全性
    • 准备:为类变量分配存储空间并设置类变量(static变量)初始值(各种形式的0)
    • 解析:JVM将常量池内的符号引用转换为直接引用
  • 初始化:执行类变量赋值和静态代码块

.class、loadClass和forName的区别

  • .class操作不进行加载
  • ClassLoader.loadClass得到的class是还没有链接的(只完成了加载
  • Class.forName得到的class是已经完成初始化

Java反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java的反射机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Robot {
private String name;
public void sayHi(String helloSentence) {
System.out.println(helloSentence + " " + name);
}
private String throwHello(String tag) {
return "Hello " + tag;
}
}
public vlass ReflectSample {
public static void main(String args[]) throws ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException, NoSuchFieldException {
Class rc = Class.forName("com.reflect.Robot");
Robot r = (Robot) rc.newInstance();
System.out.println("Class name is " + rc.getName());
// r.sayHi("bob"); // 这种情况无法证明是反射
Method getHello = rc.getDeclaredMethod("throwHello", String.class); // 能获取该类的所有方法,但是不能获取继承/实现接口的方法
getHello.setAccessible(true); // private -> true;
Object str = getHello.invoke(r, "Bob");
System.out.println("getHello result is " + str);
Method sayHi = rc.getMethod("sayHi", String.class); // 能获取该类的所有public方法,包括继承类的公用方法,接口方法。
sayHi.invoke(r, "Welcome"); // syso:Welcome null;
Field name = rc.getDeclaredField("name"); // "name"为成员变量名
name.setAccessible(true);
name.set(r, "Alice");
sayHi.invoke(r, "Welcome"); // syso:Welcome Alice;
}
}

类从编译到执行的过程

  • 编译器将.java源文件编译为.class字节码文件
  • ClassLoader将字节码转换为JVM中的Class对象
  • JVM再利用Class\<T>对象实例化为T对象

ClassLoader

  • ClassLoader主要工作在Class装载的加载阶段,其主要作用是从系统外部获得Class二进制数据流。
  • 它是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过将Class文件里的二进制数据流装载进系统,然后交给Java虚拟机进行连接、初始化等操作。

ClassLoader种类

  • BootStrapClassLoader(启动类加载器):C++编写,加载核心库java.*
  • ExtClassLoader(扩展类加载器):Java编写,加载扩展库javax.*
  • AppClassLoader(应用程序类加载器):Java编写,加载程序所在目录
  • UserClassLoader(自定义类加载器):Java编写,定制化加载

为什么要使用双亲委派模型

双亲实际上是一种对parents的翻译错误,实际上并没有“双”这个概念!

对于任意一个类,都需要有加载它的类加载器(ClassLoader)和这个类本身来一同确立其在Java虚拟机中的唯一性。

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

双亲委派模型

图片来源:【JVM】浅谈双亲委派和破坏双亲委派

一个符合规范的类加载器,应当仅覆写ClassLoader#findClass(),以支持自定义的类加载方式。

使用双亲委派模型的意义

  • 避免多份同样字节码的加载

  • Java类随着加载它的类加载器一起具备了一种带有优先级的层次关系。比如,Java中的Object类,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。如果不采用双亲委派模型,那么由各个类加载器自己取加载的话,那么系统中会存在多种不同的Object类。容易引发安全问题。

    作者:shuixingge

    链接:https://www.jianshu.com/p/6dbe64a333e7

    来源:简书

JVM调优

查找占用内存最大的进程:ps -ef | grep java 或者top 再按m获得占用内存最大的进程的pid

jmap -histo:live 25085 | head -20 查看前20个占用最大的对象

  • jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。-l : 输出主类全名或jar路径

  • jstat:JVM statistics Monitoring,是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

  • jmap:Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    >jmap -heap 14732
    Attaching to process ID 14732, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 25.151-b12

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

    Heap Configuration:
    MinHeapFreeRatio = 0
    MaxHeapFreeRatio = 100
    MaxHeapSize = 2124414976 (2026.0MB)
    NewSize = 44564480 (42.5MB)
    MaxNewSize = 707788800 (675.0MB)
    OldSize = 89653248 (85.5MB)
    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 = 34078720 (32.5MB)
    used = 26630104 (25.396446228027344MB)
    free = 7448616 (7.103553771972656MB)
    78.14291147085336% used
    From Space:
    capacity = 5242880 (5.0MB)
    used = 0 (0.0MB)
    free = 5242880 (5.0MB)
    0.0% used
    To Space:
    capacity = 5242880 (5.0MB)
    used = 0 (0.0MB)
    free = 5242880 (5.0MB)
    0.0% used
    PS Old Generation
    capacity = 89653248 (85.5MB)
    used = 0 (0.0MB)
    free = 89653248 (85.5MB)
    0.0% used

    5611 interned Strings occupying 466504 bytes.
    • jmap -histo | more:打印堆的对象统计,包括对象数、内存大小等等 (因为在dump:live前会进行full gc,如果带上live则只统计活对象,因此不加live的堆大小要大于加live堆的大小 )
Ty.Wings wechat
欢迎您订阅我的公众号,并在GitHub上为我Star!