JVM学习笔记

一. 走进Java
1. JavaEE方向的Java语言学习大致可分为三部分:
a) 支撑Java程序运行的虚拟机 b) 为各开发领域提供接口支持的java API c) Java语言及第三方Java框架(如Struts, Spring等)

2. 为什么必须掌握JVM?
一般情况下,程序员只要了解必要的Java API, Java语法, 并学会使用一些第三方的开发框架,就已经能基本满足日程开发的需要。那么为什么要深入学习JVM呢?在一些领域,如电商,对于程序的性能、稳定性、可扩展性等方面有着极高的要求。例如:一个程序在10个人同时使用的时候,能完全正常,但在10000人同时使用时,就会变慢。甚至产生死锁和崩溃。显然,产生问题是因为,要同时满足10000人需要更高性能的物理硬件,但在绝大多数情况下,简单地提升硬件并不能按比例地提升程序的性能和并发能力。
目前,商用的高性能的JVM都提供了相当多的优化性能和调节手段,用于满足应用程序在实际生产环境中对性能和稳定性的要求。
所以,如果用于生产环境,尤其是企业级应用开发中,就迫切需要开发人中至少有一部分人(架构师、系统调优师、高级程序员),对JVM的特性以及调优方法有着清晰的认识。

3. 怎样对待Java的第三方类库及框架?
Java语言不仅有一套完善的API, 还有无数的来自于商业机构和开源社区的第三方类库来帮程序员提供实现各种功能,这使得java开发的效率极大提升。作为好的Java程序员,不仅需要熟练使用这些第三方类库,还要去了解和思考,这些类库及框架是如何实现的。
在认清了这些技术的运作本质后,再去思考,自己的程序这样写到底是不是最好。
[b]学会第三方类库和框架的底层原理和实现本质--是步入高手境界的必经之路。[/b]

4. Java技术体系包含哪几部分?
1) java程序设计语言
2) 运行在各个硬件平台上的虚拟机
3) Java API类库
4) Class文件格式
5) 来自于商业机构和开源社区的第三方Java类库
我们把1),2),3)称为JDK, 把Java API中的Java SE和Java虚拟机称为JRE, JRE是支撑Java运行的标准环境。

二. Java内存区域
Java与C++之间有一堵由动态内存分配和垃圾回收技术围成的高墙,墙外面的人想进去,墙里面的人想出来。

Java内存运行时数据区包括:1)方法区 2)虚拟机栈 3)本地方法栈 4)堆 5)程序计数器

1. 程序计数器(Program Counter Register)
程序计数器是标识当前线程所执行的字节码的行号指示器。
字节码解释器工作的时候,就是通过这个计数器的值,来选取下一条需要执行的字节码指令。如分支、循环、跳转、异常处理、线程恢复等基础功能,都是依赖这个程序计数器来完成的。
由于各条线程之间的计数器互不影响,独立存储,所以程序计数器为“线程私有”的内存。
程序计数器是唯一一个不会OutOfMemory的区域。

2. Java虚拟机栈(Java Virtual Machine Stack)
Java虚拟机栈也是线程私有的,其生命周期与该线程相同。
虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候,都会同时创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用,直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

在初学者概念中,常把Java的内存区分为堆内存(Heap)和栈内存(Stack),这种划分时粗糙的,这里所说的栈,就是虚拟机栈,或者说虚拟机栈中的局部变量表部分。

局部变量表存放了编译器可知的各种基本数据类型和引用类型。其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot), 其余的数据类型只占用1个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要的帧中分配多大的局部变量空间是完全确定的,在方法的运行期间,不会改变局部变量表的大小。

在JVM规范中,对这个区域规定了两种异常:
1)如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
2)如果虚拟机栈可动态扩展(目前大部分JVM都可动态扩展,只是JVM规范中也允许固定长度的虚拟机栈),当扩展无法申请到足够的内存时,会抛出OutOfMemoryError异常。

3. 本地方法栈(Native Method Stack)
本地方法栈与虚拟机栈的作用非常类似,区别仅在于虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
注:Native Method就是一个java调用非java代码的接口。一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C。

4. Java堆(Java Heap)
Java堆为所有线程所共享,由虚拟机启动时创建。堆内存的唯一目的就是存放对象实例和数组。
Java堆是垃圾回收器管理的主要区域,也叫"GC"堆。
Java堆是物理上不连续的内存空间,只要逻辑上连续即可。在实现时,可以实现成固定大小的,也可以是可扩展的。
为了让GC更高效,JDK1.2开始对堆采用了分代管理方式:
1)新生代(New Generation):
大多数情况下,Java程序中新建的对象都从新生代分配内存。
2)旧生代(Old Generation):
a) 用于存放新生代中经过多次GC仍然存活的对象,如缓存对象
b) 新建的对象也可能在旧生代上直接分配内存:
(1) 大对象: 可在启动参数上设置:-XX:PretenureSizeThreshold=1024 来表示如果当前对象超过多大时就不再新生代上创建对象了。
(2) 大对象数组
c) 需要分配的内存大小超过了Eden Space时

