1、了解历史
1、JDK、JRE、JVM 之间的关系
2、Java 的发展史
以后补充
3、Jvm发展史
以后补充
2、自动内存管理机制
1、Java内存区域与内存溢出异常
1、运行时数据区域(5)
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域根据《Java虚拟机规范(Java SE 7版)》
的规定,Java虚拟机所管理的内存将会包括以下几个运行时数据区域:
1、程序计数器
程序计数器(Program Counter Register) 是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条执行字节码指令。
**每条线程都有一个独立的程序计数器(线层私有)。程序计数器能够保证线程切换后能够恢复到正确的执行位置**
如果一个线程正在执行的是java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址。如果是native方法,计数器为空。==此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。==
2、虚拟机栈
同样是线程私有,它的生命周期跟线程是一样的。==虚拟机栈描述的是Java方法执行的内存模型==:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
我们平常所说的引用变量在栈里面
等词汇其实说的是 虚拟机栈中的局部变量表
局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)returnAddress类型(指向了一条字节码指令的地址)
结合上图 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError
异常;如果虚拟机栈可以动态扩展(当前大部分的Java虚拟机都可动态扩展,只不过Java虚拟机规范中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,就会抛出OutOfMemoryError异常
3、本地方法栈
和Java虚拟机栈很类似,不同的是本地方法栈为Native方法服务。
HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError和OutOfMemoryError
异常。
4、堆
- 对于大多数应用来说,Java堆(Java Heap)是Java虚拟机所管理的内存中最大的一块,由所有线程共享。在虚拟机启动时创建。堆区唯一目的就是存放对象实例。
- 堆是垃圾回收器管理的主要区域,因此很多时候也被称为 “GC堆”,因为现在收集器基本都采用分代收集算法,所以 堆还可以被细分为: 新生代和老年代。再细致一点的有Eden空间、From Survivor空间 ToSurvivor空间等 无论怎么划分,无论哪个区域,存储的都仍然是对象实例,进一步划分的目的是为了更好地回收内存,或者更快地分配内存
- 堆内存可以扩展,当无法扩展时将抛出
OutOfMemoryError
异常
5、方法区
所有线程共享,存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
对于 HotSpot 虚拟机来说,人们更愿意把方法区成为 永久带(PermGen),两者本质上并不等价仅仅是因为HotSpot虚拟机的设计团队使用永久代来实现方法区(把GC分代收集拓展至方法区)而已,这样 HotSpot 的垃圾收集器可以像管理Java 堆一样管理这部分内存
这个区域的回收“成绩”比较难以令人满意,尤其是类型的卸载,条件相当苛刻,但是这部分区域的回收确
实是必要的
关于方法区跟永久带的区分:
- 方法区是jvm的一种规范,而永久带是该规范的一种实现
- 只有 HotSpot 才有 永久带 (PermGen),其他虚拟机没有
6、运行时常量池
运行时常量池是方法区的一部分,是 Class 文件中的其中一项描述信息,用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放
7、直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现,所以我们放到这里一起说明
在JDK 1.4中新加入了NIO,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据
显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,肯定还是会受到本机总内存大小以及处理器寻址空间的限制,会出现OutOfMemoryError异常
8、1.8的变化(Metaspace)
使用永久带实现方法区这并不是一个好主意,因为这样更容易出现内存溢出问题,移除永久代的工作从JDK1.7就开始了JDK1.7中,存储在永久代的部分数据就已经转移到了Java Heap或者是 Native Heap。但永久代仍存在于JDK1.7中,并没完全移除
JDK 1.8 中已经不存在永久代的结论,取而代之的是 Metaspace(元空间)
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制,但可以通过以下参数来指定元空间的大小:
-XX:MetaspaceSize
,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。-XX:MaxMetaspaceSize
,最大空间,默认是没有限制的。
除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio
,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集-XX:MaxMetaspaceFreeRatio
,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集
9、总结
通过上面分析,大家应该大致了解了 JVM 的内存划分,也清楚了 JDK 8 中永久代向元空间的转换。不过大家应该都有一个疑问,就是为什么要做这个转换?所以,最后给大家总结以下几点原因:
字符串存在永久代中,容易出现性能问题和内存溢出。
类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
Oracle 可能会将HotSpot 与 JRockit 合二为一。 (emmm)
2、Hotspot虚拟机对象探秘
1、对象的创建
我们这里不详细说明对象创建的整个过程,我们只说明对象创建 在虚拟机(Hotspot)中的过程:
虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程(类加载过程后面再说)
在类加载检查通过后,接下来虚拟机将为对象分配内存。对象所需内存的大小在类加载完成后便可完全确定
- 两种分配方式:
- 指针碰撞(堆内存比较规整)
- 空闲列表(堆内存不规整)
- 在堆中分配内存的并发问题解决方式
- 对分配内存的动作采用 CAS,保证更新的原子性
- 线程副本类似于 ThreadLocal 的 TLAB(本地线程分配缓冲区),只有 TLAB用完需要重新分配 的时候才需要同步锁定
- 内存分配完后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值
- 虚拟机还要对对象进行必要的设置
- 从虚拟机的角度看,一个新的对象已经产生了,但是对于java程序来说,对象的创建才刚刚开始,还要执行
<init>
方法(构造器、初始化块等)
2、对象的内存布局
在 Hotspot 虚拟机中对象在内存中的存储结构分为3个区域:
- 对象头(Header)
- 对象头用于存储对象的元数据信息
- 第一部分:Mark Word 部分数据的长度在32位和64位虚拟机(未开启压缩指针)中分别为32bit和64bit,存储对象自身的运行时数据如哈希值等,Mark Word一般被设计为非固定的数据结构,以便存储更多的数据信息和复用自己的存储空间
- 第二部分:类型指针 指向它的类元数据的指针,用于判断对象属于哪个类的实例
- 实例数据(Instance Data)
- 存储的是真正有效数据,如各种字段内容,各字段的分配策略为 longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同宽度的字段总是被分配到一起,便于之后取数据。父类定义的变量会出现在子类定义的变量的前面
- 对其填充(Padding)
- 部分仅仅起到占位符的作用,并非必须
3、对象的访问定位
对象的访问定位也取决于具体的虚拟机实现。当我们在堆上创建一个对象实例后,就要通过虚拟机栈中的reference类型数据来操作堆上的对象。现在主流的访问方式有两种(HotSpot虚拟机采用的是第二种):
- 直接指针访问对象
- 即reference中存储的就是对象地址,相当于一级指针(Hotspot默认采用)
- 使用句柄访问对象
- 即reference中存储的是对象句柄的地址,而句柄中包含了对象实例数据与类型数据的具体地址信息,相当于二级指针
两种方式有各自的优缺点。当垃圾回收移动对象时,对于句柄访问对象而言,reference中存储的地址是稳定的地址,不需要修改,仅需要修改对象句柄的地址;而对于直接指针访问对象,则需要修改reference中存储的地址。从访问效率上看,直接访问由于使用句柄访问,因为只进行了一次指针定位,节省了时间开销,而这也是HotSpot采用的实现方式。下图是句柄访问与指针访问的示意图。