钻牛角尖系列之JVM(一)

首先说一下,刚开始学习Java的时候就知道Java不需要程序员自己释放内存,感觉和C++一比方便许多,只不过当时是个菜比(不过现在也是)。既然进入了Java这个知识的大坑中,那么我们就得绞尽乳汁的去学呀。
上面的都是废话,预知后事如何,往下瞅:
一:首先谈一下GC是干什么的呢?
1、JVM在执行GC的时候去判断那些内存需要进行回收(看看是不是在GCRoots这棵大树的庇佑下)。
2、判断之后,那么什么时候回收呢(没大哥罩着就危险了,不过可以逃跑但是机会只有一次呦!)?
3、知道什么时间去回收,那么该如何回收呢(我叶良辰(JVM)有一百种方法让你呆不下去)?
二:既然GC什么都做了我们学他干什么呢(虽然sun公司描绘的很好,但是不了解机制的话出了事就是N脸懵逼呀,所以得学)?
1、第一点简单粗暴面试官会问的。(不要喷我,毕竟我很肤浅。)
2、因为机器毕竟是机器不是那么完美,有的时候会出现一些问题。在以下情境我们会用到:a、排查内存溢出;b、排查内存泄漏;c,性能调优;排查并发瓶颈。
三:我们知道GC执行的对象的回收,那么什么时候触发一个对象的回收呢?
1、对象没有引用。
2、作用域发生未捕获异常。
3、程序在作用域正常执行完毕。
4、程序执行了System.exit()。
5、程序发生意外终止。

 

JVM在进行GC的时候,首先是使用一些算法对对象记性标记,哪些是垃圾,哪些不是,在标记过后,在使用一些算法,将他们回收。但是在说这些之前我们先来看看我们要GC的东西在哪呢?

 

 

首先看一下上面这张图(不认识?别开玩笑?):

这张图用一句话概括就是:两个子系统和两个组件。
两个系统:1、Class Loader 子系统和 Execution engine(执行引擎)子系统。
两个组件:2、Runtime data area(运行时数据区)组件和Native interface(本地接口)组件。
Class loader子系统的作用:虚拟机团队是这样描述的“通过一个类的权限定名来获取描述此类的二进制字节流”,这个动作是放到虚拟机外部实现的,以便让 应用程序自己决定如何去获取所需要的类。当然我们可以自定义类加载器。
Execution engine子系统的作用:执行classes中的字节码指令。任何JVM specification实现(JDK)的核心都是Execution engine。
Native interface组件:与native libraries交互说白了就是为本地方法提供服务的,是其它编程语言交互的接口。当调用native方法的时候,就进入了一个全新的并且不再受虚拟机限制的世界,容易出现JVM无法控制的native heap OutOfMemory。
Runtime data area(运行时数据区)组件分为下面五个部分:

第一块:可以看做是当前线程所执行的字节码的行号指示器,如该方法为native的,则PC寄存器中不存储任何信息,如果执行的是Java代码记录的是正在执行的字节码指令的地址。


第二块:JVM栈JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。

第三块:堆(Heap)它是JVM用来存储对象实例以及数组值的区域,可以认为Java中所有通过new创建的对象的内存都在此分配,Heap中的对象的内存需要等待GC进行回收。
(1) 堆是JVM中所有线程共享的,因此在其上进行对象内存的分配均需要进行加锁,这也导致了new对象的开销是比较大的
(2) Sun Hotspot JVM为了提升对象内存分配的效率,对于所创建的线程都会分配一块独立的空间TLAB(Thread Local Allocation Buffer),其大小由JVM根据运行的情况计算而得,在TLAB上分配对象时不需要加锁,因此JVM在给线程的对象分配内存时会尽量的在TLAB上分配如果TLAB使用掉后创建新的TLAB的时候才会进行加锁提高了效率,在这种情况下JVM中分配对象内存的性能和C基本是一样高效的,但如果对象过大的话则仍然是直接使用堆空间分配
(3) TLAB仅作用于新生代的Eden Space,因此在编写Java程序时,通常多个小的对象比大的对象分配起来更加高效。
(4) 所有新创建的Object 都将会存储在新生代Yong Generation中。如果Young Generation的数据在一次或多次GC后存活下来,那么将被转移到OldGeneration。新的Object总是创建在Eden Space。