如果堆中没有内存来完成对象实例分配,且堆也无法再扩展时,将会抛出OutOfMemoryError异常。

5. 方法区(Method Area)
方法区也是为所有线程所共享的。
方法区用于存储已被JVM加载的类信息(如类名、修饰符,成员变量与方法信息等)、final常量、静态变量、即时编译器编译后的代码等数据。
dev在调用class对象的getName, isinstanceof等方法来获取信息时,这些数据都来自方法区
方法区对应“持久代”(Permanet Generation), 默认最小值为16MB,最大值为64MB.
JVM规范中,可以选择不实现垃圾收集。
方法区也是不需要连续的内存,可以选择固定大小和可扩展。
相对来说,垃圾收集在方法区是很少出现的,这个区域的垃圾回收,主要目标就是针对常量池的回收,和对类型的卸载。

当方法区无法满足内存分配需求时,也会抛出OutOfMemoryError异常。

1) 运行时常量池(Runtime Constant Pool):从属于方法区
运行时常量池是方法区的一部分。
Class文件中除了有类的version、field、method、interface等描述信息外,还有常量池(Constant Pool Table), 用于存放编译期生成的各种字面量和符号引用。

除了程序计数器外,其他区域都可能发生OutOfMemoryError异常。

三. 垃圾回收机制
1. 既然Java实现了动态内存分配和自动垃圾回收机制,为什么还要学会GC机制呢?
答案很简单:若仅仅依靠JVM自动管理内存的分配与垃圾回收,虽降低了开发难度,但在不知不觉中浪费了内存,导致JVM要花费很多时间进行垃圾回收。若不清楚JVM的内存分配和回收机制,很可能会造成OutOfMemory. 因此,Java dev要清楚知道内存的分配和回收机制,及内存的使用状况,从而准确地判断程序的运行状况及进行性能调优。

2. 内存分配
Java对象所占用的内存主要从堆上进行分配,堆是所有线程共享的,因此在堆上分配内存时需要进行加锁。这导致new对象时开销比较大。当堆空间不足时,会出发GC,如果GC后空间仍然不足,则抛出OutOfMemory异常。

Sun JDK为了提升内存分配的效率,会为每个新创建的线程在新生代Eden Space(伊甸园)上分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行情况计算而得,默认大小为Eden Space的1%, JVM将根据该比率,线程数以及是否频繁分配对象来给每个线程分配合适大小的LTAB空间。因为LTAB不需要加锁,所以JVM给对象分配内存时,先在LTAB上分配,用完再在堆上分配。
所以在编写Java程序时,通常多个小对象比大的对象分配起来更高效。

3. 内存回收原理与算法
JVM通过GC来回收Heap和方法区中的内存。

GC的基本原理:首先找到不再被使用的对象,然后回收这些对象所占用的内存。

Sun JDK采用垃圾回收器采用了跟踪收集器的方式:全局记录数据的引用状态,在一定条件下被触发(如定时或者内存空间不足),执行时需要从根集合来扫描对象的引用关系,这可能造成应用程序暂停。
主要算法有:复制(Copying), 标记-清除(Mark-Sweep), 标记压缩(Mark-Compact)。

1)复制(Copying): ---- 新生代采用的方式
从根集合扫描出存活的对象,并将找到的存活对象复制到一块新的完全未使用的空间中。
当要回收空间中的存活对象较少时,复制算法比较高效,但其缺点是要增加一块空的内存空间及进行对象的移动。

2)标记-清除(Mark-Sweep):
从根集合开始扫描,对存活的对象进行标记,标记结束后,再扫描整个空间中未标记的对象,将其回收。
标记-清除动作不需要进行对象的移动,其仅对不存活的对象进行处理。当空间中存活对象较多的情况下较为高效,但该方式会造成内存碎片。

3)标记-压缩(Mark-Compact):
与标记-清除方式一直,只是在清除时不同:在回收不存活对象所占用的空间后,会将其他所有存活对象都往左端空闲的空间进行移动,并更新引用对其对象的指针。
缺点是要进行对象的移动,优点是不产生内存碎片。

4. JVM的GC方式
(一)新生代的GC(也叫monitor GC)
由于新生代的对象存活时间较短,所以存活的对象较少,故采用Copying算法。
Sun JDK提供了三种方式类回收新生代的内存:串行GC(Secial GC), 并行回收GC(Parallel Scanvenge), 并行GC(ParNew).

