【JVM】JVM 监控及诊断工具

JVM 调优概述

生产环境中的问题

  • 生产环境发生了内存溢出该如何处理?
  • 生产环境应该给服务器分配多少内存合适?
  • 如何对垃圾回收器的性能进行调优?
  • 生产环境CPU负载飙高该如何处理?
  • 生产环境应该给应用分配多少线程合适?
  • 不加log,如何确定请求是否执行了某一行代码?
  • 不加log,如何实时查看某个方法的入参与返回值?

为什么要调优

  • 防止出现OOM
  • 解决OOM
  • 减少Full GC出现的频率

不同阶段的考虑

  • 上线前
  • 项目运行阶段
  • 线上出现OOM

调优概述

监控的依据

  • 运行日志
  • 异常堆栈
  • GC日志
  • 线程快照
  • 堆转储快照

调优的大方向

  • 合理地编写代码
  • 充分并合理的使用硬件资源
  • 合理地进行JVM调优

性能优化的步骤

第1步:性能监控

  • GC频繁
  • cpu load过高
  • OOM
  • 内存泄露
  • 死锁
  • 程序响应时间较长

第2步:性能分析

  • 打印GC日志,通过GCviewer或者 http://gceasy.io 来分析异常信息
  • 灵活运用命令行工具、jstack、jmap、jinfo等
  • dump出堆文件,使用内存分析工具分析文件
  • 使用阿里Arthas、jconsole、JVisualVM来实时查看JVM状态
  • jstack查看堆栈信息

第3步:性能调优

  • 适当增加内存,根据业务背景选择垃圾回收器
  • 优化代码,控制内存使用
  • 增加机器,分散节点压力
  • 合理设置线程池线程数量
  • 使用中间件提高程序效率,比如缓存、消息队列等
阅读全文

【数据结构】图

图的数据结构

Graph 代表图的数据结构,其又由 NodeEdge 两种数据结构组成。