第四块:方法区域(Method Area)也就 Non—heap非堆
(1)在Sun JDK中这块区域对应的为PermanetGeneration,又称为持久代。
(2)方法区域存放了所加载的类的信息(名称、修饰符等)、类中的静态变量、类中定义为final类型的常量、类中的Field信息、类中的方法信息,当开发人员在程序中通过Class对象中的getName、isInterface等方法来获取信息时,这些数据都来源于方法区域,同时方法区域也是全局共享的,在一定的条件下它也会被GC,当方法区域需要使用的内存超过其允许的大小时,会抛出OutOfMemory的错误信息。

第五块:运行时常量池(Runtime Constant Pool)存放的为类中的固定的常量信息、方法和Field的引用信息等,其空间从方法区域中分配(是方法区(Method Area)的一部分)。

第六块:本地方法堆栈(Native Method Stacks)JVM采用本地方法堆栈来支持native方法的执行,此区域用于存储每个native方法调用的状态。

程序计数器、虚拟机栈、本地方法栈是随着线程的创建而创建的共生共灭;方法区、堆是随着虚拟机的启动而创建的。

了解了上面之后我们知道了收集的垃圾所在的位置,那么哪些需要收集呢?
Java对象有三代:新生代(Young Generation)、老年代(Old Generation)、持久代(Permanent Generation)。废话少说直接上图没图说个........:

如上图所示,为Java堆中的各代分布。  
1. Young(年轻代)
HotSpot JVM把年轻代分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1,为啥默认会是这个比例,接下来我们会聊到。一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,复制算法的基本思想就是将内存分为两块,每次只用其中一块,当这一块内存用完,就将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
2. Tenured(年老代)
年老代存放从年轻代存活的对象。一般来说年老代存放的都是生命期较长的对象。  
3. Perm(持久代)
用于存放静态文件,如今Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过-XX:MaxPermSize=进行设置。

了解了上面这些之后,我们看看一些GC的算法吧:

零:引用计数法。思想是给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时那么值就减一,任何时刻计数器值为0的对象就是不可能再被使用的。虽说该算法实现简单效率也高,但是主流的JavaJVM并没有采用这个实现来管理内存,假设有两个对象 A和B,A中引用了B对象,并且B中也引用了A对象, 那么这时两个对象的引用计数器都不为0,但是在SunHotSpot中还是可以进行垃圾回收的,这个可以参考《深入理解Java虚拟机作者周志明》里面有案例。

一:可达性算法:思想就是通过一系列的叫做“GC Roots ”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径被称为引用链”当一个对象不可达时则证明对象是不可用的那么就可以判定为回收。

二:标记清除法。
            这个方法主要分为两个阶段:标记阶段和清除阶段。 在标记阶段,通过跟对象,标记所有从跟节点开始的可达的对象,那么未标记的对象就是未被引用的垃圾对象。 在清除阶段,清除掉所以的未被标记的对象。 缺点:垃圾回收后可能存在大量的磁盘碎片,准确的说是内存碎片。因为对象所占用的地址空间是固定的。
三:标记压缩清除法(Java中老年代采用)。
         在标记清除算法上对于内存碎片进行了一个整理。分为三个阶段:标记阶段,压缩阶段,清除阶段。标记阶段和清除阶段不变,只不过增加了一个压缩阶段,在做完标记阶段后,将这些标记过的对象集中放到一起,确定开始和结束地址,比如全部放到开始处,这样再去清除,将不会产生磁盘碎片。但是我们也要注意到几个问题,压缩阶段占用了系统的消耗,对象过多的话,损耗很大,适用于对象少的区域,效率较高。
四:复制算法(Java中新生代采用)。
           核心思想是将内存空间分成两块,同一时刻只使用其中的一块,在垃圾回收时将正在使用的内存中的存活的对象复制到未使用的内存中,然后清除正在使用的内存块中所有的对象, 然后把未使用的内存块变成正在使用的内存块,把原来使用的内存块变成未使用的内存块。很明显如果存活对象较多的话,算法效率会比较差,并且这样会使内存的空间折半,但是这种方法也不会产生内存碎片。
五:分代法(Java堆采用)。
          主要思想是根据对象的生命周期长短特点将其进行分块,根据每块内存区间的特点,使用不同的回收算法,从而提高垃圾回收的效率。比如Java虚拟机中的堆就采用了这种方法分成了新生代和老年代。然后对于不同的代采用不同的垃圾回收算法。 新生代使用了复制算法,老年代使用了标记压缩清除算法。

可达性算法只是作为一个标记而已,剩下的345才是真正的回收的。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值