1)串行GC(Secial GC):client模式下默认的GC方式。串行GC在整个扫描和copy过程中采用单线程的方式,更适用于单CPU,新生代空间较小,对暂停时间要求不是非常高的应用。

2)并行回收GC(Parallel Scanvenge): 并行回收GC采用的也是copying算法,但其扫描和copy时采用多线程方式,且并行回收GC为大的新生代回收做了很多优化,因此适用于多CPU,对暂停时间要求比较短的应用上。

3)并行GC(ParNew): 并行GC与并行回收GC的区别在于:并行GC须配合旧生代CMS GC(并发GC)使用,CMS GC在进行旧生代GC时,如此时发生新生代GC,须进行相应的处理。在配置使用CMS GC的情况下,则新生代默认采用并行GC的方式。

(二)旧生代和持久代的GC
1) 串行GC: 单线程,采用Mark-Compact方式实现
2)并行GC: 多线程,采用Mark-Compact方式实现
3)并发GC(CMS: Concurrent Mark-Sweep): 多线程,采用Mark-Sweep方式
Mark-Sweep方式对整个空间中的对象进行扫描并标记,该过程会造成较长时间的应用暂停,但有些应用对响应时间有较高的要求。因此,Sun JDK提供了CMS GC, 其优势是CMS GC的大部分动作均与应用并发进行,这样大大缩短了GC造成的应用暂停时间。
GMS GC易产生内存碎片,CMS GC针对此问题,提供了碎片整理功能。
默认情况下,CMS GC并不开启。开通过配置参数开启。

(三)Full GC
除了CMS GC外,当旧生代和持久带触发GC时,其实是对新生代,旧生代,持久代都进行GC, 成为Full GC.
如下方式可以触发Full GC:
1) 程序调用System.gc();
2) 旧生代空间不足(新生代对象转入大对象,大对象数组时)
3) 持久代空间满(当系统中要加载的类,反射的类,调用的方法过多时)
4) CMS GC时出现promotion failed(新生代,旧生代都放不下)和concurrent mode failure(CMS GC时有对象要放入旧生代,但旧生代放不下)
5) 统计得到的Monitor GC晋升到旧生代的平均大小大于旧生代的剩余空间。

5. JVM调优
JVM调优主要是针对内存管理的调优,包括各个代的大小,GC策略等。
由于GC时会暂停应用线程,所以要根据应用情况的不同选择不同的内存管理策略,尤其是对于内存消耗较多的应用。

代大小的调优:通常新生代的GC会远快于Full GC. 所以各个代的大小设置直接决定了Monitor GC和Full GC的触发时机。

在JVM调优时需要考虑的问题:
1)避免新生代设置过小:
新生代过小会造成:a) Monitor GC频繁; b) 新生代对象进入旧生代,造成Full GC
2)避免新生代设置过大:
新生代过大会造成:a) Full GC频繁; b) Monitor GC耗时大幅度增加
3)避免survivor space(旧生代)过大或过小:
默认值为Eden:s0:s1 = 8:1:1
4) 合理设置新生代的存活周期
默认为超过15次(第16次)Monitor GC后,仍存活的新生代进入旧生代。

四. Java代码的执行过程
1. Java编译器如何把java代码编译成 .class文件?
1)语法分析,并输入到符号表
2)annotation处理(如 @Override 即属于annotation)
3)语义分析和生成class文件

2. Java类的加载机制(即 .class 文件是如何被加载到JVM的)?
1)装载(Load):由ClassLoader及其子类来完成,将二进制字节码加载到JVM中。
2)链接(Link):对二进制字节码的格式进行校验,初始化装载的类中的静态变量,解析类中调用的interface, class.
3)初始化(Initialize):执行类中的静态初始化代码,构造器代码,静态属性的初始化。
以下情形初始化会被触发:
a)调用了new
b)反射调用了类中的方法
c)子类调用了初始化
d)JVM启动过程中制定了初始化类

3. JVM对类的执行机制:
有以下三种方式执行:
1) 解释执行:当java代码被编译成.class文件后,由JVM在运行时对其进行解释并执行。
2)编译执行:解释执行的效率较低,为了提升代码的执行性能,Sun JDK支持将字节码编译为机器码(编译在运行时进行)。Sun JDK在执行过程中对执行频率高的代码进行编译,对执行不频繁的代码则继续采用解释执行的方式。
3)反射执行:反射执行可以动态实例化对象。
反射虽然提升了代码编写的灵活性,但比直接编译字节码复杂很多,所以性能也相对较差,但JDK6之后两者性能就差距不大了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值