JVM那点事(二)

转载自大佬 https://www.cnblogs.com/songjn/p/13999898.html

Java程序的开发过程为:

我们利用 JDK (调用 Java API)编写出 Java 源代码,存储于 .java 文件中
JDK 中的编译器 javac 将 Java 源代码编译成 Java 字节码,存储于 .class 文件中
JRE 加载、验证、执行 Java 字节码
JVM 将字节码解析为机器码并映射到 CPU 指令集或 OS 的系统调用。

JVM包含两个子系统和两个组件

两个子系统:

Class loader(类装载)
Execution engine(执行引擎)
两个组件:

Runtime data area(运行时数据区)
Native Interface(本地接口)

在这里插入图片描述
类的加载过程
在加载阶段,虚拟机需要完成以下3件事情:

通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载.class文件的方式
①从本地系统中直接加载
②通过网络下载.class文件
③从zip,jar等归档文件中加载.class文件
④从专有数据库中提取.class文件
⑤将Java源文件动态编译为.class文件

三种类加载器
启动类加载器(Bootstrap ClassLoader)
扩展类加载器(Extension ClassLoader)
应用程序类加载器(Application ClassLoader)
在这里插入图片描述
1)启动类加载器:用来加载Java的核心库,主要加载的是JVM自身所需要的类,使用C++实现,并非继承于java.lang.ClassLoader,是JVM的一部分。负责加载JAVA_HOME\lib目录中的,或者-Xbootclasspath参数指定的路径中的,且被虚拟机认可[注1]的类。开发者无法直接获取到其引用。

注1:JVM是按文件名识别的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包放在lib目录下也没有作用,同时启动加载器只加载包名为java,javax,sun等开头的类。且java是特殊包名,开发者的包名不能以java开头,如果自定义了一个java.包来让类加载器加载,那么就会抛出异常java.lang.SecurityException: Prohibited package name: java.

2)扩展类加载器: 用来加载Java的扩展库。负责加载JAVA_HOME\lib\ext目录中的,或通过系统变量java.ext.dirs指定路径中的类库。由java语言实现。开发者可以直接使用。

3)应用程序类加载器:负责加载用户路径(classpath)上的类库。开发者可以直接使用。可以通过ClassLoader.getSystemClassLoader()获得。一般情况下程序的默认类加载器就是该加载器。

4)除了提供的加载器外,开发者可以通过继承ClassLoader类的方式实现自己的类加载器。

双亲委派机制
解释:当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。

作用:

防止重复加载同一个.class。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。
保证核心.class不能被篡改。通过委托方式,不会去篡改核心.clas,即使篡改也不会去加载,即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。

java中的引用类型
强引用:发生 gc 的时候不会被回收。
软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。
弱引用:有用但不是必须的对象,在下一次GC时会被回收。
虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

内存分配策略
三大原则和担保机制:

优先分配到eden区
大对象,直接进入到老年代
长期存活的对象分配到老年代
空间分配担保
在这里插入图片描述
如图所示,想要了解jvm的内存分配就要熟悉堆空间,在堆中,我给大家划分出了两个大的部分:
1)新生代和老年代
①.新生代主要是刚出生的对象,比如你代码中经常用的new ,以及Method.invoke()等操作,这些操作的对象都是在Eden去先分配出内存,然后等待gc的回收,那么在这个区域里面我也提交,jvm主要做的回收就是MinorGC,jvm默认的是假如一个对象在新生代的年龄到达15岁之后,将其晋升到老年代存储。意思就是这个对象要在新生代中经历15次MinorGC之后不被回收,那么将进入老年代。当然不排除你自己的设定,可以利用参数配置使大对象在Eden出生后直接进入老年代。
大对象直接进入老年代:-XX:PretenureSizeThreshold=n(n代表你要限制的对象的字节数BIT)
②老年代主要存储的都是一些老的油条对象,在此内存区域,是不可能采用标记复制算法的,因为那样会减少一半的空间存储量,降低程序的效率。
2.新生代中又划分出了三个区域
①Eden主要接受刚新生的对象
②Survivor0
③Survivor1
这两个内存区域主要是用于gc做垃圾回收算法时用到的,也就是MinorGC发生的主要内存区域。

