只有光头才能变强
JVM在准备面试的时候就有看了,一直没时间写笔记。现在到了一家公司实习,闲的时候就写写,刷刷JVM博客,刷刷电子书。
学习JVM的目的也很简单:
声明:全文默认指的是HotSpot VM
现在我有一个JavaBean:
public class Java3y {
// 姓名
private String name;
// 年龄
private int age;
//.....各种get/set方法/toString
}
一个测试类:
public class Java3yTest {
public static void main(String[] args) {
Java3y java3y = new Java3y();
java3y.setName("Java3y");
System.out.println(java3y);
}
}
我们在初学的时候肯定用过javac
来编译.java
文件代码,用过java
命令来执行编译后生成的.class
文件。
Java源文件:
在使用IDE点击运行的时候其实就是将这两个命令结合起来了(编译并运行),方便我们开发。
生成class文件
解析class文件得到结果
.java
文件是由Java源码编译器(上述所说的java.exe)来完成,流程图如下所示:
Java源码编译由以下三个过程组成:
语法糖可以看做是 编译器实现的一些“小把戏”,这些“小把戏”可能会使得 效率“大提升”。
最值得说明的就是泛型了,这个语法糖可以说我们是经常会使用到的!
有了泛型这颗语法糖以后:
了解泛型更多的知识:
至此,我们通过java.exe
编译器编译我们的.java
源代码文件生成出.class
文件了!
这些.class
文件很明显是不能直接运行的,它不像C语言(编译cpp后生成exe文件直接运行)
这些.class
文件是交由JVM来解析运行!
现在我们例子中生成的两个.class
文件都会直接被加载到JVM中吗??
虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(class文件加载到JVM中):
所以说:
class文件是通过类的加载器装载到jvm中的!
Java默认有三种类加载器:
各个加载器的工作责任:
工作过程:
其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。
好处:
特别说明:
java.lang.Class
类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。加载器加载到jvm中,接下来其实又分了好几个步骤:
- 1)验证,文件格式、元数据、字节码、符号引用验证;
- 2)准备,为类的静态变量分配内存,并将其初始化为默认值;
- 3)解析,把类中的符号引用转换为直接引用
一般我们可能会想:JVM在加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行-->解析器解析。
但如果是这样的话,那就太慢了!
我们的JVM是这样实现的:
热点代码解释:一、多次调用的方法。二、多次执行的循环体
使用热点探测来检测是否为热点代码,热点探测有两种方式:
目前HotSpot使用的是计数器的方式,它为每个方法准备了两类计数器:
按我们程序来走,我们的Java3yTest.class
文件会被AppClassLoader加载器(因为ExtClassLoader和BootStrap加载器都不会加载它[双亲委派模型])加载到JVM中。
随后发现了要使用Java3y这个类,我们的Java3y.class
文件会被AppClassLoader加载器(因为ExtClassLoader和BootStrap加载器都不会加载它[双亲委派模型])加载到JVM中
详情参考:
扩展阅读:
在类加载检查通过后,接下来虚拟机将为新生对象分配内存。
首先我们来了解一下JVM的内存模型的怎么样的:
简单看了一下内存模型,简单看看每个区域究竟存储的是什么(干的是什么):
我来宏观简述一下我们的例子中的工作流程:
java.exe
运行Java3yTest.class
,随后被加载到JVM中,元空间存储着类的信息(包括类的名称、方法信息、字段信息..)。Java3y java3y = new Java3y();
就是让JVM创建一个Java3y对象,但是这时候方法区中没有Java3y类的信息,所以JVM马上加载Java3y类,把Java3y类的类型信息放到方法区中(元空间)java3y.setName("Java3y");
的时候,JVM根据java3y引用找到Java3y对象,然后根据Java3y对象持有的引用定位到方法区中Java3y类的类型信息的方法表,获得setName()
函数的字节码的地址setName()
函数创建栈帧,开始运行setName()
函数从微观上其实还做了很多东西,正如上面所说的类加载过程(加载-->连接(验证,准备,解析)-->初始化),在类加载完之后jvm为其分配内存(分配内存中也做了非常多的事)。由于这些步骤并不是一步一步往下走,会有很多的“混沌bootstrap”的过程,所以很难描述清楚。
参考资料:
在写这篇文章的时候,原本以为我对String s = "aaa";
类似这些题目已经是不成问题了,直到我遇到了String.intern()
这样的方法与诸如String s1 = new String("1") + new String("2");
混合一起用的时候
首先我是先阅读了美团技术团队的这篇文章:https://tech.meituan.com/in_depth_understanding_string_intern.html---深入解析String#intern
嗯,然后就懵逼了。我摘抄一下他的例子:
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
}
打印结果是
调换一下位置后:
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
}
打印结果为:
文章中有很详细的解析,但我简单阅读了几次以后还是很懵逼。所以我知道了自己的知识点还存在漏洞,后面阅读了一下R大之前写过的文章:
看完了之后,就更加懵逼了。
后来,在zhihu上看到了这个回答:
结合网上资料和自己的思考,下面整理一下对常量池的理解~~
针对于jdk1.7之后:
常量池存储的是:
- 类和接口的全限定名(Full Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
常量池(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在 类加载后进入方法区的运行时常量池中存放--->来源:深入理解Java虚拟机 JVM高级特性与最佳实践(第二版)
现在我们的运行时常量池只是换了一个位置(原本来方法区,现在在堆中),但可以明确的是:类加载后,常量池中的数据会在运行时常量池中存放!
HotSpot VM里,记录interned string的一个全局表叫做StringTable,它本质上就是个HashSet。注意 它只存储对java.lang.String实例的引用,而不存储String对象的内容
字符串常量池只存储引用,不存储内容!
再来看一下我们的intern方法:
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
本来打算写注释的方式来解释的,但好像挺难说清楚的。我还是画图吧...
public static void main(String[] args) {
// 1.1在堆中创建"1"字符串对象
// 1.2字符串常量池引用"1"字符串对象
// 1.3s引用指向堆中"1"字符串对象
String s = new String("1");
// 2. 发现字符串常量池中已经存在"1"字符串对象,直接返回字符串常量池中对堆的引用(但没有接收)-->s引用还是指向着堆中的对象
s.intern();
// 3. 发现字符串常量池已经保存了该对象的引用了,直接返回字符串常量池对堆中字符串的引用
String s2 = "1";
// 4. s指向的是堆中对象的引用,s2指向的是在字符串常量池对堆中对象的引用
System.out.println(s == s2);// false
System.out.println("-----------关注公众号:Java3y-------------");
}
第一句:String s = new String("1");
第二句:s.intern();
发现字符串常量池中已经存在"1"字符串对象,直接返回字符串常量池中对堆的引用(但没有接收)-->此时s引用还是指向着堆中的对象
第三句:String s2 = "1";
发现字符串常量池已经保存了该对象的引用了,直接返回字符串常量池对堆中字符串的引用
很容易看到,两条引用是不一样的!所以返回false。
public static void main(String[] args) {
System.out.println("-----------关注公众号:Java3y-------------");
// 1. 在堆中首先创建了两个“1”对象
// 1.1 +号运算符解析成stringBuilder,最后toString(),最终在堆中创建出"11"对象
// 1.2 注意:此时"11"对象并没有在字符串常量池中保存引用
String s3 = new String("1") + new String("1");
// 2. 发现"11"对象并没有在字符串常量池中存在,于是将"11"对象在字符串常量池中保存当前字符串的引用,并返回当前字符串的引用
s3.intern();
// 3. 发现字符串常量池已经存在引用了,直接返回(拿到的也是与s3相同指向的引用)
String s4 = "11";
System.out.println(s3 == s4); // true
}
第一句:String s3 = new String("1") + new String("1");
在堆中首先创建了两个“1”对象。+
号运算符解析成stringBuilder,最后toString(),最终在堆中创建出"11"对象。注意:此时"11"对象并没有在字符串常量池中保存引用。
第二句:s3.intern();
发现"11"对象并没有在字符串常量池中,于是将"11"对象在字符串常量池中保存当前字符串的引用,并返回当前字符串的引用(但没有接收)
第三句:String s4 = "11";
发现字符串常量池已经存在引用了,直接返回(拿到的也是与s3相同指向的引用)
根据上述所说的:最后会返回true~~~
如果还是不太清楚的同学,可以试着接收一下intern()
方法的返回值,再看看上述的图,应该就可以理解了。
下面的就由各位来做做,看是不是掌握了:
public static void main(String[] args) {
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);//false
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);//false
}
还有:
public static void main(String[] args) {
String s1 = new String("he") + new String("llo");
String s2 = new String("h") + new String("ello");
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1 == s3);// true
System.out.println(s1 == s4);// true
}
可以说GC垃圾回收是JVM中一个非常重要的知识点,应该非常详细去讲解的。但在我学习的途中,我已经发现了有很好的文章去讲解垃圾回收的了。
所以,这里我只简单介绍一下垃圾回收的东西,详细的可以到下面的面试题中查阅和最后给出相关的资料阅
读吧~
在C++中,我们知道创建出的对象是需要手动去delete掉的。我们Java程序运行在JVM中,JVM可以帮我们“自动”回收不需要的对象,对我们来说是十分方便的。
虽然说“自动”回收了我们不需要的对象,但如果我们想变强,就要变秃..不对,就要去了解一下它究竟是怎么干的,理论的知识有哪些。
首先,JVM回收的是垃圾,垃圾就是我们程序中已经是不需要的了。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。判断哪些对象“死去”常用有两种方式:
现在已经可以判断哪些对象已经“死去”了,我们现在要对这些“死去”的对象进行回收,回收也有好几种算法:
(这些算法详情可看下面的面试题内容)~
无论是可达性分析算法,还是垃圾回收算法,JVM使用的都是准确式GC。JVM是使用一组称为OopMap的数据结构,来存储所有的对象引用(这样就不用遍历整个内存去查找了,时间换空间)。
并且不会将所有的指令都生成OopMap,只会在安全点上生成OopMap,在安全区域上开始GC。
上面所讲的垃圾收集算法只能算是方法论,落地实现的是垃圾收集器:
上面这些收集器大部分是可以互相组合使用的
很多做过JavaWeb项目(ssh/ssm)这样的同学可能都会遇到过OutOfMemory这样的错误。一般解决起来也很方便,在启动的时候加个参数就行了。
上面也说了很多关于JVM的东西--->JVM对内存的划分啊,JVM各种的垃圾收集器啊。
内存的分配的大小啊,使用哪个收集器啊,这些都可以由我们根据需求,现实情况来指定的,这里就不详细说了,等真正用到的时候才回来填坑吧~~~~
参考资料:
拿些常见的JVM面试题来做做,加深一下理解和查缺补漏:
题目来源:
根据 JVM 规范,JVM 内存共分为虚拟机栈、堆、方法区、程序计数器、本地方法栈五个部分。
具体可能会聊聊jdk1.7以前的PermGen(永久代),替换成Metaspace(元空间)
图片来源:https://blog.csdn.net/tophawk/article/details/78704074
参考资料:
内存泄漏的原因很简单:
常见的内存泄漏例子:
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工具查看线程状态
参考资料:
这题就依据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
参考资料:
检验一下是不是真懂了:
class Dervied extends Base {
private String name = "Java3y";
public Dervied() {
tellName();
printName();
}
public void tellName() {
System.out.println("Dervied tell name: " + name);
}
public void printName() {
System.out.println("Dervied print name: " + name);
}
public static void main(String[] args) {
new Dervied();
}
}
class Base {
private String name = "公众号";
public Base() {
tellName();
printName();
}
public void tellName() {
System.out.println("Base tell name: " + name);
}
public void printName() {
System.out.println("Base print name: " + name);
}
}
输出数据:
Dervied tell name: null
Dervied print name: null
Dervied tell name: Java3y
Dervied print name: Java3y
第一次做错的同学点个赞,加个关注不过分吧(hahaha
当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版本):
总的来说,JVM在初级的层面上还是偏理论多,可能要做具体的东西才会有更深的体会。这篇主要是入个门吧~
这篇文章懒懒散散也算把JVM比较重要的知识点理了一遍了,后面打算学学,写写SpringCloud的东西。
如果文章有错的地方欢迎指正,大家互相交流。习惯在微信看技术文章,想要获取更多的Java资源的同学,可以 关注微信公众号:Java技术zhai。为了大家方便,建了一下 qq群:650385180,大家也可以去交流交流。谢谢支持了!希望能多介绍给其他有需要的朋友