网站建设验收合同,库存网站建设定制,北京新增病例最新消息,jannah wordpressJava虚拟机是Java的核心和基础#xff0c;他是Java编译器和操作系统平台之间处理器#xff0c;能实现跨平台运行Java程序。本文主要讲解的是虚拟机如何管理对象#xff0c;即Java对象在JVM虚拟机中被创建到回收的流程 Java对象从创建到回收的生命周期对象创建流程1.类加载检…Java虚拟机是Java的核心和基础他是Java编译器和操作系统平台之间处理器能实现跨平台运行Java程序。本文主要讲解的是虚拟机如何管理对象即Java对象在JVM虚拟机中被创建到回收的流程
Java对象从创建到回收的生命周期对象创建流程1.类加载检查类加载过程2.分配内存JVM运行时内存数据区布局运行时数据区关系3.初始化4.设置对象头5.执行init方法对象回收流程1.标记存活对象2.非存活对象回收垃圾回收策略垃圾回收算法垃圾回收器Serial收集器Parallel Scavenge收集器ParNew收集器CMS对象创建流程
public class Main {public static void main(String[] args) {createObject();}public static void createObject() {Object object new Object();}
}当虚拟机碰到new或者克隆一个类的实例对象时【如上new Object()】它会经历如下过程:
1.类加载检查
当虚拟机遇到一个new指令的时候首先去判断这个指令的参数是否能在方法区找到类的引用并且检查这个引用是否已经被加载解析和初始化过。如果没有则必须先执行先执行类的加载过程
类加载过程
参考博客Java类的加载过程
2.分配内存
当类加载完以后虚拟机将会为新生对象分配内存。对象所需的内存大小在类加载完后便可完全确定便会将Java堆中一块确定大小的内存从Java堆中划分出来 此时会涉及两个问题 2.1.虚拟机如何分配内存 2.2.在并发情况下如何保证多个对象的内存不重叠
JVM运行时内存数据区布局 a)堆区 Java堆是用来存储对象本身的以及数组。堆是被所有线程共享的在JVM中只有一个堆。堆有分代逻辑堆划分为了年轻代老年代年轻代分为了eden区s0区s1区比例默认为老年代年轻代2:1eden:s0:s18:1:1; 这里引发两个问题 1.为什么堆中要分代 2.为什么年轻代还要分为3个区为什么区的比例为8:1:1
会涉及到关于对象回收的机制 1.堆中分代是为了提高效率对象是有生命周期有的对象存活时间长有的对象存活时间短存活时间长的放到老年代存活时间短的放到年轻代。老年代回收频率低点年轻代回收频率高点 2.分为3个区是为了gc之后方便把存活的对象复制到s1或者s2中方便内存整理。比如为8:1:1是因为大多数的对象都是朝生夕死的存活时间比较短所以jvm默认8:1:1的比例是合适的让 Eden区尽量大survivor区足够用即可 b)栈 Java栈中存放一个个栈帧每个栈帧对应一个被调用的方法在栈帧中包括局部变量表操作栈动态链接【指向当前方法所属的类的运行时常量池的方法】方法出口【方法执行完后需返回地址】 c)本地方法栈 本地方法栈与Java栈作用和原理类似区别在于Java栈是为执行本地方法服务的而本地方法栈则是执行本地方法服务的 d方法区 与堆一样是线程共享的区域。在方法区中存储了类信息静态变量常量以及编译器编译后的代码 e程序计数器 在jvm中多线程是通过线程轮流切换来获得cpu执行时间的。为了保证每个线程在切换后能够恢复到切换之前程序执行的位置每个线程都有自己独立的程序计数器
运行时数据区关系
由下图可以看出堆和方法区是所有线程共用的本地方法栈Java栈程序计数器是线程私有的 看到堆中存放对象引发两个问题 1.所有新创建对象都存放到堆中吗 2.堆中既有老年代又有年轻代新创建的对象应该放在哪里
我们先整体看一下对象在内存的分配流程然后再逐一回答上面的问题
根据图流程所示回答第一个不是所有的对象都会存放到堆中有可能是栈。栈上分配主要是虚拟机根据该对象是否被外部访问。如果不会逃逸出该对象在栈上分配随着方法的结束出栈而销毁。 对象逃逸分析分析对象的动态作用域当一个对象在方法中被定义后它可能被外部方法引用例如被作为参数传递到其他地方去
public User createUser(){User user new User();user.setName(judy);user.setAge(18);return user;
}public void test1(){User user createUser();
}像上面的方法它可能被其他方法调用无法在栈内分配而像作用域非常确定比如
public void createUser(){User user new User();user.setName(judy);user.setAge(18);
}那这种作用域非常确定不会被其他方法调用就可以在栈内分配。 因为栈内存比较小为了防止栈内没有一大块连续空间导致对象内存不够分配虚拟机会采用标量替换方法去解决这个问题 标量替换当对象确定在栈内分配时候并且开启了标量替换它不会直接创建对象因为创建对象需要设置对象头也会占用空间而是将该Java对象成员变量进行分解分为若干个被这个方法使用的局部成员变量然后为分解后的变量分配空间。jdk7以后默认开启。标量一般是指Java中的基本数据类型
解决完第一个问题对象否则都存在于堆中我们再来看第二个问题堆中既有老年代又有年轻代新创建的对象应该放在哪里 第二个问题的答案是一个新创建的对象如果非常大超过虚拟机设置的值就会直接放到老年代但是如果没有超过限定值先放到Eden中。至于S0,S1区对象在通过minorGC以后才会存放到这两个区域中后续讲解到对象的垃圾回收时还会重点解决一下。
3.初始化
内存分配完成后虚拟机需要将分配到的内存空间都初始化为零值【不包括对象头】。这一步保证对象的实例字段在Java代码中可以不赋初始值就直接使用程序就能访问到这些字段的数据类型所对应的零值
4.设置对象头
对对象的实例属性值设置为零值后需要对对象进行必要的设置例如这个对象属于哪个类的实例如果才能找到类的元数据信息对象的哈希码对象gc分代信息。这些信息在对象的对象头中。 对象分为三部分信息对象头实例数据对象填充 4.1对象头分为三部分信息 4.1.1第一部分存储对象自身运行时的数据 4.1.2第二部分存储类型指针即对象指向它的类元数据的指针虚拟机通过这个指针确定这个对象属于哪个类的实 4.1.3第三部分是数组长度若为数组则有数若没有则为空 4.2实例数据对象的实例属性 4.3对象填充当对象头实例数据的字节数不是8字节的整数时字节填充使其为8字节的整数倍加快内存寻址
5.执行init方法
为对象赋值和执行构造方法
对象回收流程
1.标记存活对象
对象回收之前先判断对象是否活着判断对象是否活着两种方式 1.引用计数法 给对象中添加一个引用计数器每当有一个地方引用它计数器就加1 当引用失效计数器就减1任何时候计数器为0的对象就是不可能再被使用的。这个方法实现简单效率高但是目前主流的虚拟机中并没有选择这个算法来管理内存其最主要的原因是它很难解决对象之间相互循环引用的问题。 2.可达性分析算法 将GC Roots对象作为起点从这些起点开始向下搜索引用的对象找到的对象是非垃圾的对象其余标记为垃圾对象。 GC Roots根节点线程栈的本地变量静态变量本地方法栈中的变量 其中引用关系可以分为强引用软引用弱引用虚引用 强引用普通的变量引用
public static User usernew User();软引用被SoftReference对象包裹着正常情况不会被回收但是gc做完后发现释放不出空间存放新的对象则会将这些软引用对象回收掉。用来实现内存敏感的高速缓存
public static SoftReferenceUser usernew SoftReferenceUser(new User());弱引用将对象用WeakReference软引用类型的对象包裹着弱引用跟没有引用差不多GC会直接回收掉。 虚引用最弱的引用几乎不用
引发一个问题是否没有可达性分析分析到的对象就会被回收 答案是不会一个对象被回收要经历两次标记的过程 1.第一次标记进行筛选 筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize方法对象将直接被回收。 2.第二次标记 第二次标记如果这个对象覆盖了finalize方法finalize方法是对象脱逃死亡命运的最后一次机会如果对象要在finalize()中成功拯救 自己只要重新与引用链上的任何的一个对象建立关联即可譬如把自己赋值给某个类变量或对象的成员变量 那在第二次标记时它将移除出“即将回收”的集合。 如果对象这时候还没逃脱那基本上它就真的被回收了。 注意 一个对象的finalize()方法只会被执行一次 也就是说通过调用finalize方法自我救命的机会就一次。
2.非存活对象回收
被标记的对象是如何在内存中被回收首先堆在内存是分代的两者根据对象生命周期的不同存放着不同类型的对象。那意味着也需要采取不同的回收策略【年轻代回收和老年代回收】回收策略也有着相应的回收算法根据不同垃圾回收算法JVM也提供了一系列垃圾回收器
垃圾回收策略
Minor GC/Yong GC:指发生新生代的垃圾收集动作Minor GC会非常的频繁回收速度一般比较快 Major GC/Full GC:一般会回收老年代年轻代方法区的垃圾Major GC的速度一般会比Minor GC慢
垃圾回收算法 标记复制 将内存分为大小相同的两块每次使用其中的一块。当一块用完后将存活的对象复制到另一块去然后把使用的空间一次清理掉。 标记清除 算法分为标记和清除阶段标记存活的对象统一回收所有未被标记的对象也可以标记需要回收的对象在标记完成后统一回收被标记的对象。它是最基础的手机算法比较简单但是会带来1个问题清除后产生大量不连续的碎片 标记整理 根据老年代特有的标记算法标记过程和标记-清除算法一样但后续步骤不是直接对可回收的对象回收而是让所有存活的对象向一端移动然后直接清理掉边界以外的内存。
垃圾回收器
垃圾回收算法是内存分配的方法论那么垃圾回收器是内存分配的具体实现
Serial收集器
Serial串行收集器是最基本、历史最悠久的垃圾收集器了。 大家看名字就知道这个收集器是一个单线程收集器了。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程“StopTheWorld”直到它收集结束。新生代采用复制算法老年代采用标记-整理算法。 Serial Old收集器是Serial收集器的老年代版本它同样是一个单线程收集器。可以和CMS收集器搭配使用 配置参数如下 -XX:UseSerialGC -XX:UseSerialOldGC
Parallel Scavenge收集器
Parallel收集器其实就是Serial收集器的多线程版本除了使用多线程进行垃圾收集外 其余行为控制参数、收集算法、回收策略等等和Serial收集器类似。 默认的收集线程数跟cpu核数相同当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数但是一般不推荐修改。Parallel Scavenge收集器关注点是吞吐量高效率的利用CPU CMS等垃圾收集器的关注点更多的是用户线程的停顿时间提高用户体验 新生代采用复制算法老年代采用标记-整理算法。 Parallel Old收集器是Parallel Scavenge收集器的老年代版本。 使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合都可以优先考虑 ParallelScavenge收集器和Parallel Old收集器(JDK8默认的新生代和老年代收集器)。
参数配置 -XX:UseParallelGC(年轻代),-XX:UseParallelOldGC(老年代)
ParNew收集器
ParNew收集器其实跟Parallel收集器很类似区别主要在于它可以和CMS收集器配合使用。 新生代采用复制算法老年代采用标记-整理算法。
参数配置 -XX:UseParNewGC
CMS
CMSConcurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的收集器。 它非常符合在注重用户体验的应用上使用它是HotSpot虚拟机第一款真正意义上的并发收集器它第一次实现了让垃圾收集线程与用户线程基本上同时工作。从名字中的MarkSweep这两个词可以看出CMS收集器是一种“标记-清除”算法实现的它的运作过程相比于前面 几种垃圾收集器来说更加复杂一些。 整个过程分为四个步骤 初始标记 暂停所有的其他线程(STW)并记录下gcroots直接能引用的对象速度很快。 并发标记 并发标记阶段就是从GCRoots的直接关联对象开始遍历整个对象图的过程 这个过程耗时较长但是不需要停顿用户线程可以与垃圾收集线程一起并发运行。 因为用户程序继续运行可能会有导致已经标记过的对象状态发生改变。 重新标记 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录这个阶段的停顿时间一般会比初始标记阶段的时间稍长远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。 并发清理开启用户线程同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理 并发重置重置本次GC过程中的标记数据。 参数配置 -XX:UseConcMarkSweepGC(old)
Java虚拟机对对象的管理讲解的这里有问题可以留言喔