深入理解Java虚拟机超详细笔记1

JVM笔记_1

5年码农一枚,一直在传统行业,现在的工作轻松却无趣,打算给自己3个月时间年前换个有挑战性的工作。之前工作中没有太注重理论知识的学习,对新技术也没有深入了解。以此为界,从《深入理解Java虚拟机》开始,以换高薪工作为目的,将自己这段时间所学记录下来,作为一个总结。这里会把书中知识点内容详实记录下来,方便以后查看,对想读此书却迟迟拿不起书的童鞋也可以通过此快速了解书中内容且不会有大的遗漏。好了,备战,学习结果如何,3个月后见分晓。越努力越幸运,加油!

第一章 走近Java

这一部分主要介绍了Java发展史和JDK编译。Java发展史了解一下就好,况且读完了真心没记住啥,还是从后面干货开始走起。至于JDK编译,是否按书上实操不影响后面的阅读(书中介绍了Linux和Mac下的编译,Windows附录中有但可能需要自己研究一下),本人是个懒人,实操暂且跳过了,等通读完本书需要读JDK源码时再去搞。

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

1. 内存区域

    Java虚拟机执行Java程序过程中会把它管理的内存划分为若干不同的数据区域。
《深入理解Java虚拟机超详细笔记1》

图1. Java虚拟机运行时数据区

程序计数器

  • 线程私有。各条线程之间计数器互不影响,独立存储。
  • 当前线程所执行的字节码行号指示器。字节码解释器工作时通过改变这个计数器值选取下一条需要执行的字节码指令(分支、循环、跳转、异常处理都需要依赖此计数器)。
  • 多线程运行时通过此计数器在线程切换后恢复正确执行位置。

Java 虚拟机栈

  • 线程私有。
  • 方法执行的内存模型。
    方法执行时创建栈帧(局部变量表、操作数栈、动态链接、方法出口等)。
  • 局部变量表存放编译期可知的基本数据类型、对象引用和returnAddress类型。其中long, double占用2个局部变量空间(slot),其余占用1个slot。
  • 局部变量所需内存空间编译期完成分配,运行期不改变其大小。

本地方法栈

  • 与虚拟机栈类似,为Native方法服务。

Java 堆

  • 所有线程共享,虚拟机启动时创建。
  • 堆上分配(对象实例及数组)。
    JIT编译期发展 + 逃逸分析技术 ——> 栈上分配、标量替换(后面章节会讲到,只需知道所有对象在堆上分配非绝对)。
  • 又称GC堆,垃圾收集管理的主要区域。

方法区(非堆 Non-Heap)

  • 所有线程共享。
  • 类信息、常量、静态变量、JIT编译后的代码等。
  • 此区域垃圾收集:常量池回收和类型的卸载。(HotSpot虚拟机中为永久代)

运行时常量池

  • 方法区的一部分。
  • Class文件中除类的版本、字段、方法、接口等描述信息外,还有一项是常量池。
    存放编译期生成的各种字面量和符号引用。这部分内容将在类加载后进入方法区的运行时常量池中存放。
  • 除符号引用,还会把翻译出来的直接引用存入运行时常量池中。
  • 动态性。运行期也可放入新的常量。eg. String.intern()方法。

直接内存

  • NIO:可以使用Native函数库直接分配堆外内存。避免Java堆和Native堆来回复制数据。

2. HotSpot虚拟机对象

创建对象

创建对象
查找
false
true


invokespecial指令 new 类的符号引用(常量池)

(检查符号引用代表的类是否

已被加载、解析和初始化) 类加载 分配内存(所需内存、类加载后可完全确定)

1. Java堆规整,只需移动指针:

    指针碰撞(Bump the Pointer)

    Serial, ParNew等带压缩功能的垃圾收集器

2. 不规整,维护列表记录哪些内存块可用:

    空闲列表(Free List);  CMS等基于Mark-Sweep垃圾收集

问题:非线程安全

eg. 给对象A分配内存,指针没来得及修改,对象B使用原指针分配内存

1) 同步分配内存空间的动作: CAS + 失败重试(原子性)

2) 本地线程分配缓冲(TLAB): 每个线程在Java堆中预先分配一块内存分

    配动作按照线程划分在不同空间。TLAB用完分配新的TLAB时同步锁定。 将分配到的内存空间初始化为零值(不包括对象头)/

使用TLAB时,提前至TLAB分配时进行 对象设置

(类信息、对象哈希码、对象GC分代年龄等信息。存放于对象头。) 执行<init>方法 代码清单1. HotSpot解释器的代码片段

