Java虚拟机复习
一、内存分配
1.1 JVM 内存结构
Java 虚拟机的内存空间分为 5 个部分:
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 堆
- 方法区
程序计数器(PC)
为什么需要程序计数器
因为Java虚拟机的多线程是通过线程轮流切换
并分配处理器执行时间
的方式来实现的。任意时刻,一个处理器只执行一条指令,为了进程切换后恢复到正确的执行位置,所以才有了程序计数器
作用
记录当前线程的执行字节码的位置,线程私有,也就是每个线程都有一个单独的计数器来记录。
特点
- 不会发生OutOfMemoryError
- 执行Native方法,计数器值为空
- 线程私有
Java虚拟机栈
描述Java方法执行的内存模型。
方法在执行的时候会创建一个栈帧,方法的调用到完成,对应着栈帧
在虚拟机栈中入栈
到出栈
的过程。
一个线程的方法调用可能很长,很多方法都处于执行状态。对于执行引擎来说,只有处于栈顶的栈帧(当前栈帧
)才有效,与之相关联的方法是当前方法
。
栈帧用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。
局部变量表
局部变量表存放编译期可知的基本数据类型
、对象引用
和return Address类型
局部变量表所需要的内存空间在编译期间完成分配。
特点
- 线程私有
- 线程请求的栈深度大于JVM允许深度,抛出
StackOverflowError
异常 - 虚拟机栈动态扩展时,无法申请足够的内存,抛出
OutOfMemoryError
异常
本地方法栈
堆
用来存放对象的内存空间,几乎所有的对象都存储在堆中。
堆的特点
- 线程共享
- 垃圾回收的主要场所
方法区
方法区的定义
运行时常量池
二、垃圾回收
垃圾回收主要解决三个问题(回收哪些Which,什么时候回收WHEN,如何回收HOW)
一、回收哪些
这三个问题,最主要的还是第一个,Which回收哪些,评断回收还是不回收的标准是看对象是否被引用
引用分为四种:
- 强引用:一个对象被一个引用所指向。绝对不会被JVM回收的,即使内存不过用
- 软引用:只有在堆内存不够用的时候才会被回收。使用SoftReference实现
- 弱引用:弱引用相对于软引用,引用级别更低。只要垃圾回收器启动了回收,就会被回收掉。绝WeakReference实现
- 虚引用:虚引用的主要作用是跟踪对象被垃圾回收的状态。虚引用对对象本身没有太大影响,必须和引用队列(ReferenceQueue)联合使用
1、引用计数法
如果两个对象相互引用,就不会被回收,当然,GC并没有采用这种算法
2、根搜索算法
从根开始,沿着整个对象图上的每条链接,确定可达的对象,如果对象不可达,则作为垃圾收集
是对(1)的改进,根对象到达某一对象不可达,GC就会对其回收。
public static void main(String[] args) {
Node n1 = new Node();
Node n2 = new Node();
Node n3 = new Node();
n1.next = n2;
n3 = n2;
n2 = null;
}
二、何时回收,如何回收
这就需要垃圾收集算法来解决,但讲垃圾回收算法之前需要明白一个分代的概念
1、分代的策略
绝大多数的对象不会被长时间引用,这些对象在其Young期间就会被回收
很老的对象和很新的对象之间很少存在相互引用
Young代
大部分垃圾回收器对Young代都采用复制算法,为什么?因为Young代处于可达的对象数量少,所以复制成本不大
Young代由一个Eden区和2个Survivor区构成。绝大多数对象先分配到Eden区中,Survivor区中的对象都至少经历过一次垃圾回收
为什么要有2个Survivor区,是因为其中一个Survivor是空的,来存放Young代的对象。最后会清空Eden区和第一个Survivor区
思考:Survivor的大小设置的变化会产生什么影响
Old代
Young代的对象经过多次垃圾回收依然没有被回收,就会被转移到Old代
随着时间流逝,Old代的对象会越来越多,因此Old代的空间要与Young代的空间大
Old代的垃圾回收的两个特征:Old代垃圾回收的执行频率不需要太高,因为死的少。每次回收需要更长的时间来完成(如何理解,因为对象多吧)
垃圾回收通常会采用标记压缩算法。因为对象不会很快死亡,也不会大量产生内存碎片
Permanent代
主要用于装载Class、方法等信息
垃圾回收机制通常不会回收这一代的对象。
服务器程序通常会加载很多类,需要加大Permanent代内存。
OutOfMemoryError:PermGen space错误
2、垃圾回收算法
标记-清除算法
标记所有需要回收的对象,标记完成后,统一回收所有被标记的对象,
不足之处,标记和清除的效率不高,标记清除后会产生大量不连续的内存碎片
复制算法
为了解决效率问题,将内存分为大小相等的两块,每次使用一块,当这一块内存使用完了之后,将存活的对象放到另一块内存中,然后对原先那一半内存进行回收。实现简单,运行高效,不过代价是将内存缩小一半,代价过高。
标记-整理算法
是对标记清除算法的改进,进行标记好了之后,将存活的对象都向一边移动,然后直接清理掉端边界以外的内存
分代收集算法
商业虚拟机都采用“分代收集”算法,根据对象的存活周期将内存分为几块,一般是将java堆分成新生代和老年代,如果新生代有很少的存活对象,就用复制算法,老年代有很多存活对象,就用标记-清除或标记-整理算法
HotSpot虚拟机下的垃圾收集器
由于内存中的对象,是按存活周期存放在不同的内存块中的,所以,我们选择不同的算法来针对不同的内存块进行垃圾收集。从而,对于,不同的内存块,我们需要有不同的垃圾收集器。
新生代的垃圾收集器有:Serial收集器
、ParNew收集器
、Parallel Scavenge收集器
老年代的垃圾收集器有:Serial Old收集器
、Parallel Old收集器
、CMS收集器
、G1收集器
Serial收集器/Serial Old收集器
串行,是单线程的,使用“复制”算法。当它工作时,必须暂停其它所有工作线程
。特点:简单而高效。一般用于Client模式的JVM中
Serial Old是老年代的单线程收集器,使用标记-整理算法。
ParNew收集器
ParNew收集器,是Serial收集器的多线程版。是运行在Server模式下的虚拟机中首选的新生代收集器。除了Serial收集器外,目前只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
吞吐量优先收集器,吞吐量=程序运行时间/(JVM执行回收时间+程序运行时间),是server模式JVM的默认配置
Parallel Old收集器
老年代的多线程收集器,使用标记-整理算法,吞吐量优先,适合于Parallel Scavenge搭配使用
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,使用“标记-清除”算法。
回收线程数=(CPU核心数+3)/4
CMS收集器分4个步骤进行垃圾收集工作:
1、初始标记 2、并发标记 3、重新标记 4、并发清除
其中“初始标记”、“重新标记”是需要暂停其它所有工作线程的。
G1收集器
G1(Garbage First)收集器,基于“标记-整理”算法,可以非常精确地控制停顿。
可以不与其他收集器搭配,独立收集新生代和老年代。
三、内存管理技巧
1、尽量使用直接量:String str = "hello"
2、使用StringBuilder和StringBuffer进行字符串连接
3、尽早释放无用对象的引用
Object obj = new Object();obj = null;这行代码并不能发挥C++中的delete和free作用,其作用仅仅是断开obj 与new Object()的关联,new Object所占用的内存并没有释放掉。以此来诱发GC对其进行回收。如果是在方法中,其实要考虑两种情况,多数情况下不需要这么写,对象会随着因为方法调用的结束而结束。但如果obj =null之后,还有耗时耗内存的操作的话,就需要这样写。
四、设置Java虚拟机内存的一些参数
- -Xmx 设置堆内存的最大容量
- -Xms 设置堆内训初始容量
- -XX:NewSize = size 设置Young代内存的默认容量
- -XX:SurvivorRatio = 8 设置Young代中eden/survivor的比例
- -XX:MaxNewSize = size 设置Young代内存的最大容量
- -XX:PermSize = size 设置永久代的默认容量
- -XX:MaxPermSize = size 设置永久代内存的最大容量
五、一次完整的GC流程
从ygc到fgc
六、监控工具
jstat
jstat -gc 14122 250 20
jstat -gcutil 14122 250 20
一、Class结构
dir
参考文档
[1]: JVM 解剖公园(19): 锁省略
[2]: Java性能调优实战视频全集
关于作者
后端程序员,五年开发经验,从事互联网金融方向。技术公众号「清泉白石」。如果您在阅读文章时有什么疑问或者发现文章的错误,欢迎在公众号里给我留言。