上次博客,咱们说了jvm运行时的内存模型,堆,栈,程序计数器,元空间和本地方法栈。咱们主要说了堆和栈,栈的流程大体也说了一遍,同时咱们知道堆是用来存对象的,分别年轻代和老年代。可是具体的堆是怎么来存放对象的呢?何时能够将对象放置在老年代呢。下面我来看一下。html
若是都为默认设置,大体就是这样的。假设咱们设置内存堆的大小为600M,那么老年代就大概是400M,咱们的年轻代就是200M,而后年轻代的eden区域占160M也就是200M的8/10,通常新建的对象都在这,我是说通常啊。后面会用这个600M来详细说明,from和 to区域各占20M,也就是Survivor区域占用40M,每次作完minor GC,对象就放在这个区域。java
我刚才说到,有时候对象不在年轻代,那么我来具体分析一下,什么状况放置在年轻代,而何时又放置在老年代。算法
1,Minor GC以后,存活的对象Survivor区域放不下。缓存
public class Main { public static void main(String[] args) { byte[] bt1; bt1 = new byte[60000 * 1024]; } }
加入堆内存日志,咱们获得打印结果为:mybatis
咱们获得bt1新建之后,咱们的堆内存几乎占满了,如今已经99%了,那么咱们再来看一下。jvm
public class Main { public static void main(String[] args) { byte[] bt1,bt2; bt1 = new byte[60000 * 1024]; bt2 = new byte[10000 * 1024]; } }
从代码里咱们能够得知,咱们新建了bt1以后又新建了bt2,这时咱们的eden区域应该不够用了,那么咱们的内存会怎么来处理呢。咱们来看一下结果优化
咱们能够看到已经作了一次GC了,可是仍是放不下,那么咱们直接将较大的对象直接放置在了堆内存上。spa
2,长期存活的对象移到老年代。也就是通过屡次minorGC之后,对象仍是存活的,咱们将该对象移置老年代,通常是15次,也就是对象头内的分代年龄达到15岁时,咱们将该对象移置老年代。线程
3,对象动态年龄判断。日志
这个很重要的一个理论知识,大概来讲一下,当咱们作完minorGC之后,对象放在to区域,也就是咱们Survivor的to区域,可能对象是放不下的,这时会来计算分类年龄,大体是这样来算的将全部分代年龄为1的相加,再加上分代年龄为2的,再加分代年龄为3的,依次相加,一直加到最大的分代年龄,但在相加过程当中,你会发现加到分代年龄为m的对象,总大小已经放满了to区域,这时就将m到n分代年龄的对象都移置到老年代,包含m。也就是大于Survivor区域的50%时,则后面的对象,包含该年龄的对象都放置在老年代。
4,大对象直接放在老年代。再来看段代码。
public class Main { public static void main(String[] args) { byte[] bt1; bt1 = new byte[90000 * 1024]; } }
上面我知道咱们建立一个大概600M的对象放置在eden时,占了99%,那么咱们建立大于600M的对象,eden必定放不下了。那么直接放置在老年代。这里参数也是能够设置的。我来设置一个参数再看看,设置参数为
-XX:PretenureSizeThreshold=10000000 -XX:+UseSerialGC -XX:+PrintGCDetails
public class Main { public static void main(String[] args) { byte[] bt1; bt1 = new byte[20000 * 1024]; } }
咱们设置了参数,声明10M的对象就为大对象,咱们建立了一个大概20M的对象,就直接放置在了老年代上。就是对象经历那么屡次的minorGC了,jvm虚拟机会认为你可能会一直存活,趁着此次放不下了,你就趁早过来吧,来咱们老年代混吧。
5,老年代空间分配担保机制。
其实咱们每次进行minorGC前,会有一系列操做的,可能会进行full GC的,那么咱们来看一下流程吧。
我来解释一下上面那个五彩缤纷的图。等咱们的eden区满时,须要进行minorGC,这时会优先看一下老年代的剩余空间大小,若是老年代剩余的空间很少了,咱们就可能进行full GC,也就是咱们老年代的剩余空间小于咱们的eden区内将要进行minorGC对象的总和。
若是真的小了,那么咱们往下走,咱们会判断时候配置了-XX:-HandlePromotionFailure (jdk8以上默认设置)这个参数,若是没配置,直接进行fullGC,若是配置了就去判断老年代的剩余空间是否小于咱们每次minorGC后每次要放在老年代对象大小的平均值,若是老年代小于minorGC了,那么进行fullGC。不然不须要进行full GC。
eden和Survivor(from和to)默认比例是8:1:1,可是jvm可能会将咱们的参数优化,也就是-XX:+UseAdaptiveSizePolicy这个默认参数,我将其改成-XX:-UseAdaptiveSizePolicy不进行优化,保持8:1:1的比例了。
咱们再来看一下什么样的对象是能够被回收的。
1,引用计数法(基本不用,循环引用对象永远没法销毁,可能内存溢出)
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用 失效,计数器就减1;任什么时候候计数器为0的对象就是不可能再被使用的。
GC Roots根节点通常为线程栈的本地变量、静态变量、本地方法栈的变量等等。
2,可达性分析算法。
这个算法的基本思想就是经过一系列的称为 “GC Roots” 的对象做为起点, 从这些节点开始向下搜索,找到的对象都标记为非垃圾对象,其他未标记的对象都是垃圾对象
3,常见的引用类型。
java的引用类型通常分为四种:强引用、软引用、弱引用、虚引用
import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; public class Main { public static void main(String[] args) { User user = new User();//强引用 WeakReference<User> user2 = new WeakReference<User>(new User());//弱引用 SoftReference<User> user3 = new SoftReference<User>(new User());//软引用 } }
通常将对象用SoftReference软引用类型的对象包裹,正常状况不会被回收,可是GC作完后发现释放不出空间存放新的对象,则会把这些软引用的对象回收掉。软引用可用来实现内存敏感的高速缓存。
4,finalize最终判断对象存活。
finalize是在对象立刻要被收回以前运行的最后一个方法,能够写逻辑,可是彻底不建议去这样去写,极可能出现对象永远不会被回收,形成内存溢出,也就是说在finalize方法内还可能“救活”咱们的对象。
即便在可达性分析算法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
如何判断一个类是无用的类
1.该类全部的实例都已经被回收,也就是 Java 堆中不存在该类的任何 实例。
2.加载该类的 ClassLoader 已经被回收。
3.该类对应的 java.lang.Class对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。
最后咱们来看一下逃逸分析。
JVM的运行模式用三种,分别是解释模式,编译模式和混合模式,这里简单说一下这个问题,否则后面会蒙圈的。
解释模式就是执行一行JVM字节码就编译一行为机器码,这样的好处就是很节省内存空间,不用把全部的字节码都塞到内存里面去,运行效率低,可是启动快。
编译模式和解释模式偏偏相反,是先将全部JVM字节码一次编译为机器码,而后一次性执行全部机器码。这样会提升咱们的运行效率,可是消耗空间资源。
混合模式是上面的总和,依然使用解释模式执行代码,可是对于一些 "热点" 代码采用编译模式执行,JVM通常采用混合模式执行代码。
咱们来看一段代码。
public class Main { public User getUserBeanTest() { User user = new User();//放置在堆上 return user; } public void userBeanTest() { User user = new User();//优先和方法一块儿发放置在栈上. } }
也就是说明,对象也是有很小的可能放置在栈上的。中秋放假了,明天补一下mybatis的底层是实现原理。过几天继续来讲咱们的jvm优化
最进弄了一个公众号,小菜技术,欢迎你们的加入
原文出处:https://www.cnblogs.com/cxiaocai/p/11520731.html