1
2
3
4
5
6
7
8
9
public class Graph {
public HashMap<Integer, Node> nodes;
public HashSet<Edge> edges;

public Graph() {
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Node {
public int value;
public int in;
public int out;
public ArrayList<Node> nexts;
public ArrayList<Edge> edges;

public Node(int value) {
this.value = value;
this.in = 0;
this.out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Edge {
public int weight;
public Node from;
public Node to;

public Edge(int weight, Node from, Node to) {
this.weight = weight;
this.from = from;
this.to = to;
}
}
阅读全文

【数据结构】树

二叉树的遍历

二叉树的前序、中序、后序遍历本质上就是将打印语句放到 “第一次来到该节点、第二次回到该节点、第三次回到该节点” 的位置。

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
public static void traversal(Node head) {
if (head == null) {
return;
}

// --------------------------------
// 第一次来到该节点, 对应前序
// 访问其左右子树前, 就会来到这里
// --------------------------------

// 递归遍历左子树
traversal(head.left);

// --------------------------------
// 第二次回到该节点, 对应中序
// 访问完其整个左子树后, 才会来到这里
// --------------------------------

// 递归遍历右子树
traversal(head.right);

// --------------------------------
// 第三次回到该节点, 对应后序
// 访问完其整个右子树后, 才会来到这里
// --------------------------------
}

前序遍历

前序遍历:头 -> 左 -> 右

递归方式

1
2
3
4
5
6
7
8
public static void preOrderRecur(Node head) {
if (head == null) {
return;
}
System.out.print(head.value + " ");
preOrderRecur(head.left);
preOrderRecur(head.right);
}

非递归方式:借助一个栈结构

后序遍历的非递归方式需要借助两个栈,步骤和前序遍历相似,只不过将一个栈中的元素压入到另一个栈后再统一出栈打印

非递归的前序遍历,需要使用一个栈结构。

先将根节点入栈,然后不断循环,按照 “弹栈打印 -> 右孩子入栈 -> 左孩子入栈” 的顺序遍历,即可实现前序遍历。

因为每次都将右孩子先入栈,同时弹出左孩子时就立刻再次压入其右孩子和左孩子,所以某个节点的右子树会堆积在栈底,一直到其左子树都弹栈后才会再遍历,从而达到了先序遍历的效果。

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
//    1. 首先, 树的根结点先入栈,
// while (栈不空) {
// 2. 弹出栈顶节点并打印
// 3. 先将右孩子入栈, 再将左孩子入栈 (注意先后顺序要先右再左, 这样弹栈时就会先弹左再弹右)
// }
// 因为每次都将右孩子先入栈,同时弹出左孩子时就立刻再次压入其右孩子和左孩子,所以某个节点的右子树
// 会堆积在栈底,一直到其左子树都弹栈后才会再遍历,从而达到了先序遍历的效果
// 总结: 先弹顶 -> 压左 -> 压右 -> 弹栈 -> 压左 -> 压右

// 前序遍历的效果: 先把根结点的左子树上的所有节点遍历后才会遍历右子树
// 并且每个子树都遵循该效果
public static void preOrderUnRecur(Node head) {
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.push(head);
Node cur = null;
while (!stack.isEmpty()) {
cur = stack.pop();
System.out.print(cur.value + " ");

if (cur.right != null) {
stack.push(cur.right);
}
if (cur.left != null) {
stack.push(cur.left);
}
}
}
}
阅读全文

【JUC】异步编程

异步回调 CompletableFuture

CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调的方式在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。

  • Future 接口不是通过回调的方式,而是使用 get() 方法阻塞判断业务是否执行完毕,所以本质还是同步
  • CompletableFuture接口通过回调的方式,并不会阻塞等待业务执行完毕,而是在业务执行完毕后回调通知主线程运行结果,这才是真正的异步。

image-20210917164341898

CompletableFuture实现了 Future, CompletionStage接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture

  • runAsync():调用没有返回值方法,主线程调用 get()方法时会阻塞(这种方式和 Future相似,本质还是同步)
  • supplyAsync():调用有返回值方法(回调的方式得到运行结果,程序不会阻塞,真正的异步)
阅读全文

【数据结构】链表

链表基础操作

链表逆序打印

使用递归将链表逆序打印,最先打印的是链表的尾结点,他是从后往前打印的:

1
2
3
4
5
6
private void printListNode(ListNode head) {
if (head == null)
return;
printListNode(head.next);
System.out.println(head.val);
}

说明要想实现链表的逆序操作,可以使用递归实现。链表的题目常常可以用递归解决。

反转链表

方法一:使用栈结构

因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。但是效率较低。原理如下:

image.png

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public ListNode reverseList(ListNode head) {
Stack<ListNode> stack = new Stack<>();
// 把链表节点全部摘掉放到栈中
while (head != null) {
stack.push(head);
head = head.next;
}
if (stack.isEmpty())
return null;
ListNode node = stack.pop();
ListNode dummy = node;

// 栈中的结点全部出栈,然后重新连成一个新的链表
while (!stack.isEmpty()) {
ListNode tempNode = stack.pop();
node.next = tempNode;
node = node.next;
}

// 最后一个结点就是反转前的头结点,一定要让他的next
// 等于空,否则会构成环
node.next = null;
return dummy;
}
阅读全文

【JVM】JVM 垃圾回收器

垃圾回收的相关概念

在介绍垃圾回收器之前,首先介绍一下垃圾回收的相关概念

System.gc() 的理解

在默认情况下,通过System.gc()或者Runtime.getRuntime().gc() 的调用,会显式触发Full GC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

然而System.gc()调用附带一个免责声明,无法保证对垃圾收集器的调用(不能确保立即生效)

JVM实现者可以通过 System.gc() 调用来决定JVM的GC行为。而一般情况下,垃圾回收应该是自动进行的,无须手动触发,否则就太过于麻烦了。在一些特殊情况下,如我们正在编写一个性能基准,我们可以在运行之间调用System.gc()

阅读全文

【JVM】JVM 垃圾回收算法

垃圾回收概述

前言

在提到什么是垃圾之前,我们先看下面一张图

image-20200712085456113

从上图我们可以很明确的知道,Java 和 C++语言的区别,就在于垃圾收集技术和内存动态分配上,C语言没有垃圾收集技术,需要程序员手动收集。

垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。

关于垃圾收集有三个经典问题:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

什么是垃圾?

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出。

磁盘碎片整理

机械硬盘需要进行磁盘整理,同时还有坏道

image-20200712090848669

阅读全文

【JVM】JVM 字符串常量池

String 的基本特性

String:字符串,使用一对 "" 引起来表示。Java 程序中的所有字符串字面值(如"abc")都作为此类的实例实现。

1
2
String s1 = "hello";                // 字面量的定义方式
String s2 = new String("hello"); // new 对象的方式
  • String是一个final类,代表其不可被继承,从而保护其线程安全性(避免被子类继承后破坏线程安全性)字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改
  • String对象的字符内容是存储在一个字符数组常量final char value[]中的
  • String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示String可以比较大小

img

String 的不可变性

String:代表不可变的字符序列。简称:不可变性。

  1. 当对字符串重新赋值时,需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  2. 当对现有的字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  3. 当调用Stringreplace()方法修改指定字符或字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值。
  4. 通过字面量(如"abc")的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池
  5. 字符串常量池中是不会存储相同内容的字符串的。

img

字面量包含:文本字符串、声明为 final 的常量值

阅读全文

【JVM】JVM 执行引擎

执行引擎概述

执行引擎属于JVM的下层,里面包括解释器及时编译器垃圾回收器

image-20200710080707873

执行引擎是Java虚拟机核心的组成部分之一。

“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。

那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

image-20200710081118053

  • 前端编译:从Java程序员-字节码文件的这个过程叫前端编译
  • 执行引擎这里有两种行为:一种是解释执行,一种是编译执行(被称为后端编译)。
阅读全文

【JVM】JVM 对象实例化过程

对象实例化

面试题

  • 对象在JVM中是怎么存储的?
  • 对象头信息里面有哪些东西?
  • Java对象头有什么?

image-20200709095356247

对象创建方式

  • new:最常见的方式、单例类中调用getInstance()的静态类方法,XxxFactory的静态方法
  • Class的newInstance()方法:在JDK 9 里面被标记为过时的方法,因为只能调用空参构造器
  • Constructor的newInstance(xxx)反射的方式,可以调用空参的,或者带参的构造器
  • 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone()接口(Cloneable接口只是标识接口,不带任何方法,实际上是重写 Object 父类的 clone() 方法,这属于原型模式)
  • 使用序列化:序列化一般用于Socket的网络传输
  • 第三方库 Objenesis

创建对象的步骤

对象实例化的过程

  • 加载类元信息
  • 为对象分配内存
  • 处理并发问题
  • 属性的默认初始化(只进行零值初始化)
  • 设置对象头信息
  • init() 方法:属性的显示初始化、代码块中初始化、构造器中初始化

为对象属性赋值的顺序

给对象的属性赋值的操作:

  • 属性的默认初始化
  • 属性的显式初始化 / 代码块中初始化(执行顺序取决于代码中的编写顺序)
  • 构造器中初始化
阅读全文