Java进阶作业一

一、自己写一个简单的 Hello.java,里面需要涉及基本类型,四则运行,if 和 for,然后自己分析一下对应的字节码
public class Homework {

    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        double c = doPlus(a, b);
        c = doSub(a, b);
        c = doMuti(a, b);
        c = doDiv(a, b);
        doIf(a,b);
        doFor(a,b);
    }

    public static double doPlus(int a, int b) {
        return a + b;
    }

    public static double doSub(int a, int b) {
        return a - b;
    }

    public static double doMuti(int a, int b) {
        return a * b;
    }

    public static double doDiv(int a, int b) {
        return a / b;
    }

    public static void doIf(int a, int b) {
        if (a > b) {
            a = b;
        }
    }

    public static void doFor(int a, int b) {
        for (int i = 0; i < b; i++) {
            a = a + 100;
        }
    }
}

使用javac命令编译出同名的class文件之后,使用javap -c Homework查看字节码内容:

public class Homework {
  public Homework();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: bipush        10                  // 将单字节的常量值10推至栈顶
       2: istore_1                          // 将栈顶int数值存入第二个本地变量
       3: bipush        20
       5: istore_2
       6: iload_1                           // 将第二个int型变量推送到栈顶
       7: iload_2                           // 将第三个int型变量推送到栈顶
       8: invokestatic  #2                  // Method doPlus:(II)D 调用静态方法
      11: dstore_3                          // 将栈顶double型数值存入第四个本地变量
      12: iload_1
      13: iload_2
      14: invokestatic  #3                  // Method doSub:(II)D
      17: dstore_3
      18: iload_1
      19: iload_2
      20: invokestatic  #4                  // Method doMuti:(II)D
      23: dstore_3
      24: iload_1
      25: iload_2
      26: invokestatic  #5                  // Method doDiv:(II)D
      29: dstore_3
      30: iload_1
      31: iload_2
      32: invokestatic  #6                  // Method doIf:(II)V
      35: iload_1
      36: iload_2
      37: invokestatic  #7                  // Method doFor:(II)V
      40: return

  public static double doPlus(int, int);
    Code:
       0: iload_0                 // 将第一个int型本地变量推到栈
       1: iload_1                 // 将第二个int型本地变量推到栈
       2: iadd                    // 将栈顶两个int数值相加并把值存到栈顶
       3: i2d                     // 将栈顶int型数值转换为double型,并将数值存到栈顶
       4: dreturn                 // 从当前方法返回double

