z3
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
十二、JVM浅析
本来打算学习类加载的,但是类加载时发生了什么,加载后的class是什么形式存在?
所以打算先简单了解下jvm。
java语言
java是解释型语言还是编译型语言?
虽说java代码运行前需要编译,但编译后的结果是.class文件,里面是字节码,字节码需要在jvm中运行。而jvm依附于操作系统运行。编译型语言编译后得到的是机器码,将机器码写入内存,当前操作系统就可以直接调这些机器码运行。
所以java是解释型语言,由于字节码在虚拟机运行,所以会导致效率低。但是java在经过这么久的更新迭代,已经有了很多优化方案了。例如jit(运行时编译),aot(运行前编译)。
jvm意思是java的虚拟机,这个虚拟机的实现有很多种,最流行的就是HotSpot虚拟机,用了JIT及自适应优化技术。sun公司在jdk8后,默认的jvm就是HotSpot vm。
在运行java代码时可以指定以下参数,指定字节码的运行方式:
-XX:RewriteFrequentPairs 用于开启动态编译。
-Xint:禁用JIT编译,即禁用两个编译器,纯解释执行。
-Xcomp:纯编译执行,如果方法无法编译,则回退到解释执行模式解释无法编译的代码。
还想到一种优化方案:
如果将jvm做成物理机,那java就算编译型语言了(但是成本特别高,相同成本的虚拟机效率要远远高于物理机)。
JVM介绍
JVM组成
首先,JVM是指java虚拟机,而java虚拟机有很多种,最流行的就是HotSpot,JVM和HotSpot的区别就像 水果 与 苹果、香蕉、菠萝、哈密瓜 的区别
JVM有很多种,但它们都遵从JVM规范,以下学习的就是JVM规范的虚拟机结构,适用于所以JVM
JVM 整体组成可分为以下四个部分:
类加载器(ClassLoader)# 用于加载字节码
运行时数据区(Runtime Data Area)# 储存运行时产生的数据
执行引擎(Execution Engine)# 解析字节码,调用当前系统api去实现字节码的功能
本地库接口(Native Interface)# 调用其它语言的方法
而运行时数据区又可以分为以下几个部分:
程序计数器(Program Counter Register)# 标志着当前字节码执行到第几行(像汇编中的ip寄存器)
Java虚拟机栈(Java Virtual Machine Stacks)# 记录java方法调用顺序的栈
本地方法栈(Native Method Stack)# 记录native方法调用顺序的栈(JVM规范没对这里做要求,所以有的虚拟机直接将这部分合并到了Java虚拟机栈)
Java堆(Java Heap)# java虚拟机占用内存最大的一块,用来储存对象实例
方法区(Methed Area)# 用来储存被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据(常量池也在这里)
它们关系如图
图中提到了,运行时数据区在所有线程间共享、运行时数据区线程私有。
运行时数据区在所有线程间共享意思就是,无论创建多少线程,这些线程都储存到这个区。
运行时数据区线程私有就是,每创建一个线程,对应的这部分数据区也要创建,每一个线程都要有一个自己的数据区。
对应上图,绿色是线程们共用的,黄色的每个人都有一份自己的。
堆
堆和栈是无论什么语言都会用到的数据结构。java中的堆用于储存对象实例,栈用于储存局部变量和方法调用(就是前几篇,用idea调试的时候,看见的方法调用栈)。
当空间不足时会报错:
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.lang.OutOfMemoryError。
Java堆分为新生代和老年代,新生代可以分为Eden空间、From Survivor空间、To Survivor空间(这些空间了解下就行,分代用的)
如图
图中的GC 过程,是指垃圾回收过程,回收那些没人要的对象。GC过程有三种,Minor GC、Major GC 和 Full GC,分别对应年轻、老年、全部区域进行垃圾回收。这个过程还会对对象分代,例如xx对象存活时间久,那就扔进老年代区域。
为什么要分代?
上面图中提到了,GC过程耗时较长,要先扫描,再确定哪些回收,如果不分代,则每次GC都要扫描整个堆内存,所以很耗时。例如有个对象存在于堆内存中很久了,每次扫描都扫描到它,但它每次扫描都存活,那GC是不是应该“聪明点”,下次尽量不扫描它。
那怎么做?
划分一块内存区域,把这些钉子户,都放进这块内存,GC扫描内存时,不扫描这里,如果其它内存实在不能释放了,再扫描这块内存。
其实这就是分代,通过分代划分内存区域,不同代使用不同的回收机制,例如面上的回收机制就可以粗略的总结为年轻代进行GC频率高,老年代GC频率低,这样既保证了即时GC,又在GC过程中不需要扫描整个堆内存,提高了GC效率。
JVM如何加载类
java的类加载,有个大名鼎鼎的双亲委派机制。加载类需要用到类加载器,java语言支持下面4种类加载器:
Bootstrap ClassLoader 启动类加载器,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
Extention ClassLoader 标准扩展类加载器,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
Application ClassLoader 应用类加载器,主要负责加载当前应用的classpath下的所有类
User ClassLoader 用户自定义类加载器,用户自定义的类加载器,可加载指定路径的class文件
双亲委派机制就是下面这张图
这些ClassLoader加载class时,都会将加载类的请求向上委托,如果父加载器都不能加载,再自己加载,因为每个加载器各司其职,只会加载自己负责的区域的类。
在加载器加载类时,都会检查这个类是否加载过,避免重复加载,检查范围是当前加载器加载过的类。
所以在java中,类的唯一性不仅由类名,包名决定,还有它的类加载器,即,判断两个类是否相等,先判断包名,再判断类名,再判断类加载器。
所以可能会出现两个类名包名相同的类,但他们类加载器不同,则这两个类类型也不同,不能互相做转换,赋值等操作。
例:同一个java类由不同的classloader加载问题
类被加载后,会存到方法区,方法区内的数据,在进程结束前都不会被删掉,所以方法区也被叫做永久区。
加载类不等于初始化类,加载类只是将字节码存储到方法区,,只有触发类初始化条件(我将这些条件总结为当使用到类时)时才会初始化。初始化时会从上到下执行静态代码。
代码细节在下一篇类加载的利用中详细分析。
怎么看见JVM
JVM是java虚拟机,很像一个简陋的操作系统,它有自己的指令集(class文件就包含jvm指令)、自己的命令、环境变量等等。
平时运行java文件,运行jar包,jvm都是在背后偷偷运行的,让我们看起来,java和c语言一样,都是编译后直接运行。
JVM在运行java程序时创建,每个java程序创建,都会创建相应的JVM。
在配置好java环境变量后,打开cmd,执行jvisualvm就可以看见当前所有的java进程,查看进程运行状态,还可以安装btrace插件,在进程中植入代码。
运行jConsole就可以看到下面如图,不仅可以查看本地进程信息,还可以看远程java进程信息,和jVisualvm类似。
通过这些工具,可以实时监控java程序运行状态,甚至修改java进程。JVM还有自己的命令,如jps,jstat等等,可以通过这些命令,可以来查看,修改jvm信息,达到JVM调优的目的。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