JVM有哪些垃圾回收器?
1.Serial收集器(复制算法): 新生代单线程收集器,标记和清理都是单线程,优点是简单高效;

2.ParNew收集器 (复制算法): 新生代收并行集器,实际上是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现;

3.Parallel Scavenge收集器 (复制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用户线程时间/(用户线程时间+GC线程时间),高吞吐量可以高效率的利用CPU时间,尽快完成程序的运算任务,适合后台应用等对交互相应要求不高的场景;

4.Serial Old收集器 (标记-整理算法): 老年代单线程收集器,Serial收集器的老年代版本;

5.Parallel Old收集器 (标记-整理算法): 老年代并行收集器,吞吐量优先,Parallel Scavenge收集器的老年代版本;

6.CMS(Concurrent Mark Sweep)收集器(标记-清除算法): 老年代并行收集器,以获取最短回收停顿时间为目标的收集器,具有高并发、低停顿的特点,追求最短GC回收停顿时间。

7.G1(Garbage First)收集器 (标记-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。此外,G1收集器不同于之前的收集器的一个重要特点是:G1回收的范围是整个Java堆(包括新生代,老年代),而前六种收集器回收的范围仅限于新生代或老年代。

新生代垃圾回收器一般采用的是复制算法,复制算法的优点是效率高,缺点是内存利用率低;老年代回收器一般采用的是标记-整理的算法进行垃圾回收。

GC的触发条件
Minor GC
特点: 发生在新生代上,发生的较频繁,执行速度较快
触发条件: Eden区空间不足\空间分配担保

Full GC
特点:主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢
触发条件:
调用 System.gc()
老年代区域空间不足
空间分配担保失败
JDK 1.7 及以前的永久代(方法区)空间不足
CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间补

垃圾回收算法
1.标记-清除算法:标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。

2.复制算法:按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。年轻代中eden,survivor1,survivor2比例为8:1:1,年轻代和老年代的比例为1:2

判断对象是否存活(怎样判断对象是否可以被回收?)
引用计数算法(已被淘汰的算法)
可达性分析算法
1)引用计数算法(已被淘汰的算法)

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

目前主流的java虚拟机都摒弃掉了这种算法,最主要的原因是它很难解决对象之间相互循环引用的问题。尽管该算法执行效率很高。

2)可达性分析算法

通过判断对象的引用链是否可达来决定对象是否可以被回收;

可以作为GC Root的对象有:

虚拟机栈中引用的对象(栈帧中的本地变量表);
方法区中的常量引用对象;
方法区中类静态属性引用对象;
本地方法栈中JNI(Native方法)的引用对象;
活跃线程中的引用对象;

3.标记-整理算法:标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

4.分代算法:根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

内存泄露
内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间的浪费称为内存泄漏。

长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

发生内存泄漏的原因以及处理方式:

1)静态集合类引起内存泄漏

像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。

2)当集合里面的对象属性被修改后,再调用remove()方法时不起作用。

3、监听器

在释放对象的时候却没有去删除这些监听器,增加了内存泄漏的机会。

4)各种连接

比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。

5)内部类和外部模块的引用

内部类的引用是比较容易遗忘的一种,而且一旦没释放可能导致一系列的后继类对象没有释放。此外程序员还要小心外部模块不经意的引用,例如程序员A 负责A 模块,调用了B 模块的一个方法如: public void registerMsg(Object b); 这种调用就要非常小心了,传入了一个对象,很可能模块B就保持了对该对象的引用,这时候就需要注意模块B 是否提供相应的操作去除引用。

6)单例模式

不正确使用单例模式是引起内存泄漏的一个常见问题,单例对象在初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部的引用,那么这个对象将不能被JVM正常回收,导致内存泄漏。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值