本文大量参考:https://www.cnblogs.com/lfs2640666960/p/9297176.html
1. 一次编译,到处运行
从软件架构的角度来看,如果需要实现不同平台的兼容性,有效办法便是加一个中间层进行封装,在Java世界中,JVM就是这个中间层。JVM屏蔽了平台间的差异,针对不同的操作系统开发了不同的本地方法栈,对上层暴露出同意的接口,使得在Windows系统编写的Java代码编译后能在linux系统上部署的JVM里正常运行,开发人员只需完成代码开发编译成.class文件的JVM字节码,即可实现一次编译,到处运行。
1.1 Java源码编译由以下三个过程组成:
class文件为8位字节码(ByteCode)组成,Java所有指令有200个左右,8位可以表示256种指令信息。前4个字节CAFE BABE为Gosling定义的一个魔法数,标志位一个Java类文件,缺失此魔法数则表示该文件不是一个Java文件或者文件已损坏。紧接着的4个字节为JDK的版本号,0x34为52,对应JDK1.8.0。
.class文件中的字节码
Java代码编译过程
语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”。
泛型是最常见的语法糖:
泛型的作用:
那么从.class文件到最终的代码执行,JVM到底进行了哪些工作,下面从代码的加载出发,详细描述。
2. JVM的加载class文件
2.1 JVM加载class文件的策略
虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(class文件加载到JVM中):
所以JVM是动态加载java类的:
2.2 JVM的类加载器
2.2.1 各个加载器的工作责任:
2.2.2 双亲委派模式
简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
双亲委派模式的优点:
特别说明:
java.lang.Class
类的实例缓存起来。下次再请求加载该类时,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。2.2.3 类加载详细过程
加载器加载到jvm中,接下来其实又分了好几个步骤:
1)验证,文件格式、元数据、字节码、符号引用验证;
2)准备,为类的静态变量分配内存,并将其初始化为默认值;
3)解析,把类中的符号引用转换为直接引用
2.3 class文件执行模式
字节码由类加载器加载到JVM环境后,有三种执行模式:
第一种:解释执行;
第二种:JIT编译执行;将代码转换成机器码,直接交给CPU执行,提高执行效率;
第三种:解释与JIT编译混合执行(主流JVM默认执行方式)。
由于混合执行在机器启动时以解释执行为主,执行效率会低于经过JIT动态编译热点代码的热机,冷机能承受负载要小于热机,在发布切流时需注意此差别可能造成冷机过载假死。
热点代码解释:一、多次调用的方法。二、多次执行的循环体
使用热点探测来检测是否为热点代码,热点探测有两种方式:
目前HotSpot使用的是计数器的方式,它为每个方法准备了两类计数器:
详情参考:
扩展阅读:
3 JVM内存模型
类加载进虚拟机后,会为新生对象分配内存。
3.1 Heap(堆区)
Heap是OOM(OutOfMemory)的主要发源地,它存储着几乎所有的实例对象,堆由垃圾收集器自动回收,被各子线程共享。
堆主要分为两大块:新生代和老年代。Eden为新对象的出生区,当Eden区填满时会出发YGC(Young Garbage Collection),将依然存活的对象送往Survivor区(S0|S1)。S0|S1在每次清理时会将存活对象整理到未使用的空间,然后清除当前使用的空间,可以减少内存碎片化。老年代于保存超过YGC次数阈值的对象以及超大对象。当老年代无法存放更多对象时会触发FGC(Full Garbage Collection),如果依然无法放下,则抛出OOM。
3.2 JVM Stack(虚拟机栈)
虚拟机栈是描述Java方法执行的内存区域,是线程私有的。线程是CPU执行任务的最小单位,任一时刻一个CPU内核只能运行一个线程的一条指令。方法的调用开始到执行完成的过程,就是栈桢从入栈到出栈的过程。在线程活动中,只有位于栈顶的桢才是有效的,称为当前帧,正在执行的方法称为当前方法,所有指令都只能对当前栈桢进行操作。StackOverflowError表示栈溢出,常出现在递归方法中。
3.3 Metaspace(元空间)
以Hotspot(JVM)为例,在JDK7及之前版本中有Perm(永久代)区,在启动时固定大小,难以调优。在某些动态加载类过多的场景,易发生Perm区的OOM。此外,永久代在垃圾回收过程中还存在诸多问题。所以,在JDK8中用元空间替代。
元空间不同于永久代,它在本地内存中分配。在JDK8中,Perm区的字符串常量移至堆内存,其他如类元信息、字段、静态属性、方法、常量等移至元空间。
常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放--->来源:深入理解Java虚拟机 JVM高级特性与最佳实践(第二版)
****注:关于String的加载过程有疑虑,需进一步了解。
HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet<String>。注意它只存储对java.lang.String实例的引用,而不存储String对象的内容
如下例中test2的结果不能详解。
@Test public void test1() { System.out.println("-----*******test1*********-----"); String s = new String("1"); s.intern(); String s1 = "1"; System.out.println(s == s1); // false String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4); // false } @Test public void test2() { System.out.println("-----*******test2*********-----"); String s1 = new String("he") + new String("llo"); String s2 = new String("h") + new String("ello"); String s5 = "hello"; String s3 = s1.intern(); String s4 = s2.intern(); System.out.println(s1 == s3); // false System.out.println(s1 == s4); // false System.out.println(s1 == s2); // false System.out.println(s1 == s5); // false System.out.println(s2 == s5); // false } @Test public void test2() { System.out.println("-----*******test2*********-----"); String s1 = new String("he") + new String("llo"); String s2 = new String("h") + new String("ello"); // String s5 = "hello"; String s3 = s1.intern(); String s4 = s2.intern(); System.out.println(s1 == s3); // true System.out.println(s1 == s4); // true System.out.println(s1 == s2); // false // System.out.println(s1 == s5); // false // System.out.println(s2 == s5); // false }
3.4 本地方法栈&程序计数器
本地方法栈主要为本地(Native)方法服务,本地方法通过JNI(Java Native Interface)来访问虚拟机运行时数据区,具有和JVM相同的权限和能力。一般不建议大量实现JNI,易丧失跨平台特性,影响稳定性。
程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或回复都要依赖程序计数器。
4. GC垃圾回收
4.1JVM垃圾回收简单介绍
在C++中,我们知道创建出的对象是需要手动去delete掉的。我们Java程序运行在JVM中,JVM可以帮我们“自动”回收不需要的对象。
首先,JVM回收的是垃圾,垃圾就是我们程序中已经是不需要的了。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。判断哪些对象“死去”常用有两种方式:
4.2 JVM垃圾回收算法
现在已经可以判断哪些对象已经“死去”了,我们现在要对这些“死去”的对象进行回收,回收也有好几种算法:
无论是可达性分析算法,还是垃圾回收算法,JVM使用的都是准确式GC。JVM是使用一组称为OopMap的数据结构,来存储所有的对象引用(这样就不用遍历整个内存去查找了,时间换空间)。
并且不会将所有的指令都生成OopMap,只会在安全点上生成OopMap,在安全区域上开始GC。
4.3 常用垃圾收集器
上面所讲的垃圾收集算法只能算是方法论,落地实现的是垃圾收集器:
上面这些收集器大部分是可以互相组合使用的。 垃圾收集器主要关注空间碎片和STW(Stop The World:STW执行时会暂停整个应用程序的执行)影响性能的问题。Hotspot新一代的垃圾回收器G1具备压缩功能,能避免碎片问题,且其暂停时间更加可控。
参考资料:
5. JVM面试题
5.1详细jvm内存模型
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
具体可能会聊聊jdk1.7以前的PermGen(永久代),替换成Metaspace(元空间)
5.2讲讲什么情况下回出现内存溢出,内存泄漏?
内存泄漏的原因很简单:
public static void main(String[] args) { Set set = new HashSet(); for (int i = 0; i < 10; i++) { Object object = new Object(); set.add(object); // 设置为空,这对象我不再用了 object = null; } // 但是set集合中还维护这obj的引用,gc不会回收object对象 System.out.println(set); }
解决这个内存泄漏问题也很简单,将set设置为null,那就可以避免上诉内存泄漏问题了。其他内存泄漏得一步一步分析了。
内存泄漏参考资料:
内存溢出的原因:
解决:
参考资料:
这里的线程栈应该指的是虚拟机栈吧...
JVM规范让每个Java线程拥有自己的独立的JVM栈,也就是Java方法的调用栈。
当方法调用的时候,会生成一个栈帧。栈帧是保存在虚拟机栈中的,栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息
线程运行过程中,只有一个栈帧是处于活跃状态,称为“当前活跃栈帧”,当前活动栈帧始终是虚拟机栈的栈顶元素。
通过jstack工具查看线程状态
参考资料:
5.5JVM 出现 fullGC 很频繁,怎么去线上排查问题
这题就依据full GC的触发条件来做:
- 所以看看是不是perm gen区的值设置得太小了。
System.gc()
方法的调用- 这个一般没人去调用吧~~~
- 是不是频繁创建了大对象(也有可能eden区设置过小)(大对象直接分配在老年代中,导致老年代空间不足--->从而频繁gc)
- 是不是老年代的空间设置过小了(Minor GC几个对象就大于老年代的剩余空间了)
双亲委托模型的重要用途是为了解决类载入过程中的安全性问题。
java.lang.Object
的类,想借此欺骗JVM。现在他要使用自定义ClassLoader
来加载自己编写的java.lang.Object
类。Bootstrap ClassLoader
的路径下找到java.lang.Object
类,并载入它Java的类加载是否一定遵循双亲委托模型?
- https://zhuanlan.zhihu.com/p/28909673
- https://www.cnblogs.com/huzi007/p/6679215.html
- https://blog.csdn.net/sigangjun/article/details/79071850
参考资料:
当young gen中的eden区分配满的时候触发MinorGC(新生代的空间不够放的时候).
这题不是很明白意思(水平有限...如果知道这题的意思可在评论区留言呀~~)
YGC和FGC是什么
什么时候执行YGC和FGC
System.gc()
,ygc时的悲观策略, dump live的内存信息时(jmap –dump:live),都会执行full gcGC最基础的算法有三种:
具体:
图来源于《深入理解Java虚拟机:JVM高级特效与最佳实现》,图中两个收集器之间有连线,说明它们可以配合使用.
stackoverflow错误主要出现:
permgen space错误(针对jdk之前1.7版本):
原文:https://www.cnblogs.com/beichenroot/p/11380674.html