JVM - jvm工具

jvm工具简介

命令行
  1. jps(查看jvm进程id)

  2. jstat (查看jvm进程的运行时内存状态)

    1
    2
    3
    kb-jaydeMacBook-Air:tree kb_jay$ jstat -gcutil 5552
    S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
    66.70 0.00 79.02 0.16 97.32 93.78 2 0.013 0 0.000 0.013

    对应的信息。survivor0,1,eden,老年代,元数据,压缩类信息,新生代gc次数,新生代gc时间。。。

  3. jinfo (查看jvm配置)

  4. jstack (查看线程运行信息)

  5. jMap (堆转储)

可视化
  1. jconsole (查看cpu,内存,线程等)

  2. VisualVm (多和一,想看啥看啥,支持安装插件)

JVM - 内存分配

jvm内存管理分为内存回收跟内存分配两个大部分,内存分配时本篇的主题。

分配规则

  1. 对象优先在eden区域申请空间
  2. 大对象直接进入老年代
  3. 长期存活的对象进入老年代
  4. 内存分配担保(老年代对survivor区域的担保)

前三个很好理解,我们分析下第四个,堆分为老年代跟新生代,新生代有一块eden跟两块survivor,eden跟survivor占新生代空间大小是可以通过jvm运行参数配置的,一般8:1;

假设我们配置了新生代跟老年代分别10M,eden跟survivor8:1;我们依次创建2M,2M,2M,4M的对象,那么情况如下:

  1. 前三个对象进入eden区域
  2. 第四个对象发现空间不够了(eden区域剩下2M,survivorA 1M),触发GC
  3. GC时前三个对象都是不可回收对象,需要放入survivorB,但是survivorB空间不够,此时就会触发分配担保,这样这三个对象就直接放入了老年代。此时老年代使用了6M,eden跟两个survivor区域都是空,
  4. 将4M的对象放入eden区域

所有的对象都会在堆上申请空间吗?

大部分是的,但也会有在栈上申请空间的对象。

这里涉及内存逃逸跟栈内存分配。

我们先回顾下虚拟机栈的内容,它可以用来描述方法执行过程,每个方法对应一个栈帧,栈帧中有局部变量表等。

如果在一个方法中有一个变量的作用域只是该方法,那么就会在栈上给它申请空间(包含在方法栈帧中),此时该变量对方法来说就是没有逃逸的,也就是被方法栈帧“捉”住了。当方法执行完毕之后随着栈帧的释放会释放该变量的内存。

JVM - GC

Q:如何确认那些对象的可以回收的

  1. 引用计数

    “jvm引用计数”的图片搜索结果

    如上图,左边为堆,右边为虚拟机栈中的局部变量表,如果堆中的对象相互引用了,那么使用这种方式,即使将引用1跟2断开,对象的引用数量不为0,此时两个堆中的对象无法回收,所以现有的主流jvm虚拟机实现基本不会采用这种方式。

  2. GCRoot(可达性分析)

    从如下的GCRoot节点开始向下搜索,当一个对象跟GCRoot节点没有任何引用链的时候,这个对象就是可以回收的。

可以作为GCRoot的有

  • 虚拟机栈中的局部变量表中的引用
  • 本地方法栈。。
  • 方法区中类静态变量引用的对象
  • 方法区中常量引用的对象

Q:怎么回收

  1. 标记-清除:通过GCRoot找到可回收对象,标记之后清除掉。

    会有空间碎片

  2. 标记-整理:找到可回收对象之后将它拼接到内存空间的首部

  3. 复制:将空间分为两部分A跟B,当A需要回收时,将不回收的对象copy到B,之后清除A。

    空间浪费

  4. 分代算法:将堆空间分为新生代跟老年代,新生代中的对象在GC的时候有很大一部分需要被回收(IBM-98%),对新生代的GC我们可以改进下复制算法,将区域分为eden(80%)跟两个survivor A跟B(20%);申请空间的时候我们从优先从eden申请,之后从A申请,回收的时候将eden跟A中的不回收对象copy到B,之后清理掉eden跟A。如此。。

    对于老年代,每次GC都会有很大部分对象不需要被回收掉,所以更加适合标记-整理算法。

JVM - 对象的创建

