《深入理解Java虚拟机》——阅读笔记(一)——20201102~1104

本文是《深入理解Java虚拟机》的阅读笔记,涵盖了Java技术体系、发展历程和未来展望,重点讨论了Java内存区域,包括程序计数器、虚拟机栈、本地方法栈、Java堆、方法区和直接内存,以及内存溢出异常和垃圾收集器的工作原理,如对象分配策略、垃圾收集算法和内存回收策略。
摘要由CSDN通过智能技术生成

走进java

1.1 概述

java优点:

  • 拜托了硬件平台的束缚,实现了“一次编写,到处运行”的理想
  • 提供了一种相对安全的内存管理和访问机制,避免了绝大部分的内存泄漏和指针越界问题
  • 实现了热点代码检测和运行时编译及优化,使得java应用能随着运行时间的增加而获得更高的性能
  • 有一套完善的应用程序接口,还有无数的来自商业机构和开源社区的第三方类库来帮助实现各种各样的功能
  • ……

1.2 java技术体系

传统意义上,Sun官方所定义的Java技术体系包含了:

  • Java程序设计语言
  • 各种硬件平台上的Java虚拟机
  • Class文件格式
  • Java API类库
  • 来自商业机构和开源社区的第三方Java类库

我们可以把Java程序设计语言、Java虚拟机、Java API类库这三部分统称为JDK,JDK是支持Java程序开发的最小环境。

可以把Java API类库中的Java SE API子集和Java虚拟机这两部分统称为JRE,JRE是支持Java程序运行的标准环境。
Java技术体系所包含的内容

按照技术所服务的领域来划分,或者说按照Java技术关注的重点业务领域来划分,Java技术体系可以分为四个平台:

  • Java Card:支持一些Java小程序(Applets)运行在小内存设备(如智能卡)上的平台。
  • Java ME:移动终端(手机、PDA)上的平台,对Java API有所精简,并加入了针对移动终端的支持,以前叫J2ME。
  • Java SE:支持面向桌面级应用(如Windows下的应用程序)的平台,提供了完整的Java 核心API,以前叫J2SE。
  • Java EE:支持使用多层架构的企业应用(如ERP、CRM)的Java平台,除了提供Java SE API外,还对其做了大量的扩充并提供了相关的部署支持,以前叫J2EE。

1.3 Java发展史

Java技术发展的时间线

1.4 展望Java技术的未来

  • 模块化
  • 混合语言
    常见语言的jvm实现版本
  • 多核并行
    • 1.7加入Fork/Join模式
  • 进一步丰富语法
  • 64位虚拟机

1.5 实战:自己编译JDK(略)

  1. 获取JDK源码
  2. 系统需求
  3. 构建编译环境
    1. 安装CYGWIN:是在Windows平台下模拟Linux运行环境
    2. 安装编译器
    3. 下载一个已经编译好的jdk
    4. 下载一个Apache ANT
  4. 准备依赖项
    1. JDK Plug
    2. JDK 运行时包
    3. FreeType(字体渲染库)
    4. 下载Microsoft directX 9.0 SDK
    5. 寻找“MSVCR 100.DLL”动态链接库
  5. 进行编译

自动内存管理机制(2~5章)

第2章 Java内存区域与内存溢出异常

运行时数据区域

程序计数器(线程私有):

  • 字节码解释权工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是native方法,这个计数器的值则为空(undefined)。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OOME情况的区域。

Java虚拟机栈(线程私有):

  • 每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
  • 局部变量表中存放了编译期可知的各种基本数据类型、对象引用(reference类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其它与此对象相关的位置)和returnAddress类型(指向了一条字节码指令的地址)。
  • 其中64位长度的long和double类型的数据会占用2个slot,其它只占用1个。
  • 局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
  • 在Java虚拟机规范中,对这个区域规定了两种异常状况
    • 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),当扩展时无法申请到足够的内存时会抛出OutOfMemoryError异常。

本地方法栈(线程私有)
与虚拟机栈类似。有的虚拟机(譬如Sun HotSpot)直接将本地方法栈和虚拟机栈合二为一。

Java堆(线程共享)

  • 对于大多数应用来说,Java堆是Java虚拟机管理的内存中最大的一块。
  • Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。
  • 此内存区域的唯一目的是存放对象实例,几乎所有的对象都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配,但是随着JIT编译器的发展和逃逸分析技术的逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化发生,所有的对象都分配在堆上也渐渐变得不是那么绝对了。
  • 线程共享的Java堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
  • Java堆可以处于物理上不连续的内存空间中,只要逻辑上是连续的即可。在实现时,既可以实现成固定大小,也可以是 可扩展的,不过当前主流的虚拟机都是按照可扩展来实现的。如果在堆中没有内存完成实例分配,且堆也无法再扩展时,会抛出OutOfMemoryError异常。

方法区(线程共享)

  • 存储已被虚拟机加载的类信息、常量、静态变量、即使编译器编译后的代码等数据。
  • 这个区域的回收目标主要是针对常量池的回收和对类型的卸载。
  • 当方法区无法满足内存分配需求时,将抛出OOM

运行时常量池(线程共享)

  • 运行时常量池是方法区的一部分。
  • class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池,用于存放编译期生成的各种字面量和符号引用,这部分内容在类加载后存放到方法区的运行时常量池中。
  • Java虚拟机堆class文件中的每一部分(自然也包括常量池)的格式都有严格的规定,每一个字节用于存储哪种数据都必须符合规范上的要求,这样才会被虚拟机认可、装载和执行。但对于运行时常量池,Java虚拟机规范没有做任何细节的要求,不同的提供商实现的虚拟机可以按照自己的要求实现这个区域。不过,一般来说,除了保存class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
  • 运行时常量池相对于class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只能在编译期产生,也就是并非预置入class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可将新的常量放入池中,这种特性被利用的 比较多的便是String类的intern方法。

