简单聊聊jvm-ag九游会j9官方网站

ag九游会j9官方网站-j9九游会登录入口首页新版
简单聊聊jvm
2023-05-28
21 浏览
江海入海,知识涌动,这是我参与江海计划的第9篇。
一、简单聊聊jvm
1.1先来看看简单的java程序
现在我有一个javabean:
plain text
复制代码
public class java3y {
// 姓名
private string name;
// 年龄
private int age;
//.....各种get/set方法/tostring
}
一个测试类:
plain text
复制代码
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文件得到结果

1.2编译过程
.java文件是由java源码编译器(上述所说的javac.exe)来完成,流程图如下所示:

java源码编译由以下三个过程组成:
分析和输入到符号表
注解处理
语义分析和生成class文件

1.2.1编译时期-语法糖
语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”。
最值得说明的就是泛型了,这个语法糖可以说我们是经常会使用到的!
泛型只会在java源码中存在,编译过后会被替换为原来的原生类型(raw type,也称为裸类型)了。这个过程也被称为:泛型擦除
有了泛型这颗语法糖以后:
代码更加简洁【不用强制转换】
程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现classcastexception异常】
可读性和稳定性【在编写集合的时候,就限定了类型】
了解泛型更多的知识:

1.3jvm实现跨平台
至此,我们通过javac.exe编译器编译我们的.java源代码文件生成出.class文件了!

这些.class文件很明显是不能直接运行的,它不像c语言(编译cpp后生成exe文件直接运行)
这些.class文件是交由jvm来解析运行
jvm是运行在操作系统之上的,每个操作系统的指令是不同的,而jdk是区分操作系统的,只要你的本地系统装了jdk,这个jdk就是能够和当前系统兼容的。
而class字节码运行在jvm之上,所以不用关心class字节码是在哪个操作系统编译的,只要符合jvm规范,那么,这个字节码文件就是可运行的。
所以java就做到了跨平台--->一次编译,到处运行!

1.4class文件和jvm的恩怨情仇
1.4.1类的加载时机
现在我们例子中生成的两个.class文件都会直接被加载到jvm中吗??
虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(class文件加载到jvm中):
创建类的实例(new 的方式)。访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法
反射的方式
初始化某个类的子类,则其父类也会被初始化
java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类(包含main方法的那个类)
当使用jdk1.7的动态语言支持时(....)
所以说:
java类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载。这当然就是为了节省内存开销
1.4.2如何将类加载到jvm
class文件是通过类的加载器装载到jvm中的!
java默认有三种类加载器