流程

  1. new 对象之后,先看方法区的常量池中是否有类的符号引用
  2. 如果没有,那么加载类(todo),完成之后就知道我们需要申请多少空间了
  3. 堆中申请空间
  4. 初始化,基本对象为0,引用对象为null
  5. 调用< init > 构造方法
内存分配问题

两种方案

  1. 使用指针碰撞:可用内存跟不可用内存之间有个指针,可用跟不可用内存是规整的
  2. 使用空闲列表:空闲区域的指针放入链表中
线程安全问题

假如线程1有个a对象,线程2有个b对象,a正在申请的时候,b也开始申请,堆是共享的,会有线程安全问题。

两种方案:

  1. 每次分配内存都加锁
  2. 堆中按线程分区域(线程缓存区),申请线程缓存区的时候加锁

对象的内存布局

  1. 对象头
    • 运行时数据(对象的hashcode,gc年代,锁状态等)
    • 类指针(指向方法区中的类信息,表明该对象是哪个类)
  2. 实例数据
  3. 对齐填充padding
Hotspot对象访问定位

栈中有堆对象的引用,堆对象中有方法区中对象类型数据(class)的引用

tip:还有一种是句柄的方式

堆中有句柄池跟对象池,栈中有句柄池的引用,句柄池中有对象的引用跟对象类型数据(class)的引用

参考 《深入理解jvm》49页

JVM - 内存区域

全貌

盗图一张如下:

img

程序计数器

  1. 记录字节码文件当前的执行行数
  2. 是每个线程私有的,共有的话,切换线程会懵逼掉的
  3. 如果执行native方法的话,它的值是undifined。因为native方法是java通过JNI直接调用本地C/C++库,可以近似的认为native方法相当于C/C++暴露给java的一个接口,java通过调用这个接口从而调用到C/C++方法。由于该方法是通过C/C++而不是java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由自己语言决定的,而不是由JVM决定的。
  4. 不会oom。因为它的空间很小并且不是我们申请的,是jvm自己管理的。

虚拟机栈

  1. 它是跟方法对应的,是用来描述方法调用过程中的内存模型。(方法区是跟class对应的)
  2. 栈帧:他的数据结构是个栈,每个方法对应一个栈帧,假如执行a方法,a方法中又调用了b方法,那么会先把a 压栈,执行到b的时候把b压栈,b执行完b出栈,之后接着执行a,a执行完a出栈。
  3. 局部方法表:每个方法执行需要的内存大小在编译的时候就确定了,局部方法表存放执行方法需要的变量,如果该变量是个对象的话,存放的是引用。局部方法表是在栈帧中的。(所有对象的真实存放区域是堆)
  4. 虚拟机的栈的深度是有最大值的,如果栈太深会报stackoverflow
  5. 如果方法执行没有到达栈的最大值,但是没有内存空间可用了,那么会报oom
  6. 线程私有的

本地方法栈

  1. 虚拟机栈跟本地方法栈基本一致,只不过不是java方法,而是对应native方法
  2. 线程私有的
  3. 会有oom跟stackoverflow

堆

  1. 存放对象的
  2. 线程共享的
  3. gc主要的目标
  4. 会oom

方法区

  1. 存放类信息(跟class对应),存放常量(常量池,1.8 将常量池放入了堆),存放静态变量等
  2. 线程共享的
  3. 会oom
  4. 在java8中被移除了,取而代之的是metaspace(元数据空间)

eg:

运行如下代码:以下只分析1.8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String a = "123";
String b = "123";
//true 两个都是常量池中 123 的地址
System.out.println(a == b);

String c = new String("123");
//一个堆 一个常量池,不一样 false
System.out.println(a == c);

//java 1.8 因为常量池已经有 123 了,所以返回常量池中的123的地址
String d = c.intern();
//所以true
System.out.println(a == d);

//下面的代码将 "我是wzq" 放入常量池,并创建了一个堆对象e
String e = new String("我是wzq");
//java 1.8 因为常量池有 我是wzq ,所以将 我是wzq 在常量池中的地址返回,
String f = e.intern();
//e是堆地址,f是常量池地址 false
System.out.println(e == f);

//往常量池中放入 我是wzq,以为已经有了,所有返回 我是wzq 在常量池中的地址,所以是true
String g = "我是wzq";
System.out.println(f == g);

