初探JVM

此文章属于个人阅读深入理解java虚拟机的总结记录,如有错误望提出。

JVM基本概念:

JVM是可运行Java代码的假想计算机,包括一套字节码指令集、一组寄存器、一个栈、一个垃圾回收、堆和一个存储方法域。JVM是运行在操作系统之上的,他与硬件没有直接的交互

1、jvm布局:

jdk1.6版本JVM布局分为:heap(堆),method(方法区),stack(虚拟机栈),native stack(本地方法栈),程序计数器共五大区域。
其中方法区包含运行时常量池。堆和方法区是线程共享的,虚拟机栈和本地方法栈、程序计数器是随线程而建的。

1.1、堆:储存对象信息和数组。对象信息/数组包括对象头,实例数据和对齐填充共三个区域;

1.1.1、对象头包括二/三部分内容:

一是类型指针,即对象指向它的类元数据的指针,通过这个指针来确定那个类的实例(指向方法区储存的对象类型数据);

二是用于存储对象自身的运行时数据,如哈希码,gc分代年龄,锁状态标志,线程持有的锁,偏向线程id,偏向时间戳等(ps:不了解这些,后续学习);

三是如果存储的是数组,对象头除了包含以上俩个内容外还必须得有数组的长度数据。
其中类型指针还有一个知识点是对象的访问定位,对象的访问方式主流的有俩种,句柄和直接指针。这俩种区别与特点是:
①、在对象被移动时(垃圾收集时会移动对象即内存整理),句柄只需修改句柄中的对象实例数据的指针,栈中的reference(引用)不需要修改。而直接指针需要修改。
②、直接指针的优势就是速度更快,因为少了一次指针定位的时间开销。

1.1.2、实例数据包括实例化对象存储的数据。

1.1.3、对齐填充:对齐填充并不是必然存在的,也没有特殊含义。由于hotspot甲鱼的臀部(规定)对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时就需要通过对齐填充来补全。

1.1.4、对象创建分配内存有俩种方式:指针碰撞(无内存泄漏)和空间列表(内存不连续,分配时找到一块足够大的内存划分给对象实例);选择哪种方式创建由垃圾收集器是否有压缩整理功能决定。

Java堆是被所有线程共享的一块内存区域,主要用于存放对象实例,为对象分配内存就是把一块大小确定的内存从堆内存中划分出来,通常有指针碰撞和空闲列表两种实现方式。

1.1.4.1.指针碰撞法
假设Java堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的GC收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。

1.1.4.2.空闲列表法
事实上,Java堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。

内存分配并发问题

在创建对象的时候有一个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采用两种方式来保证线程安全:

  • CAS: CAS 是乐观锁的一种实现方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性。

  • TLAB: 为每一个线程预先分配一块内存,JVM在给线程中的对象分配内存时,首先在TLAB分配,当对象大于TLAB中的剩余内存或TLAB的内存已用尽时,再采用上述的CAS进行内存分配。

如果想多了,创建一个对象还是挺麻烦的,需要这么多步骤,那么我们在开发过程中尽量非必须的对象创建呢?

创建对象有以下几个要点:

  1. 1).类加载机制检查:JVM首先检查一个new指令的参数是否能在常量池中定位到一个符号引用,并且检查该符号引用代表的类是否已被加载、解析和初始化过

  2. 2).分配内存:把一块儿确定大小的内存从Java堆中划分出来

  3. 3).初始化零值:对象的实例字段不需要赋初始值也可以直接使用其默认零值,就是这里起得作用

  4. 4).设置对象头:存储对象自身的运行时数据,类型指针

  5. 5).执行<init>:为对象的字段赋值