直接内存

  • NIO中,引入了一种基于通道与缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
  • 显然,本机直接分配的内存不会收到Java堆的限制,但会受到本机总内存的大小及处理器寻址空间的限制。服务器管理员配置虚拟机参数时,一般会根据实际内存设置-Xmx等参数信息,但经常会忽略掉直接内存,使得各个内存区域的总和大于物理内存限制(包括物理上的和操作系统级的限制),从而导致动态扩展时出现OOM异常。

对象访问

随记—20200519—对象的实例化内存布局与访问定位

Object obj = new Object();
  • Object obj 的语义会反映到Java栈的本地变量表中,作为一个reference类型数据出现。
  • new Object() 将会反映到Java堆中,形成一块存储了Object类型所有实例数据值的结构化内存,根据具体类型以及虚拟机实现的对象内存布局的不同,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。
  • 对象访问方式的实现主要有两种:
    • 句柄
    • 直接指针(hotspot)

实战:OutOfMemoryError异常

Java堆溢出

-Xms和-Xmx设置堆大小为20M,不可扩展

-XX:+HeapDumpOnOutOfMemoryError 可以使虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照以便事后分析。

static class OOMObject{
}
psvm{
	List<OOMObject> list = new ArrayList<>();
	//不断分配对象直到OOM
	while(true){
		list.add(new OOMObject());
	}

虚拟机栈和本地方法栈溢出

-Xss参数减少栈内存容量。结果:抛出StackOverflowError

public class JavaVMStackSOF{
	private int stackLength=1;
	public void stackLeak(){
		stackLength++;
		stackLeak();
	}
	psvm{
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try{
			oom.stackLeak();
		}catch(Throwable e){
			sout(stackLength);
			throw e;
		}
	}
  • 实验结果表明:在单个线程下,无论是由于栈帧太大,还是虚拟机栈容量太小,当内存无法分配时,抛出的都是StackOverflowError
  • 如果不限于单线程,通过不断建立线程的方式可以产生OOM。但是,这样产生的内存溢出与栈空间是否足够大并不存在任何联系,或者准确的说,在这种情况下,给每个线程的栈分配的内存越大,反而越容易产生内存溢出异常。
  • 操作系统分配给每个进程的内存是有限制的。每个线程分配到的栈容量越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽。
  • 在开发的时候,如果使用虚拟机默认参数,栈深度在大多数情况下达到1000~2000完全没有问题,对于正常的方法调用,这个深度应该完全够用了。但是,如果是建立过多线程导致的内存溢出,在不能减少线程数或更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。如果没有这方面的经验,这种通过“减少内存”的手段来解决内存溢出的方式会比较难以想到。

运行时常量池溢出

-XX:PermSize和 -XX:MaxPermSize限制方法区大小,从而间接控制其中常量池容量。

测试(代码略,往list里加String.valueOf(i++).intern())

方法区溢出

测试:运行时产生大量的类区填满方法区,直到溢出。借助CGLib直接操作字节码运行时,产生大量的动态类。

在经常动态生成大量Class的应用中,需要注意类的回收状态。这类场景除了CGLib字节码增强外,还有大量JSP或动态产生JSP文件的应用、基于OSGi的应用等。

本机直接内存溢出

可通过-XX:MaxDirectMemorySize指定。如果不指定,则默认与Java堆的最大值一样。

测试:Unsafe.allocateMemory(1024*1024) 分配1MB内存,不断申请。

第3章 垃圾收集器与内存分配策略

概述

对象已死

引用计数算法
根搜索算法

finalize()方法不是C/C++的折构函数,而是Java刚诞生时为了让C/C++程序员更容易接受它所做出的一个妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。有些教材中提到它适合做“关闭外部资源”之类的工作,这完全是对这种方法的用途的一种自我安慰。finalize()能做的工作,使用try-finally或其他方式都可做的更好、更及时,大家完全可以忘掉Java语言中还有这个方法的存在。

垃圾收集算法

垃圾收集器

内存分配与回收策略

1. 对象优先在Eden分配

2. 大对象直接进入老年代

-XX:PewtenureSizeThreshold参数,令大于这个设置值的对象直接在老年代中分配。

3. 长期存活的对象将进入老年代

-XX:MaxTenuringThreshold设置年龄阈值

4. 动态对象年龄判定

如果在Survivor区中,相同年龄的所有对象大小的综合大于Survivor区空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到MaxTenuringThreshold 中要求的年龄。

5. 空间分配担保

  • 在发生Minor GC时,虚拟机会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,如果大于,则改为直接进行一次Full GC。如果小于,则查看HandlePromotionFailure设置是否允许担保失败;如果允许,则只会进行Minor GC;如果不允许,则也要改为进行一次Full GC。
  • 新生代使用复制算法,为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端就是内存回收后新生代所有对象都存活),就需要老年代进行分配担保,让survivor区无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样担保的前提是,老年代本身还有容纳这些对象的空间,一共有多少对象会活下来,在实际完成垃圾回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。
  • 取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存货后的对象突增,远远高于平均值的话,依然会导致担保失败。如果出现了担保失败HandlePromotionFailure,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。

第4章

第5章

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值