URL参数的sign签名认证

在写开放的API接口时是如何保证数据的安全性的?

tip:以下内容非原创,

原文:https://blog.csdn.net/qq_15901351/article/details/80175169

先来看看有哪些安全性问题在开放的api接口中,我们通过http Post或者Get方式请求服务器的时候,会面临着许多的安全性问题,例如:

  1. 请求来源(身份)是否合法?

  2. 请求参数被篡改?

  3. 请求的唯一性(不可复制)

解决方案:为了保证数据在通信时的安全性,我们可以采用参数签名的方式来进行相关验证。

案列分析

我们通过给某 [移动端(app)] 写 [后台接口(api)] 的案例进行分析:

客户端:以下简称app

后台接口:以下简称api

我们通过app查询产品列表这个操作来进行分析:

app中点击查询按钮==》调用api进行查询==》返回查询结果==>显示在app中

一、不进行验证的方式

api查询接口:

app调用:http://api.test.com/getproducts?参数1=value1.......

如上,这种方式简单粗暴,通过调用getproducts方法即可获取产品列表信息了,但是这样的方式会存在很严重的安全性问题,没有进行任何的验证,大家都可以通过这个方法获取到产品列表,导致产品信息泄露。

那么,如何验证调用者身份呢?如何防止参数被篡改呢?

二、MD5参数签名的方式

我们对api查询产品接口进行优化:

1.给app分配对应的key、secret

2.Sign签名,调用API 时需要对请求参数进行签名验证,签名方式如下:

a. 按照请求参数名称将所有请求参数按照字母先后顺序排序得到:keyvaluekeyvalue…keyvalue 字符串如:将arong=1,mrong=2,crong=3 排序为:arong=1, crong=3,mrong=2 然后将参数名和参数值进行拼接得到参数字符串:arong1crong3mrong2。

b. 将secret加在参数字符串的头部后进行MD5加密 ,加密后的字符串需大写。即得到签名Sign

新api接口代码:

app调用:http://api.test.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&参数1=value1&参数2=value2.......

注:secret 仅作加密使用, 为了保证数据安全请不要在请求参数中使用。

如上,优化后的请求多了key和sign参数,这样请求的时候就需要合法的key和正确签名sign才可以获取产品数据。这样就解决了身份验证和防止参数篡改问题,如果请求参数被人拿走,没事,他们永远也拿不到secret,因为secret是不传递的。再也无法伪造合法的请求。

但是…这样就够了吗?细心的同学可能会发现,如果我获取了你完整的链接,一直使用你的key和sign和一样的参数不就可以正常获取数据了…-_-!是的,仅仅是如上的优化是不够的。。。。。。

请求的唯一性:

为了防止别人重复使用请求参数问题,我们需要保证请求的唯一性,就是对应请求只能使用一次,这样就算别人拿走了请求的完整链接也是无效的。
唯一性的实现:在如上的请求参数中,我们加入时间戳:timestamp(yyyyMMddHHmmss),同样,时间戳作为请求参数之一,也加入sign算法中进行加密。

新的api接口:

app调用:
http://api.test.com/getproducts?key=app_key&sign=BCC7C71CF93F9CDBDB88671B701D8A35&timestamp=201603261407&参数1=value1&参数2=value2.......
如上,我们通过timestamp时间戳用来验证请求是否过期。这样就算被人拿走完整的请求链接也是无效的。

Sign签名安全性分析:

通过上面的案例,我们可以看出,安全的关键在于参与签名的secret,整个过程中secret是不参与通信的,所以只要保证secret不泄露,请求就不会被伪造。

总结

上述的Sign签名的方式能够在一定程度上防止信息被篡改和伪造,保障通信的安全,这里使用的是MD5进行加密,当然实际使用中大家可以根据实际需求进行自定义签名算法,比如:RSA,SHA等加密算法。

数据结构 - 树

