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”开头的为非稳定参数。
- 内存控制相关参数
- -Xms256m:最小堆的大小设置为256MB
- -Xmx1024m:最大堆大小设置为1024MB
- -Xss1m:设置线程栈大小为1MB
- -XX:NewRatio=2:新生代内存容量与老生代内存容量的比例1:2
- 垃圾回收器相关参数
- -XX:+UseParallelGC:启用并行GC(Java8默认使用)
- -XX:+UseG1GC:使用G1收集器
- -XX:+UseZGC:使用ZGC
- 其他参数
- -cp:设置类路径
- -DpropertyName=value:设定进程的自定义系统属性
- -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占用过高问题:
- 使用top命令获得java进程id
- 使用top Hp [pid] 观察该进程所有的线程CPU使用情况
- 将观察到的线程id记录下来,转化为16进制的数(例如:printf "%x\n" 13193)
- 使用jstack -l [pid] 将java进程的线程快照dump出来然后查找上一步中记录的线程id。分析线程正在执行的代码。
五、使用G1 GC启动一个Java应用,并用命令行工具分析程序运行情况
- 使用jinfo -flags pid
参数中可以看到这个Java进程使用了G1GC,最大堆大小为2GB
- 使用jstat -gc pid 2s 10 (每间隔2秒打印一次Java进程的状态,打印10次)
可以看到当前的Java进程发生的YangGC次数为14次,总耗时为116ms,平均每次不到10ms。发生FullGC的次数为0次。
-
使用jstack -l pid 查看线程栈快照信息
快照展示了有多少个线程,线程id是什么。找一下我自己的创建的线程(YulinThread):
-
使用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策略来提升系统的性能(因为提升的空间太小),首先做的是优化业务设计。