  public static double doSub(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: isub                    // 减法
       3: i2d
       4: dreturn

  public static double doMuti(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: imul                    // 乘法
       3: i2d
       4: dreturn

  public static double doDiv(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: idiv                   // 除法
       3: i2d
       4: dreturn

  public static void doIf(int, int);
    Code:
       0: iload_0
       1: iload_1
       2: if_icmple     7         // 比较栈顶两个int数值的大小,当结果小于或等于0时跳转
       5: iload_1
       6: istore_0
       7: return

  public static void doFor(int, int);
    Code:
       0: iconst_0
       1: istore_2
       2: iload_2
       3: iload_1
       4: if_icmpge     18      // 可以看到for循环使用了比较指令和goto指令
       7: iload_0
       8: bipush        100
      10: iadd
      11: istore_0
      12: iinc          2, 1
      15: goto          2
      18: return
}

观察结果:java方法的字节码类似C语言中函数编译而成的汇编语言,具有一系列操作指令,比如运算类的iadd、isub、idiv、imul,逻辑控制类的if_icmple、goto等等,并且这些操作指令都是在栈上进行计算。

二、自定义一个 Classloader,加载一个 Hello.class 文件,执行 hello 方法,此文件内容是一个 Hello.class 文件所有字节(x=255-x)处理后的文件。

Hello.java如下

package helper;
public class Hello {
    public void hello() {
        System.out.println("hello class loader" + getClass().getClassLoader().getName());
    }
}

自定义的累加载器代码如下:

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class YulinClassLoader extends ClassLoader {

    public YulinClassLoader(String name, ClassLoader parent) {
        super(name, parent);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String clazzName = "Hello.class";
        File clazzFile = new File("/Users/encrypt/" + clazzName);
        try {
            FileInputStream inputStream = new FileInputStream(clazzFile);
            byte[] bytes = inputStream.readAllBytes();
            byte[] decryptBytes = new byte[bytes.length];
            // 由于原本的class字节码的每个字节都被255做了减法,因此这里再次用255做减法可以将其还原为正常的字节码
            for (int i = 0; i < bytes.length; i++) {
                decryptBytes[i] = (byte) (255 - bytes[i]);
            }
            inputStream.close();
            return defineClass(name, decryptBytes, 0, decryptBytes.length);
        } catch (IOException e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        
        if (!name.equals("helper.Hello")) {
            return super.loadClass(name, resolve);
        }

       // 破坏双亲委派模型,当加载指定的类时,必须使用当前的类加载器。
        Class<?> clz = findClass(name);
        if (resolve) {
            resolveClass(clz);
        }

        return clz;
    }
}

使用自定义类加载器的代码如下:

    public static void main(String[] args) throws Exception {
        YulinClassLoader loader = new YulinClassLoader("YuLin", ClassCodeEncr.class.getClassLoader());
        loader.getName();
        Class<?> helloClz = loader.loadClass("helper.Hello");
        Object o = helloClz.newInstance();
        Method helloMethod = helloClz.getMethod("hello");
        helloMethod.invoke(o);
    }

总结:自定义累加载器最主要是继承ClassLoader类并重写findClass方法。另外,可以通过重写loadClass方法来限定自定义类加载器加载的类(默认会走双亲委派的方式),本例中,自定义的类加载器只会加载helper.Hello类,其他的类由父加载器加载。

三、列举常用的Java进程启动参数

以“-”开头的为标准参数,所有虚拟机都需要支持。非标准参数以“-X”开头,执行Jjava -X可查看当前虚拟机支持的非标准参数。以“-XX”开头的为非稳定参数。

  • 内存控制相关参数
    1. -Xms256m:最小堆的大小设置为256MB
    2. -Xmx1024m:最大堆大小设置为1024MB
    3. -Xss1m:设置线程栈大小为1MB
    4. -XX:NewRatio=2:新生代内存容量与老生代内存容量的比例1:2
  • 垃圾回收器相关参数
    1. -XX:+UseParallelGC:启用并行GC(Java8默认使用)
    2. -XX:+UseG1GC:使用G1收集器
    3. -XX:+UseZGC:使用ZGC
  • 其他参数
    1. -cp:设置类路径
    2. -DpropertyName=value:设定进程的自定义系统属性
    3. -XX:+HeapDumpOnOutOfMemoryError:让虚拟机在OOM异常出现之后自动生成dump文件

java进程启动的相关参数非常之多,关于GC的就有几十上百个,每种不同的垃圾回收器还有配合使用的其他参数,这些参数应该在具体情况下使用控制变量法测试不同的组合,以达到更好的调优目的。

四、列举常用的Java命令

参考博客:https://www.cnblogs.com/dennisit/p/9119535.html

  • jps -mlv:列出当前所有的Java进程
  • jstat -gc [pid] 2 5:以每2秒一次的速度,展示5次GC情况
  • jstack -l [pid]:查看线程快照
  • jmap -heap [pid]:查看堆信息(JDK9之后jmp命令集成到了jhsdb中,可以使用jhsdb jmap --heap --pid [pid]来代替此操作)
  • jmap -histo [pid]:查看堆中的对象信息
  • jmap -dump:format=b,file=heap.dump [pid]:dump堆信息到文件中(会暂停应用线程)

排查Java进程CPU占用过高问题:

  1. 使用top命令获得java进程id
  2. 使用top Hp [pid] 观察该进程所有的线程CPU使用情况
  3. 将观察到的线程id记录下来,转化为16进制的数(例如:printf "%x\n" 13193)
  4. 使用jstack -l [pid] 将java进程的线程快照dump出来然后查找上一步中记录的线程id。分析线程正在执行的代码。
五、使用G1 GC启动一个Java应用,并用命令行工具分析程序运行情况
  1. 使用jinfo -flags pid

参数中可以看到这个Java进程使用了G1GC,最大堆大小为2GB

  1. 使用jstat -gc pid 2s 10 (每间隔2秒打印一次Java进程的状态,打印10次)

可以看到当前的Java进程发生的YangGC次数为14次,总耗时为116ms,平均每次不到10ms。发生FullGC的次数为0次。

  1. 使用jstack -l pid 查看线程栈快照信息
    快照展示了有多少个线程,线程id是什么。找一下我自己的创建的线程(YulinThread):

  2. 使用jhsdb jmap --heap --pid [pid]查看堆信息
    可以看到这个Java进程使用的GC是G1,另外还有堆的各区域大小和使用情况。

六、运行一下代码,配置不同的GC,观察结果。
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.LongAdder;

public class GCLogAnalysis {
    private static Random random = new Random();
    public static void main(String[] args) {
        long startMillis = System.currentTimeMillis();
        long timeoutMillis = TimeUnit.SECONDS.toMillis(1);
        long endMillis = startMillis + timeoutMillis;
        LongAdder counter = new LongAdder();
        System.out.println("开始运行");
        int cacheSize = 2000;
        Object[] cachedGarbage = new Object[cacheSize];
        while (System.currentTimeMillis() < endMillis) {
            Object garbage = generateGarbage(100*1024);
            counter.increment();
            int randomIndex = random.nextInt(2 * cacheSize);
            if (randomIndex < cacheSize) {
                cachedGarbage[randomIndex] = garbage;
            }
        }
        System.out.println("产生对象的次数:" + counter.longValue());
    }

    private static Object generateGarbage(int max) {
        int randomSize = random.nextInt(max);
        int type = randomSize % 4;
        Object result = null;
        switch (type) {
            case 0:
                result = new int[randomSize];
                break;
            case 1:
                result = new byte[randomSize];
                break;
            case 2:
                result = new double[randomSize];
                break;
            default:
                StringBuilder builder = new StringBuilder();
                String randomString = "randomString-Anything";
                while (builder.length() < randomSize) {
                    builder.append(randomString);
                    builder.append(max);
                    builder.append(randomSize);
                }
                result = builder.toString();
                break;
        }
        return result;
    }
}

配置Xmx=1G Xms=1G,不同GC效果如下:

可见G1垃圾收集器处理垃圾的能力比较出色。

七、如何选择垃圾回收器?

使用控制变量法(控制堆内存大小、分代比例等参数相同),测试不同垃圾回收器的回收性能。性能体现在业务请求耗时、请求的吞吐量上。耗时低、吞肚量高说明性能好。一般情况下,使用默认的GC(Java8 默认的并行GC、Java11默认的G1),只要内存大小合理,通常不需要通过优化GC策略来提升系统的性能(因为提升的空间太小),首先做的是优化业务设计。

posted @ 2021-05-26 21:32  陈玉林  阅读(95)  评论(0编辑  收藏  举报