各个加载器的工作责任:
1)bootstrap classloader:负责加载$java_home中jre/lib/rt.jar里所有的class,由c 实现,不是classloader子类
2)extension classloader:负责加载java平台中扩展功能的一些jar包,包括$java_home中jre/lib/*.jar或-djava.ext.dirs指定目录下的jar包
3)app classloader:负责记载classpath中指定的jar包及目录中class
工作过程:
1、当appclassloader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器extclassloader去完成。
2、当extclassloader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给bootstrapclassloader去完成。
3、如果bootstrapclassloader加载失败(例如在$java_home/jre/lib里未查找到该class),会使用extclassloader来尝试加载;
4、若extclassloader也加载失败,则会使用appclassloader来加载
5、如果appclassloader也加载失败,则会报出异常classnotfoundexception
其实这就是所谓的双亲委派模型。简单来说:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上
好处:
防止内存中出现多份同样的字节码(安全性角度)
特别说明:
类加载器在成功加载某个类之后,会把得到的 java.lang.class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载
1.4.2类加载详细过程
加载器加载到jvm中,接下来其实又分了好几个步骤
加载,查找并加载类的二进制数据,在java堆中也创建一个java.lang.class类的对象
连接,连接又包含三块内容:验证、准备、初始化。    - 1)验证,文件格式、元数据、字节码、符号引用验证;    - 2)准备,为类的静态变量分配内存,并将其初始化为默认值;    - 3)解析,把类中的符号引用转换为直接引用
初始化,为类的静态变量赋予正确的初始值。

1.4.3jit即时编辑器
一般我们可能会想:jvm在加载了这些class文件以后,针对这些字节码,逐条取出,逐条执行-->解析器解析。
但如果是这样的话,那就太慢了!
我们的jvm是这样实现的:
就是把这些java字节码重新编译优化,生成机器码,让cpu直接执行。这样编出来的代码效率会更高。
编译也是要花费时间的,我们一般对热点代码做编译,非热点代码直接解析就好了。
热点代码解释:一、多次调用的方法。二、多次执行的循环体
使用热点探测来检测是否为热点代码,热点探测有两种方式:
采样
计数器
目前hotspot使用的是计数器的方式,它为每个方法准备了两类计数器:
方法调用计数器(invocation  counter)
回边计数器(back  edgecounter)。
在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发jit编译

1.4.4回到例子中
按我们程序来走,我们的java3ytest.class文件会被appclassloader加载器(因为extclassloader和bootstrap加载器都不会加载它[双亲委派模型])加载到jvm中。
随后发现了要使用java3y这个类,我们的java3y.class文件会被appclassloader加载器(因为extclassloader和bootstrap加载器都不会加载它[双亲委派模型])加载到jvm中

详情参考:
---浅解jvm加载class文件
---jvm杂谈之jit
扩展阅读:
---深入探讨 java 类加载器
---深入浅出 jit 编译器
---java 类加载器(classloader)的实际使用场景有哪些?
1.5类加载完以后jvm干了什么?
在类加载检查通过后,接下来虚拟机将为新生对象分配内存
1.5.1jvm的内存模型
首先我们来了解一下jvm的内存模型的怎么样的:
基于jdk1.8画的jvm的内存模型--->我画得比较

简单看了一下内存模型,简单看看每个区域究竟存储的是什么(干的是什么):
堆:存放对象实例,几乎所有的对象实例都在这里分配内存
虚拟机栈:虚拟机栈描述的是java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(stack frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息
本地方法栈:本地方法栈则是为虚拟机使用到的native方法服务
方法区:存储已被虚拟机加载的类元数据信息(元空间)
程序计数器:当前线程所执行的字节码的行号指示器
1.5.2例子中的流程

我来宏观简述一下我们的例子中的工作流程:
1、通过java.exe运行java3ytest.class,随后被加载到jvm中,元空间存储着类的信息(包括类的名称、方法信息、字段信息..)。
2、然后jvm找到java3ytest的主函数入口(main),为main函数创建栈帧,开始执行main函数
3、main函数的第一条命令是java3y java3y = new java3y();就是让jvm创建一个java3y对象,但是这时候方法区中没有java3y类的信息,所以jvm马上加载java3y类,把java3y类的类型信息放到方法区中(元空间)
4、加载完java3y类之后,java虚拟机做的第一件事情就是在堆区中为一个新的java3y实例分配内存, 然后调用构造函数初始化java3y实例,这个java3y实例持有着指向方法区的java3y类的类型信息(其中包含有方法表,java动态绑定的底层实现)的引用
5、当使用java3y.setname("java3y");的时候,jvm根据java3y引用找到java3y对象,然后根据java3y对象持有的引用定位到方法区中java3y类的类型信息的方法表,获得setname()函数的字节码的地址
6、为setname()函数创建栈帧,开始运行setname()函数
从微观上其实还做了很多东西,正如上面所说的类加载过程(加载-->连接(验证,准备,解析)-->初始化),在类加载完之后jvm为其分配内存(分配内存中也做了非常多的事)。由于这些步骤并不是一步一步往下走,会有很多的“混沌bootstrap”的过程,所以很难描述清楚。
扩展阅读(先有class对象还是先有object):
参考资料:
---java程序编译和运行的过程
---java jvm 运行机制及基本原理
1.6简单聊聊各种常量池
在写这篇文章的时候,原本以为我对string s = "aaa";类似这些题目已经是不成问题了,直到我遇到了string.intern()这样的方法与诸如string s1 = new string("1") new string("2");混合一起用的时候
我发现,我还是太年轻了。
首先我是先阅读了美团技术团队的这篇文章:---深入解析string#intern
嗯,然后就懵逼了。我摘抄一下他的例子:
打印结果是
jdk7,8下false true
调换一下位置后:
打印结果为:
jdk7,8下false false
文章中有很详细的解析,但我简单阅读了几次以后还是很懵逼。所以我知道了自己的知识点还存在漏洞,后面阅读了一下r大之前写过的文章:
---请别再拿“string s = new string("xyz");创建了多少个string实例”来面试了吧
看完了之后,就更加懵逼了。
后来,在zhihu上看到了这个回答:
---java 中new string("字面量") 中 "字面量" 是何时进入字符串常量池的?
结合网上资料和自己的思考,下面整理一下对常量池的理解~~
1.6.1各个常量池的情况
针对于jdk1.7之后:
常量池位于堆中
运行时常量池位于堆中
字符串常量池位于堆中
常量池存储的是:
字面量(literal):文本字符串等---->用双引号引起来的字符串字面量都会进这里面
符号引用(symbolic references)    - 类和接口的全限定名(full qualified name)    - 字段的名称和描述符(descriptor)    - 方法的名称和描述符
常量池(constant pool table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放--->来源:深入理解java虚拟机 jvm高级特性与最佳实践(第二版)
现在我们的运行时常量池只是换了一个位置(原本来方法区,现在在堆中),但可以明确的是:类加载后,常量池中的数据会在运行时常量池中存放
hotspot vm里,记录interned string的一个全局表叫做stringtable,它本质上就是个hashset。注意它只存储对java.lang.string实例的引用,而不存储string对象的内容
字符串常量池只存储引用,不存储内容
再来看一下我们的intern方法:
如果常量池中存在当前字符串,那么直接返回常量池中它的引用
如果常量池中没有此字符串, 会将此字符串引用保存到常量池中后, 再直接返回该字符串的引用
1.6.2解析题目
本来打算写注释的方式来解释的,但好像挺难说清楚的。我还是画图吧...
第一句:string s = new string("1");

第二句:s.intern();发现字符串常量池中已经存在"1"字符串对象,直接返回字符串常量池中对堆的引用(但没有接收)-->此时s引用还是指向着堆中的对象

第三句:string s2 = "1";发现字符串常量池已经保存了该对象的引用了,直接返回字符串常量池对堆中字符串的引用

很容易看到,两条引用是不一样的!所以返回false
第一句:string s3 = new string("1") new string("1");注意:此时"11"对象并没有在字符串常量池中保存引用

第二句:s3.intern();发现"11"对象并有在字符串常量池中,于是将"11"对象在字符串常量池中保存当前字符串的引用,并返回当前字符串的引用(但没有接收)

第三句:string s4 = "11";发现字符串常量池已经存在引用了,直接返回(拿到的也是与s3相同指向的引用)

根据上述所说的:最后会返回true~~~
如果还是不太清楚的同学,可以试着接收一下intern()方法的返回值,再看看上述的图,应该就可以理解了。
下面的就由各位来做做,看是不是掌握了:
还有:
1.7gc垃圾回收
可以说gc垃圾回收是jvm中一个非常重要的知识点,应该非常详细去讲解的。但在我学习的途中,我已经发现了有很好的文章去讲解垃圾回收的了。
所以,这里我只简单介绍一下垃圾回收的东西,详细的可以到下面的面试题中查阅和最后给出相关的资料阅
读吧~
1.7.1jvm垃圾回收简单介绍
在c 中,我们知道创建出的对象是需要手动去delete掉的。我们java程序运行在jvm中,jvm可以帮我们“自动”回收不需要的对象,对我们来说是十分方便的。
虽然说“自动”回收了我们不需要的对象,但如果我们想变强,就要变秃..不对,就要去了解一下它究竟是怎么干的,理论的知识有哪些。
首先,jvm回收的是垃圾,垃圾就是我们程序中已经是不需要的了。垃圾收集器在对堆进行回收前,第一件事情就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。判断哪些对象“死去”常用有两种方式:
引用计数法-->这种难以解决对象之间的循环引用的问题
可达性分析算法-->主流的jvm采用的是这种方式

现在已经可以判断哪些对象已经“死去”了,我们现在要对这些“死去”的对象进行回收,回收也有好几种算法:
标记-清除算法
复制算法
标记-整理算法
分代收集算法
(这些算法详情可看下面的面试题内容)~
无论是可达性分析算法,还是垃圾回收算法,jvm使用的都是准确式gc。jvm是使用一组称为oopmap的数据结构,来存储所有的对象引用(这样就不用遍历整个内存去查找了,空间换时间)。
并且不会将所有的指令都生成oopmap,只会在安全点上生成oopmap,在安全区域上开始gc。
在oopmap的协助下,hotspot可以快速且准确地完成gc roots枚举(可达性分析)。
上面所讲的垃圾收集算法只能算是方法论,落地实现的是垃圾收集器
serial收集器
parnew收集器
parallel scavenge收集器
serial old收集器
parallel old收集器
cms收集器
g1收集器
上面这些收集器大部分是可以互相组合使用
1.8jvm参数与调优
很多做过javaweb项目(ssh/ssm)这样的同学可能都会遇到过outofmemory这样的错误。一般解决起来也很方便,在启动的时候加个参数就行了。
上面也说了很多关于jvm的东西--->jvm对内存的划分啊,jvm各种的垃圾收集器啊。
内存的分配的大小啊,使用哪个收集器啊,这些都可以由我们根据需求,现实情况来指定的,这里就不详细说了,等真正用到的时候才回来填坑吧~~~~
avatarname
后发表内容
您的社区活跃积分 3,登录后即可领取  
网站地图