简书 慢黑八
转载请注明原创出处,谢谢!
如果读完觉得有收获的话,欢迎点赞加关注
1、(5分)下题方法localVar1()中变量b所占槽位index为( ),localVar2()中变量b所占槽位index为 ( )
private void localVar1() {
int a=0;
System.out.println(a);
int b=0;
}
private void localVar2() {
{
int a = 0;
System.out.println(a);
}
int b = 0;
}
答:使用jclasslib查看class信息 ,此题考的是栈上数据局部变量表的槽位复用,如下图localVar1()方法中this占0位,a的index是1,b的index是2,localVar2()方法中,当a处于代码块中的时候,代码块结束a的槽位释放,b的槽位将会使用index=1的槽位,这样做的好处是节省了空间,localVar2方法块执行后,a的槽位释放可以提供给b使用,a引用的对象也可以在满足垃圾回收的条件。
2、(20分)在java7中,画图描述一下程序,在类加载器、方法区、堆、解释执行器、pc寄存器、java栈、本地方法栈、CodeCache、垃圾回收器中是如何关联执行
public class A {
public static void main(String[] args) {
A a = new A();
a.fn();
}
public void fn() {
System.out.println("Hello World");
}
}
答:如下图所示
- A.java编译后生成A.class字节码文件
- 类加载子系统负责从文件系统或网络中加载Class信息,加载的类信息存放于一块称为方法区(JDK1.7叫永久代,JDK1.8中叫做元空间)的内存空间。除了类的信息外,方法区中还会存放运行时的常量池信息,包括字符串常量和数字常量等。
- Java字节码对于JVM就相当于汇编语言对于计算机。JVM启动后会查找带有main方法开始执行,字节码会被JVM最后翻译为计算机可以看懂的机器码在CUP中运行,这个过程由“解释执行器”进行解释执行。还有一部分热点方法字节码由JIT“即时编译器”(即时编译器的解释参见第7题)进行了编译优化,编译优化后的机器码可以更加高效的执行。
- PC寄存器(也叫程序计数器)也是每个线程的私有空间,Java虚拟机会为每一个Java线程创建PC寄存器。在任意时刻,每一个线程总是在执行一个方法,这个被执行的方法叫当前方法,如果当前方法不是本地方法,PC寄存器就会指向当前正在被执行的指令。它可以看作是当前线程所执行的字节码的行号指示器。
- Java栈是一块私有的空间,先进后出的数据结构,只支持出栈和入栈两种操纵。主要保存的内容为栈帧,每一次函数调用(例如:调用main方法,fn方法)都会有一个对应的栈帧被压入Java栈。当前正在执行的函数所对应的帧就是当前的帧(栈顶),它保存着当前函数的局部变量、栈帧数据(例如,对象A()的引用)和中间运算结果数据。在方法执行结束、异常、return之后Java栈会把方法从栈上弹出。
- Java堆在虚拟机启动的时候建立,它是Java程序最主要的内存工作区域。在字节码执行到new A()的时候,对象在堆空间被创建,严格意义上来说对象创建的过程要更复杂一些。简单来说,虚拟机会先把对象尝试在栈上分配(栈上分配的解释参见第4题),当不满足栈上分配的条件后,对象会在在堆中分配。
分配的优先级是:栈上分配 > TLAB分配 > 大对象直接老年代分配 > 新生代分配 - 垃圾回收系统是Java虚拟机的重要组成部分,垃圾回收器可以对方法区、Java堆和直接内存进行回收。其中,Java堆就是垃圾收集器的工作重点。(垃圾回收器的特点参见第3题)
3、(10分)在Java7或8下,简述Serial、ParNew、parallel、cms、G1,五种垃圾回收器的特点与垃圾回收算法的区别。针对串行垃圾回收器、parallel、cms垃圾回收器,新生代(含from、to区)、老年代的默认内存分配比例是?
答:
垃圾回收器 | 新生代垃圾收集器(算法) | 老年代收集器(算法) |
---|---|---|
SerialGC | Copy / 复制算法 | MarkSweepCompact / 标记压缩算法 |
ParNewGC | ParNew / 复制算法 | MarkSweepCompact / 标记压缩算法 |
ParallelGC | PS Scavenge / 复制算法 | PS MarkSweep / 标记整理 |
ConcMarkSweepGC | ParNew / 复制算法 | ConcurrentMarkSweep+serial old / 并发标记整理+压缩 +老年代串行垃圾回收 |
UseG1GC | G1 Eden Space(分区) | G1 Old Space(分区) |
以上五种垃圾收集器,可以出除了G1分区回收之外,另外四种垃圾回收器都是分代回收的。
- G1的特点是把堆分成若干个区,每次只回收若干个区,用于控制停顿时间。 G1中仍然包含着分代的概念,例如:最大堆是1024m,G1默认1m一个区,1024m=1024个区,堆区数=新生代区数+幸存者区数+老年代区数,1024个=24(新生代)+7(幸存者)+993(老年代),对应的内存容量也是这样1024m=24m(新生代)+7m(幸存者)+993m(老年代)
=24(新生代)+7(幸存者)+993(老年代)。 - SerialGC 比较适合在cup比较少的场合,性能往往最好。
- ParNewGC大概就是在串行的基础上多线程化。
- ParallelGC和parnew类似,但是Parallelgc更关注系统的吞吐,可以根据吞吐与停顿时间自动调节新生代、老年代的使用比例。
- 与ParallelGC不同的是ConcMarkSweepGC主要关注系统停顿,是一个多线程并行垃圾回收的老年代垃圾回收器,新生代只能与ParNew组合。
串行垃圾回收器、parallel、cms垃圾回收器新生代,老年代、from、to的比例:
SerialGC 新生代:老年代 = eden:from:to =
ParallelGC 新生代:老年代 = eden:from:to = 比例可自动调节
ConcMarkSweepGC 新生代:老年代 = eden:from:to =
4、(5分)使用JVM参数-server -Xms10m -Xmx10m启动如下程序,是否会出现内存溢出或垃圾回收失败,为什么?
public static class User{
public int id =0 ;
public String name ="";
}
public static void alloc(){
User user = new User();
user.id=5;
user.name="manheiba";
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
alloc();
}
long e = System.currentTimeMillis();
System.out.println(e-b);
}
答:不会出现内存溢出,因为alloc方法内创建的对象会在栈上分配,栈上分配是java虚拟机的一项优化技术,它的基本思想是,对于那些线程私有的对象(这里指不可能被其他线程访问的对象),可以将他们打散分配在栈上,而不是分配在堆上。栈上分配的好处是可以再函数调用结束后自行销毁,而不需要垃圾回收器的介入,从而提高系统的性能。对于大量在循环中创建对象的的服务器可以开启栈上分配从而提升系统性能。栈上分配需要在-server模式下开启内存逃逸分析和标量替换,这两个参数默认是打开的。
上面代码中,循环alloc()方法,在alloc方法中创建对象,创建的user对象并alloc方法外其他对象引用,java虚拟机会判定user对象不会逃逸,可以栈上分配,随着alloc方法执行结束user对象就会被垃圾回收,对象回收的过程不需要垃圾回收器的介入,也不会出现内存溢出的情况。
5、(10分)简述如何触发永久代溢出、栈溢出、java堆溢出、直接内存溢出、创建本地线程溢出以及解决办法?
答:
- 永久带是存放class的区域,可以通过cglib或者asm动态不停创建class让永久带溢出。
- 栈上存放栈帧信息,可以通过方法的无限递归调用,让栈帧上方法无限增加导致栈溢出。
- Java堆溢出:简单来说循环中不停创建不被回收的内存对象即可导致堆的溢出。
- 直接内存溢出:可以循环分配直接内存 造成直接内存溢出。
6、(5分)简述类加载器双亲委派模式?
答:在类加载的时候,系统会判断当前类是否已经加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载,在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载。自定义类加载器的双亲是应用类加载器,应用类加载器的双亲是扩展类加载器,扩展类加载器的双亲是启动类加载器。
7、(10分)简述解释执行器、即时编译器(JIT)在java程序执行时的工作流程,为什么有时候Java方法执行的比C快 ?
答:如上图所示,class字节码会被JVM最后翻译为计算机可以看懂的机器码在CUP中运行,这个过程由“解释执行器”进行解释执行。HotSpot通过循环回边计数器统计热点方法,即时编译器会根据JVM进程的运行时信息(profiler、hprof)进行“编译优化”,接着把“编译优化”后的热点方法放在codeCache中,Java8中默认开启了分层编译,方法调用1500次以下解释执行,达到1500次调用时候调C1编译器,达到10000次时调C2编译器。C1、C2编译后的机器码执行效率更高。通常C2代码的执行效率要比C1高出30%以上。
解释执行相当于同声传译,你说一句我翻一句给观众(CPU)听。JIT是线下翻译,可以花时间精简掉你的口语话表达(做编译优化)。相对比C程序, 执行java程序时,cpu拿到的是C1、C2编译优化后的机器码直接执行,所以执行效率更快一些。
8、(10分)简述如下JVM参数含义:
-XX:+PrintHeapAtGC 打印垃圾回收时的堆信息
-XX:+PrintGCTimeStamps gclog中输出gc发生时间
-XX:+PrintGCApplicationStoppedTime 打印GC停顿时间
-Xloggc 指定gclog的文件位置
-XX:HeapDumpPath 指定导出堆文件的文件路径
-verbose:class 跟踪类的加载和卸载
-XX:+PrintCommandLineFlags 打印虚拟机显式和隐式参数
-Xms 设置初始堆大小
-Xmx 设置最大堆大小
-Xmn 设置新生代大小
-XX:SurvivorRatio 设置eden/from的比例
-XX:MaxPermSize java7下设置永久代大小
-XX:MaxDirectMemorySize 设置直接内存大小
–server 设置为server模式
-XX:+UseConcMarkSweepGC 使用cms垃圾回收器
-XX:CMSInitiatingOccupanyFraction 出发老年代垃圾回收内存占比
-XX:MaxGCPauseMillis 预期gc停顿时间
-XX:+UseG1GC 使用G1垃圾回收器
9、(5分)简述说明如何写Java程序来证明STW(Stop-the-world)的这一特性?
答案:编写java程序,开启gclog,把jmx设置的尽量大一些(2G以上,为了让垃圾收集的时间长一些),选择串行垃圾回收器。
在java程序中运行两个线程:
线程1不断像HashMap中增加键值内容,直到老年代打满的时候进行map.clear()操作。
线程2设置每隔100ms像控制台输出时间戳日志。
当第线程1进行map.clear()后观察gclog,在gc日志中我们看到了fullgc日志,以及停顿时间。我们发现,这时候线程2的日志打印的时间戳间隔变长了,由100毫秒的停顿变成了几百毫秒的停顿,这个几百毫秒的停顿与gc日志中的fullgc停顿恰好相符,就是大名鼎鼎的Stop-the-world。在STW发生时,几乎所有线程挂起,程序像hung死一样。
10、(20分)上机题,打开vmware虚拟机,使用appuser用户登录, 密码: ********,执行脚本./TestStarter.sh
(1)解决启动时候的报错,请把调整后的JVM参数写到答题纸上。
(2)成功启动程序后,结合linux系统工具、JVM工具分析当前程序中的存在问题及可能的性能瓶颈。
答:一共5个问题,该题主要是考察结合工具分析jvm相关问题,综合应用top、vmstat、iostat、pidstat、jps、jstat、jinfo、jmap、jhat、jstack、jcmd、hprof、mat、arthas等工具发现相关性能问题。(该试题app后面放到github上)
问题1:栈空间不够。 //增加-xss大小让程序正常启动。
问题2:类加载器造成的死锁。 //使用jstack 观察线程wait
问题3:存在高cpu线程。 //arthas dashboard观察高cup线程,jad查看高cpu方法;或者使用查看比较高的线程id转换16进制之后再jstack导出的栈文件中查找导致cpu过高的线程。
问题4:存在大HashMap没有被正常垃圾回收引发fullgc的情况。//使用mat进行分析。
问题5:存在3个线程死锁问题。 // Jstack -l 查看死锁