1.2、方法区:存储已被jvm加载的类信息(类名,访问修饰符,字段描述,方法描述等)、常量、静态变量、即时编译器编译后的代码(不清楚什么是即时编译器编译后的代码)等数据。也就是说 final/static 的“基本数据类型变量”数据和指针放在方法区,没有final/static 的“基本数据类型变量”数据和指针放在虚拟机栈中。jdk1.6及以前方法区和堆独立区域,jdk1.7方法区中的字符串常量池放在堆中,jdk1.8删除方法区改成元空间(还没太了解)。

1.2.1、运行时常量池:存储编译期间生成的各种字面量和符号引用。其中有个知识点是string的instern()方法。
详情见:http://blog.csdn.net/hupoling/article/details/62423613的总结。

1.3、虚拟机栈:其实严格来说虚拟机栈包含局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存储八种基本数据类型(byte,boolean,char,short,int,float,long,double)和对象引用。平常讨论的栈都是指的局部变量。

1.4、本地方法栈:本地方法栈和虚拟机栈作业是十分相似的,只不过虚拟机栈是为虚拟机执行java方法(也就是字节码)服务,而本地方法栈是为虚拟机执行native方法服务。sun hotspot虚拟机本地方法栈和虚拟机栈合二为一。

1.5、程序计数器:可以看做是当前线程执行字节码的行号指示器。此区域是jvm规范中唯一没有规定任何OOM情况的区域。

2、直接内存:在jdk1.4之后新加入了NIO,可以使用native函数库直接分配堆外内存(本机内存),然后通过一个存储在java堆中的directByteBuffer对象作为这块内存的引用进行操作。避免了io在java堆和native堆来回复制数据提示对鞋性能。直接内存大小默认是java堆xmx(不知道为什么这样设计 容易误导);

3、OOM

3.1、堆溢出:Xmx:最大堆内存,Xms:最小堆内存,当Xmx=Xms时堆不扩展。-XX:+HeapDumpOnOutOfMemoryError ;实例化大量对象可测试堆溢出。

3.1.1、OOM解决:增大xmx扩大堆内存

3.2、栈溢出:栈溢出有俩种情况:

3.2.1、栈的深度超过最大深度限制,抛出StackOverflowError异常

3.2.2、栈扩展时内存不足,抛出OOM;

Xss:每个栈的大小;Xoss:本地方法栈大小,不过实际上xoss无效,栈容量只由-Xss参数设置。

3.2.3、实测中3.2.1说法有点不全。单线程下只有一个栈,所以只可能出现StackOverflowError,无论是栈的深度太大还是每个栈帧过大到账内存不足;多线程下会出现OOM,不断建线程时会出现内存不足;

3.2.4、OOM/StackOverflowError解决:

3.2.4.1、OOM:由于栈内存=操作系统内存 - 堆内存 - 方法区内存 - 程序计数器内存,所有可以减小堆内存来扩大栈内存大小。栈内存大小影响系统并发线程量(栈内存>=每个栈的大小xss线程量);具体设置由n台服务器,每台服务器m个cpu,则最大线程量=nm;每个栈的大小xss<=栈内存/n*m; 注意一下有个问题 ,这个公式没有直接内存?

3.2.4.2、StackOverflowError深度:如果使用jvm默认设置,栈的深度大多数情况下可达到1000~2000,足以在日常开发中使用。注意避免代码中存在超过1000的方法嵌套。每个方法嵌套对应一个栈帧。

3.2.4.3、StackOverflowError栈帧大小:单线程下避免代码中存在大量基本类型或对象引用。

3.2.4.4、多线程下假设每个栈帧特大,jvm是抛出OOM还是StackOverflowError?(待考察研究)

3.3、方法区OOM:-XX:PermSize:最小方法区内存大小;-XX:MaxPermSize:最大方法区内存大小。

3.3.1、OOM解决:避免大量的string.intern();避免大量的动态java(jsp,java反射)。

3.4、本机直接内存溢出:-XX:MaxDirectMemorySize:最大直接内存;默认为Xmx

3.4.1、OOM解决:使用NIO分配本机内存多注意是否超过MaxDirectMemorySize;平常开发容易忽略直接内存;

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值