JVM 对 Java 的原生锁做了哪些优化?
(1)自旋锁
在线程进行阻塞的时候,先让线程自旋等待一段时间,可能这段时间其它线程已经解锁,这时就无需让线程再进行阻塞操作了。自旋默认次数是10次。
(2)自适应自旋锁
自旋锁的升级,自旋的次数不再固定,由前一次自旋次数和锁的拥有者的状态决定。
(3)锁消除
在动态编译同步代码块的时候,JIT编译器借助逃逸分析技术来判断锁对象是否只被一个线程访问,而没有其他线程,这时就可以取消锁了。
(4)锁粗化
当JIT编译器发现一系列的操作都对同一个对象反复加锁解锁,甚至加锁操作出现在循环中,此时会将加锁同步的范围粗化到整个操作系列的外部。
- 锁粒度:不要锁住一些无关的代码。
- 锁粗化:可以一次性执行完的不要多次加锁执行。
JVM包括类加载子系统、堆、方法区、栈、本地方法栈、程序计数器、直接内存、垃圾回收器、执行引擎
- 类加载子系统:
- 类加载子系统负责加载class信息,加载的类信息存放于方法区中。
- 直接内存
- 直接内存是在Java堆外的、直接向系统申请的内存空间。访问直接内存的速度会由于Java堆。出于性能的考虑,读写频繁的场合可能会考虑使用直接内存。
- 垃圾回收器
- 垃圾回收器可以对堆、方法区、直接内存进行回收。
- 执行引擎
- 执行引擎负责执行虚拟机的字节码,虚拟机会使用即时编译技术将方法编译成机器码后再执行。
- 堆
- 堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。
- 方法区
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
- 栈
- 栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
- 栈帧:每个方法从调用到执行的过程就是一个栈帧在虚拟机栈中入栈到出栈的过程。
- 局部变量表:用于保存函数的参数和局部变量。
- 操作数栈:操作数栈又称操作栈,大多数指令都是从这里弹出数据,执行运算,然后把结果压回操作数栈。
- 栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈
- 与栈功能相同,本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。
- 程序计数器(PC寄存器)
- 程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
- 年轻代:用于存放新产生的对象。
- 老年代:用于存放被长期引用的对象。(年轻代在垃圾回收多次都没有被GC回收的时候就会被放到老年代,以及一些大的对象)
- 年轻代与老年代通常占比为:2::1
- 持久带:用于存放Class,method元信息。
引用类型
- 强引用
强引用是使用最普遍的引用,我们写的代码,99.9999%都是强引用
只要某个对象有强引用与之关联,这个对象永远不会被回收,即使内存不足,JVM宁愿抛出OOM,也不会去回收。 - 软引用
只有在内存不足时,JVM才会回收该对象。
当内存不足时,会触发JVM的GC,如果GC后,内存还是不足,就会把软引用包裹的对象给干掉。 - 弱引用
不管内存是否足够,只要发生GC,弱引用就会被回收。 - 虚引用
无法通过虚引用来获取对一个对象的真实引用。
虚引用必须与ReferenceQueue一起使用。当GC准备回收一个对象时,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。
怎么判断对象是否可以回收
- 引用计数器:
- 为对象创建个引用,则计数器+1,引用被释放,计数器-1,当计数器为0时就可以被回收。
- 缺点:无法解决循环引用问题:两个对象互相引用,引用计数始终不为 0,导致无法被回收,而实际上应该需要被回收。
- 可达性分析:(JVM通过该方法判断)
- 从GC Roots开始向下搜索,途径的路径称为引用链。当一个对象没有引用链时,该对象可以回收;如object567,虽然相互关联,但他们与GC Roots互不可达,所以可以被回收
- 从GC Roots开始向下搜索,途径的路径称为引用链。当一个对象没有引用链时,该对象可以回收;如object567,虽然相互关联,但他们与GC Roots互不可达,所以可以被回收
GC算法 垃圾回收器
(1)标记清除算法
如果对象被标记后进行清除,会带来一个新的问题–内存碎片化。如果下次有比较大的对象实例需要在堆上分配较大的内存空间时,可能会出现无法找到足够的连续内存而不得不再次触发垃圾回收。
(2)复制算法(Java堆中新生代的垃圾回收算法)
先标记待回收内存和不用回收内存;
将不用回收的内存复制到新的内存区域;
就的内存区域就可以被全部回收了,而新的内存区域也是连续的;
缺点是损失部分系统内存,因为腾出部分内存进行复制。
(3)标记压缩算法(Java堆中老年代的垃圾回收算法)
对于新生代,大部分对象都不会存活,所以复制算法较高效,但对于老年代,大部分对象可能要继续存活,如果此时使用复制算法,效率会降低。
标记压缩算法首先还是标记,将不用回收的内存对象压缩到内存一端,此时即可清除边界处的内存,这样就能避免复制算法带来的效率问题,同时也能避免内存碎片化的问题。
老年代的垃圾回收算法称为“Major GC”。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。 简单讲一层一层往上传。
类加载器
- 启动类加载器(Bootstrap ClassLoader):
- 是虚拟机自身的一部分,用来加载<Java_HOME>/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;
- 扩展类加载器(Extension ClassLoader):
- 负责加载<java_home style=“box-sizing: border-box; -webkit-tap-highlight-color: transparent; text-size-adjust: none; -webkit-font-smoothing: antialiased; outline: 0px !important;”>\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;
- 应用程序类加载器(Application ClassLoader):
- 负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
- 自定义类加载器
内存泄漏的类型
-
ThreadLocal
- 使用ThreadLocal时,每个线程只要处于存活状态就可保留对其ThreadLocal变量副本的隐式调用,且将保留其自己的副本。使用不当,就会引起内存泄露。一旦线程不在存在,ThreadLocals就应该被垃圾收集,而现在线程的创建都是使用线程池,线程池有线程重用的功能,因此线程就不会被垃圾回收器回收。所以使用到ThreadLocals来保留线程池中线程的变量副本时,ThreadLocals没有显示的删除时,就会一直保留在内存中,不会被垃圾回收。
- 解决方法:
- 不再使用ThreadLocal时,调用remove()方法,该方法删除了此变量的当前线程值。
- 不要使用ThreadLocal.set(null),它只是查找与当前线程关联的Map并将键值对设置为当前线程为null。
-
static字段
- 大量使用static字段会潜在的导致内存泄露,在Java中,静态字段通常拥有与整个应用程序相匹配的生命周期。
- 示例:单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。
- 解决方法:
- 最大限度的减少静态变量的使用;
- 单例模式时,依赖于延迟加载对象而不是立即加载方式。
-
未关闭的资源
- 对于使用了BroadcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,从而造成内存泄漏。
- 解决方法:
- 调用它的close()函数将其关闭掉,然后再设置为null
- 注意
- 一般使用finally块关闭资源;关闭资源的代码,不应该有异常;
- jdk1.7后,可以使用try-with-resource块。
-
集合容器
- 我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,如果在不需要该对象时,没有把对象的引用从集合中清理掉,这样这个集合就会越来越大。如果这个List是临时的,那没问题,List被回收后里边的对象引用也就不会被持有了(对象不可达),对象引用也会被回收。如果这个List不是临时的,那么就会导致内存占用越来越大。如果这个集合是static的话,那情况就更严重了。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
- 解决方法:
- 如果是static类型的集合,在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
-
改变哈希值
- 当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了。在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
- 解决方法:
- 不要修改这个对象中的那些参与计算哈希值的字段
-
内部类持有外部类
- 若一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。在Java中,非静态内部类和匿名类内部类都会潜在持有它们所属的外部类的引用,但是静态内部类却不会。
- 解决办法:
- 如果内部类不需要访问外部类成员,考虑转换为静态内部类
-
finalize()方法
- 重写finalize()方法时,该类的对象不会立即被垃圾收集器收集,如果finalize()方法的代码有问题,那么会潜在的引发OOM;
- 解决办法:
- 尽量避免重写finalize();或者保证finalize方法没问题
-
常量字符串
- 如果我们读取一个很大的String对象,并调用了intern(),那么它将放到字符串池中,位于PermGen中,只要应用程序运行,该字符串就会保留,这就会占用内存,可能造成OOM。
- 解决方法:
- 增加PermGen的大小,-XX:MaxPermSize=512m;
- 升级Java版本,JDK1.7后字符串池转移到了堆中。