//确保常量池中存放的是已解释的类
if(!constants->tag_at(index) .is_unresolved_klass()) {
	//断言确保是klassOop和instanceKlassOop(这部分下一节介绍)
	oop entry=(klassOop) *constants->obj_at_addr(index) ;
	assert(entry->is_klass(), "Should be resolved klass") ;
	klassOop k_entry=(klassOop) entry;
	assert(k_entry->klass_part()->oop_is_instance(), "Should be instanceKlass") ;
	instanceKlass * ik=(instanceKlass*) k_entry->klass_part();
	//确保对象所属类型已经经过初始化阶段
	if(ik->is_initialized()&&ik->can_be_fastpath_allocated()){
		//取对象长度
		size_t obj_size=ik->size_helper();
		oop result=NULL;
		//记录是否需要将对象所有字段置零值
		bool need_zero=!ZeroTLAB;
		//是否在TLAB中分配对象
		if(UseTLAB) {
			result=(oop) THREAD->tlab().allocate(obj_size) ;
		}
		if(result==NULL) {
			need_zero=true;
			//直接在eden中分配对象
			retry:
			HeapWord * compare_to=*Universe:heap()->top_addr();
			HeapWord * new_top=compare_to+obj_size;
			/*cmpxchg是x86中的CAS指令, 这里是一个C++方法, 通过CAS方式分配空间, 如果并发失败,转到retry中重试, 直至成功分配为止*/
			if(new_top<=*Universe:heap()->end_addr()) {
				if(Atomic:cmpxchg_ptr(new_top,Universe:heap()->top_addr(), compare_to) !=compare_to) {
					goto retry;
				}
				result=(oop) compare_to;
			}
		}
		if(result!=NULL) {
			//如果需要, 则为对象初始化零值
			if(need_zero) {
				HeapWord * to_zero=(HeapWord*) result+sizeof(oopDesc) /oopSize;
				obj_size-=sizeof(oopDesc) /oopSize;
				if(obj_size>0) {
					memset(to_zero, 0, obj_size * HeapWordSize) ;
				}
			}
			//根据是否启用偏向锁来设置对象头信息
			if(UseBiasedLocking) {
				result->set_mark(ik->prototype_header()) ;
			}else{
				result->set_mark(markOopDesc:prototype()) ;
			}
			result->set_klass_gap(0) ;
			result->set_klass(k_entry) ;
			//将对象引用入栈, 继续执行下一条指令
			SET_STACK_OBJECT(result, 0) ;
			UPDATE_PC_AND_TOS_AND_CONTINUE(3, 1) ;
		}
	}
}

对象的内存布局
      3部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

  • 对象头
    – 第一部分,存储对象自身的运行时数据。(如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等)。这部分数据长度在32位和64位虚拟机中分别为32位和64位,官方称为”Mark Word”。
    – 第二部分,类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例(数组还有一块记录数组长度)。
    由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间,32位虚拟机的markOop实现如下:

代码清单2. markOop.cpp片段

//Bit-format of an object header(most significant first,big endian layout below) :
//32 bits:
//--------
//hash:25------------>| age:4    biased_lock:1 lock:2(normal object)
//JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2(biased object)
//size:32------------------------------------------>|(CMS free block)
//PromotedObject*:29---------->| promo_bits:3----->|(CMS promoted object)
  • 实例数据:对象真正存储的有效信息。
  • 对齐填充:仅起占位符的作用。HotSpot VM要求对象起始地址必须是8字节整数倍,换句话说,就是对象的大小必须是8字节的整数倍。当实例数据部分没有对齐时,通过对齐填充来补全。

对象的访问定位

  • 使用句柄。
    对象移动时只改变句柄中实例指针,reference不需要修改。
    《深入理解Java虚拟机超详细笔记1》

图2. 通过句柄访问对象

  • 使用直接指针。
    速度快(节省一次指针定位的时间开销)。HotSpot使用此种方式。
    《深入理解Java虚拟机超详细笔记1》

图3. 通过直接指针访问对象

3. 实战:OutOfMemoryError异常

     这部分还是照着书本敲吧。代码不多,跟着敲一遍会对各种虚拟机启动参数有一个认识,对各种OOM有个了解。理论与实战结合才能更好的理解虚拟机,这里一定要动动手实践一下。具体代码就不一一贴出来了。
1)Java 堆溢出
     -XX:+HeapDumpOnOutOfMemoryError: 出现OOM时Dump堆转储快照。
     -Xms20m -Xmx20m 最小堆 最大堆20M
2)虚拟机栈和本地方法栈溢出
     -Xss参数:设置栈容量。
     StackOverflowError: 单线程时,栈帧太大 or 虚拟机栈容量太小。
     如果是建立过多线程导致内存溢出,在不能减少线程数或更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。
3)方法区和运行时常量池溢出(PermGen Space)
     JDK1.6前,运行时常量池分配在永久代中,-XX:PermSize和-XX:MaxPermSize限制方法区大小,间接限制其中常量池容量。
     JDK1.7,String.intern() 方法测试结果与JDK1.6不同。P56~P57书中例子
     产生大量类填满方法区,导致溢出。CGLib、JSP、OSGi等。
4)本机直接内存溢出
     使用NIO,可导致本机直接内存溢出。

参考资料:

  《深入理解Java虚拟机》
    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/gangtou07/article/details/82775753
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