复习-JavaGuide-Jvm

jvm监控和故障处理工具 总结

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

JDK 命令行工具 #

这些命令在 JDK 安装目录下的 bin 目录下:

  • jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;
  • jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
  • jinfo (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息;
  • jmap (Memory Map for Java) : 生成堆转储快照;
  • jhat (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
  • jstack (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。

jps: 查看所有 Java 进程 #

jps(JVM Process Status) 命令类似 UNIX 的 ps 命令。

...

jvm参数

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

本文由 JavaGuide 翻译自 https://www.baeldung.com/jvm-parametersopen in new window,并对文章进行了大量的完善补充。翻译不易,如需转载请注明出处,作者: baeldungopen in new window

概述 #

本篇文章中,将掌握最常用的JVM参数配置。下面提到了一些概念,方法区垃圾回收等。

堆内存相关 #

Java 虚拟机所管理的内存中最大的一块Java 堆所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例几乎 所有的对象实例以及数组都在这里分配内存。
ly-20241212142002639

显式指定堆内存-Xms和-Xmx #

  • 性能相关的最常见实践之一是根据应用程序要求初始化堆内存

  • 如果我们需要指定最小最大堆大小(推荐显示指定大小):

    -Xms<heap size>[unit] 
    -Xmx<heap size>[unit]
    
    • heap size 表示要初始化内存的具体大小。
    • unit 表示要初始化内存的单位。单位为***“ g”*** (GB) 、“ m”(MB)、“ k”(KB)。
  • 举例,为JVM分配最小2GB和最大5GB的堆内存大小

    -Xms2G -Xmx5G
    

显示新生代内存(Young Generation) #

  • 在堆总可用内存配置完成之后,第二大影响因素是为 Young Generation 在堆内存所占的比例。默认情况下,YG 的最小大小为 1310 MB,最大大小为无限制

  • 两种指定 新生代内存(Young Generation) 大小的方法

    1. 通过 -XX:NewSize-XX:MaxNewSize

      -XX:NewSize=<young size>[unit] 
      -XX:MaxNewSize=<young size>[unit]
      

      如,为新生代分配最小256m的内存,最大1024m的内存我们的参数为:

      ...

类文件结构

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

概述 #

  • Java中,JVM可以理解的代码就叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机
  • Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时效率极高,且由于字节码并不针对一种特定的机器。因此,Java程序无需重新编译便可在多种不通操作系统的计算机运行
  • Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译.class文件最终运行在 Java 虚拟机之上。.class文件的二进制格式可以使用 WinHexopen in new window 查看。

ly-20241212141957797

.class文件是不同语言在Java虚拟机之间的重要桥梁,同时也是支持Java跨平台很重要的一个原因

Class文件结构总结 #

根据Java虚拟机规范,Class文件通过ClassFile定义,有点类似C语言的结构体

ClassFile的结构如下:

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类可以有多个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
} 

ly-20241212141958065

...

类加载器详解

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

回顾一下类加载过程 #

开始介绍类加载器和双亲委派模型之前,简单回顾一下类加载过程。

  • 类加载过程:加载->连接->初始化
  • 连接过程又可分为三步:验证->准备->解析

[类加载过程

加载是类加载过程的第一步,主要完成下面 3 件事情:

  1. 通过全类名获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口

类加载器 #

类加载器介绍 #

类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。

根据官方 API 文档的介绍:

A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a “class file” of that name from a file system.

...

类加载过程

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

类的声明周期 #

ly-20241212141959924

类加载过程 #

  • Class文件,需要加载到虚拟机中之后才能运行和使用,那么虚拟机是如何加载这些Class文件呢
  • 系统加载Class类文件需要三步:加载->连接->初始化。连接过程又分为三步:验证->准备->解析
    ly-20241212142000194

加载 #

类加载的第一步,主要完成3件事情

构造与类相关联的方法表

  1. 通过全类名获取定义此类的二进制字节流
  2. 字节流所代表的静态存储结构,转换为方法区运行时数据结构
  3. 在内存中生成一个该类的Class对象,作为方法区这些数据的访问入口

虚拟机规范对上面3点不具体,比较灵活

  1. 对于1 没有具体指明从哪里获取、怎样获取。可以从ZIP包读取 (JAR/EAR/WAR格式的基础)、其他文件生成(JSP)等
  • 非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器控制字节流的获取方式(重写一个类加载器的**loadClass()**方法
  • 数组类型不通过类加载器创建,它由Java虚拟机直接创建

加载阶段连接阶段的部分内容是交叉执行的,即加载阶段尚未结束,连接阶段就可能已经开始了

验证 #

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《Java 虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全

验证阶段主要由四个检验阶段组成:

  1. 文件格式验证(Class 文件格式检查)
  2. 元数据验证(字节码语义检查)
  3. 字节码验证(程序语义检查)
  4. 符号引用验证(类的正确性检查)

ly-20241212142000337

准备 #

  • 准备阶段是正式为类变量分配内存设置类变量初始值的阶段,这些内存都将在方法区中分配,注意:

    1. 这时候进行内存分配的包括类变量ClassVariables,即静态变量:被static关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。

      实例变量会在对象实例化时,随着对象一块分配到Java堆

    2. 从概念上讲,类变量所使用的内存都应当在 方法区 中进行分配。不过有一点需要注意的是:JDK 7 之前,HotSpot 使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。 而在 JDK 7 及之后,HotSpot 已经把原本放在永久代字符串常量池静态变量等移动到中,这个时候类变量则会随着 **Class 对象(上面有提到,内存区生成Class对象)**一起存放在 Java 堆中

    3. 这里所设置的初始值**“通常情况”下是数据类型默认的零值(如 0、0L、null、false 等**),比如我们定义了**public static int value=111** ,那么 value 变量在准备阶段的初始值就是 0 而不是 111初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111

      ...

java垃圾回收

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

前言 #

需要排查各种内存溢出问题、当垃圾收集成为系统达到更高并发的瓶颈时,我们就需要对这些**“自动化”的技术实施必要的监控调节**

堆空间的基本结构 #

  • Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。且Java自动内存管理最核心的功能是内存中的对象分配回收

  • Java堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)

  • 垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以Java堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法

  • JDK7版本及JDK7版本之前,堆内存被通常分为下面三部分:

    1. 新生代内存(Young Generation)
    2. 老生代(Old Generation)
    3. 永久代(Permanent Generation)

hotspot-heap-structure

JDK8版本之后PermGen(永久)已被Metaspace(元空间)取代,且已经不在堆里面了,元空间使用的是直接内存

内存分配和回收原则 #

对象优先在Eden区分配 #

  • 多数情况下,对象在新生代中Eden区分配。当Eden区没有足够空间进行分配时,会触发一次MinorGC 首先,先添加一下参数打印GC详情:-XX:+PrintGCDetails

    public class GCTest {
    	public static void main(String[] args) {
    		byte[] allocation1, allocation2;
    		allocation1 = new byte[30900*1024];//会用掉3万多K
    	}
    } 
    

    运行后的结果(这里应该是配过xms和xmx了,即堆内存大小) img 如上,Eden区内存几乎被分配完全(即使程序什么都不做,新生代也会使用2000多K)

    注: PSYoungGen 为 38400K ,= 33280K + 5120K (Survivor区总会有一个是空的,所以只加了一个5120K )

    假如我们再为allocation2分配内存会怎么样(不处理的话,年轻代会溢出)

    allocation2 = new byte[900 * 1024];
    

    img 在给allocation2分配内存之前,Eden区内存几乎已经被分配完。所以当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC。GC期间虚拟机又发现allocation1无法存入空间,所以只好通过分配担保机制,把新生代的对象提前转移到老年代去,老年代的空间足够存放allocation1,所以不会出现Full GC(这里可能是之前的说法,可能只是要表达老年代的GC,而不是Full GC(整堆GC) )  

    执行MinorGC后,后面分配的对象如果能够存在Eden区的话,还是会在Eden区分配内存
    执行如下代码验证:

  public class GCTest {
  
  	public static void main(String[] args) {
  		byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
  		allocation1 = new byte[32000*1024];
  		allocation2 = new byte[1000*1024];
  		allocation3 = new byte[1000*1024];
  		allocation4 = new byte[1000*1024];
  		allocation5 = new byte[1000*1024];
  	}
  } 

大对象直接进入老年代 #

  • 大对象就是需要连续空间的对象(字符串数组等)
  • 大对象直接进入老年代,主要是为了避免为大对象分配内存时,由于分配担保机制(这好像跟分配担保机制没有太大关系)带来的复制而降低效率
  • 假设大对象最后会晋升老年代,而新生代是基于复制算法来回收垃圾的,由两个Survivor区域配合完成复制算法,如果新生代中出现大对象且能屡次躲过GC,那这个对象就会在两个Survivor区域中来回复制,直至最后升入老年代,而大对象在内存里来回复制移动,就会消耗更多的时间。

    ...

jvm-intro

转载自https://github.com/Snailclimb/JavaGuide(添加小部分笔记)感谢作者!

原文地址: https://juejin.im/post/5e1505d0f265da5d5d744050#heading-28 感谢原作者分享!!

JVM的基本介绍 #

  • JVM,JavaVirtualMachine的缩写,虚拟出来的计算机,通过在实际的计算机上仿真模拟各类计算机功能实现
  • JVM类似一台小电脑,运行在windows或者linux这些真实操作系统环境下直接和操作系统交互,与硬件不直接交互,操作系统帮我们完成和硬件交互的工作

img

Java文件是如何运行的 #

场景假设:我们写了一个HelloWorld.java,这是一个文本文件。JVM不认识文本文件,所以需要一个编译,让其(xxx.java)成为一个JVM会读的二进制文件—> HelloWorld.class

  1. 类加载器 如果JVM想要执行这个.class文件,需要将其**(这里应该指的二进制文件)装进类加载器**中,它就像一个搬运工一样,会把所有的.class文件全部搬进JVM里面 img

  2. 方法区

    类加载器将.class文件搬过来,就是先丢到这一块上

    方法区是用于存放类似于元数据信息方面的数据的,比如类信息常量静态变量编译后代码…等

  3. 堆 堆主要放一些存储的数据,比如对象实例数组…等,它和方法区都同属于线程共享区域,即它们都是线程不安全

  4. 线程独享
    栈是我们代码运行空间,我们编写的每一个方法都会放到里面运行。
    名词:本地方法栈本地方法接口,不过我们基本不会涉及这两块内容,这两底层使用C进行工作,和Java没有太大关系

  5. 程序计数器 主要就是完成一个加载工作,类似于一个指针一样的,指向下一行我们需要执行的代码。和栈一样,都是线程独享的,就是每一个线程都会自己对应的一块区域而不会存在并发和多线程问题。

  6. 小总结 img

    1. Java文件经过编译后编程.class字节码文件
    2. 字节码文件通过类加载器被搬运到 JVM虚拟机中
    3. 虚拟机主要的5大块:方法区、堆 都为线程共享区域,有线程安全问题;本地方法栈计数器都是独享区域,不存在线程安全问题,而JVM的调优主要就是围绕两大块进行

简单的代码例子 #

一个简单的学生类及main方法:

public class Student {
    public String name;

    public Student(String name) {
        this.name = name;
    }

    public void sayName() {
        System.out.println("student's name is : " + name);
    }
}

main方法:

public class App {
    public static void main(String[] args) {
        Student student = new Student("tellUrDream");
        student.sayName();
    }
}

★★ 执行main方法的步骤如下

...