二叉树遍历

  1. 定义二叉树

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * @author kb_jay
    * @time 2019/8/26
    **/
    public class Node<T> {
    public T data;
    public Node leftChild;
    public Node rightChild;

    public Node() {
    }

    public Node(T data) {
    this(data, null, null);
    }

    public Node(T data, Node leftChild, Node rightChild) {
    this.data = data;
    this.leftChild = leftChild;
    this.rightChild = rightChild;
    }
    }
  2. 遍历二叉树

    • 前序遍历(递归跟非递归)

      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
      //递归
      public static void firstRoot(Node root) {
      if (root == null) {
      return;
      }

      System.out.println(root.data);
      firstRoot(root.leftChild);
      firstRoot(root.rightChild);
      }

      //非递归
      public static void firstRootStack(Node root) {
      Stack<Node> stack = new Stack<>();
      while (root != null || stack.size() > 0) {
      //输出并压栈左孩子
      while (root != null) {
      System.out.println(root.data);
      stack.push(root);
      root = root.leftChild;
      }
      //出栈遍历右孩子
      if (stack.size() > 0) {
      Node node = stack.pop();
      root = node.rightChild;
      }
      }
      }
    • 中序遍历(递归跟非递归)

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

      centerRoot(root.leftChild);
      System.out.println(root.data);
      centerRoot(root.rightChild);
      }

      public static void centerRootStack(Node root) {
      Stack<Node> stack = new Stack<>();
      while (root != null || stack.size() > 0) {
      while (root != null) {
      stack.push(root);
      root = root.leftChild;
      }

      if (stack.size() > 0) {
      root = stack.pop();
      System.out.println(root.data);
      root = root.rightChild;
      }
      }
      }
    • 后序遍历(递归跟非递归)

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

      lastRoot(node.leftChild);
      lastRoot(node.rightChild);
      System.out.println(node.data);
      }

      public static void lastRootStack(Node root) {
      Stack<Node> stack = new Stack<>();
      //记录前一个输出的node
      Node pre = null;
      while (root != null || stack.size() > 0) {
      while (root != null) {
      stack.push(root);
      root = root.leftChild;
      }

      if (stack.size() > 0) {
      Node temp = stack.peek().rightChild;
      if (temp == null || temp == pre) {
      pre = stack.pop();
      System.out.println(pre.data);
      root = null;
      } else {
      root = temp;
      }
      }
      }
      }
    • 层序遍历

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      public static void layerOut(Node root) {
      Queue<Node> queue = new ArrayDeque<>();
      while (root != null) {
      System.out.println(root.data);

      if (root.leftChild != null) {
      queue.offer(root.leftChild);
      }

      if (root.rightChild != null) {
      queue.offer(root.rightChild);
      }
      //如果队列为空,返回null, 栈的pop方法,如果栈为空,则报异常
      root = queue.poll();
      }
      }

二分搜索树

  1. 任意一个节点的值大于左边的所有节点的值,小于右边的所有节点的值。

    1. 中序遍历是有序的。

2-3 树

  1. 满足二分搜索树
  2. 一个节点可以存放1个或者2个元素
  3. 二三树是绝对平衡树
  4. 二三树添加元素不会往空节点添加,第一步肯定是跟叶子节点合并

AVL树

  1. 平衡二叉树
  2. 任何一个节点的左右孩子到叶子节点的距离相差不超过1
  3. 它是为了解决二分搜索树的退化问题
  4. 每次更新数据都需要通过旋转来维持平衡,所以实际开发中用红黑树会更多

红黑树

  1. 红黑树是一个自平衡的二叉搜索树,并不是严格的avl树。
  2. 它是为了解决二分搜索树在数据更新的过程中复杂度退化的问题。
  3. 红黑树高度近似log2n,近似平衡,红黑树是有2-3-4树演变过来的,因为2-3-4树有绝对平衡性,所以将红色节点去掉之后的高度<=log2n,而红色节点的儿子不能是黑色节点,所以红黑树中从跟节点到叶子节点有一个黑色就最多有一个红色,我们将红色加回来之后的高度<2*log2n
  4. 插入查找删除时间复杂度是log2n
  5. 是由2-3-4树演变过来的我们将红黑树中的红色节点跟父亲节点合并之后就是一个绝对平衡的2-3-4树
  6. 红黑树的平衡过程其实跟魔方复原差不多,记住规则就行(变色跟旋转)
  7. 四条性质:根黑;叶子节点的子节点是黑色;红色节点不相邻;根节点到叶子节点的路径上黑色节点个数一致。 通过234树理解这个还是很好理解的。