【语言学习】JAVA JVM相关 - hippowc/hippowc.github.io GitHub Wiki

堆分配相关参数

查看当前堆参数

public class TestMaxMinHeap {
    private static final int M = 1024 * 1024;
    public static void main(String[] args) {
        System.out.println("maxMemory:" + ( Runtime.getRuntime().maxMemory()/M) + "M");
        System.out.println("freeMemory:" + (Runtime.getRuntime().freeMemory()/M) + "M");
        System.out.println("totalMemory:" + (Runtime.getRuntime().totalMemory()/M) + "M");
    }
}

结果:

maxMemory:3641M
freeMemory:240M
totalMemory:245M

我们可以通过参数设置内存的大小

-Xmx 设置maxMemory

最大堆大小,默认值是物理内存的1/4,jvm分配空间不能超过最大堆内存大小,否则会抛出 OutofMemory 异常

-Xmx20m 设置 maxMemory 为 20 M

结果:

maxMemory:19M
freeMemory:17M
totalMemory:19M

-Xms 设置totalMemory

初始堆大小,默认值是物理内存的1/64,totalMemory 就是初始化堆大小,它的意思是一开始限定堆大小为多少,如果不够则可以扩充,但必须小于最大堆大小。

-Xms10m 设置 total Memory 为 10 M 

结果

maxMemory:18M
freeMemory:7M
totalMemory:9M

-Xmn 设置 新生代 大小

// 新生代大小设置为 2M,-Xmn2m -Xms20m -Xmx20m -XX:+PrintGCDetails
private static final int M = 1024*1024;
public static void main(String[] args) {
        byte[] b = null;
        for (int i = 0; i < 10; ++i) {
            b = new byte[1*M];
        }
    }

结果

[GC (Allocation Failure) [PSYoungGen: 1023K->504K(1536K)] 1023K->684K(19968K), 0.0019351 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 1536K, used 1476K [0x00000007bfe00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 1024K, 95% used [0x00000007bfe00000,0x00000007bfef33d8,0x00000007bff00000)
  from space 512K, 98% used [0x00000007bff00000,0x00000007bff7e010,0x00000007bff80000)
  to   space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
 ParOldGen       total 18432K, used 10420K [0x00000007bec00000, 0x00000007bfe00000, 0x00000007bfe00000)
  object space 18432K, 56% used [0x00000007bec00000,0x00000007bf62d100,0x00000007bfe00000)
 Metaspace       used 3390K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 373K, capacity 388K, committed 512K, reserved 1048576K

发生了一次Gc,要看下什么时候会发生Gc

-XX:SurvivorRatio -XX:NewRatio

默认情况下,eden:survivor from :survivor to 是8:1:1

-SurvivorRatio:设置新生代中 Eden space 和 Survivor space 的大小
-XX:SurvivorRatio=2 设置eden和survivor的比值,比值含义不是很清楚
-XX:NewRatio 设置年老代和年轻代的比值

-Xss

设置线程栈为128K,-Xss128k

GC相关参数

打印GC日志:-XX:+PrintGCDetails

测试代码

public class GCTest {
    public static void main(String[] args) {
        for (int i = 0; i < 30; i++) {
            byte[] b = new byte[1*1024*1024];
        }
    }
}

效果:

Heap
 PSYoungGen      total 76288K, used 16802K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
  eden space 65536K, 25% used [0x000000076ab00000,0x000000076bb688c8,0x000000076eb00000)
  from space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)
  to   space 10752K, 0% used [0x000000076eb00000,0x000000076eb00000,0x000000076f580000)
 ParOldGen       total 175104K, used 0K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
  object space 175104K, 0% used [0x00000006c0000000,0x00000006c0000000,0x00000006cab00000)
 Metaspace       used 3403K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 374K, capacity 388K, committed 512K, reserved 1048576K

程序生成了10M的对象,可以看到总的YongGen为76M,其中eden 65M,survivor 10M,没有发生GC,所有对象都在eden区

打印GC前后的详细堆栈信息 -XX:+PrintHeapAtGC

-XX:+TraceClassLoading 监控类的加载

java内存模型

java内存分为这么几个区域

  • 程序计数器:储存JVM当前执行bytecode的地址;如果有多个线程正在执行指令,那么每个线程都会有一个程序计数器,它是线程私有的。
  • Java 虚拟机栈:Java虚拟机栈也是线程私有的,每一条线程都拥有自己私有的Java 虚拟机栈,它与线程同时创建。栈帧可在系统的堆上分配内存
  • 本地方法栈:本地方法栈和Java虚拟机栈的作用相似;本地方法栈使用传统的栈(C Stack)来支持native方法。
  • Java 堆:是可供各线程共享的运行时内存区域;从内存回收的角度来看,它可以分为新生代和老年代
  • 方法区:方法区是线程共享的,它储存了每一个类的结构信息;方法区是堆的逻辑部分,在JDK1.7及以前的HotSpot JVM中,方法区位于永久代;到了Java 8,永久代被彻底地移出了JVM,取而代之的是元空间

垃圾回收的机制

如何确定一个对象是否可以被回收

1 引用计数算法:判断对象的引用数量

引用计数算法是通过判断对象的引用数量来决定对象是否可以被回收。引用计数算法是垃圾收集器中的早期策略。在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1;但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1。特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。任何引用计数为0的对象实例可以被当作垃圾收集。

引用计数算法很难解决对象之间相互循环引用的问题。如果两个对象内部互相引用,那么这两个对象永远不会被回收

2 可达性分析算法:判断对象的引用链是否可达

可达性分析算法是通过判断对象的引用链是否可达来决定对象是否可以被回收。程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链;当一个对象到 GC Roots 没有任何引用链相连,时,则证明此对象是不可用的,如下图所示。在Java中,可作为 GC Root 的对象包括以下几种:

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象;
  • 方法区中类静态属性引用的对象;
  • 方法区中常量引用的对象;
  • 本地方法栈中Native方法引用的对象;

垃圾收集的时机

对象优先在Eden分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次MinorGC

大对象直接进入老年代。所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

长期存活的对象将进入老年代。当对象在新生代中经历过一定次数(默认为15)的Minor GC后,就会被晋升到老年代中。

垃圾收集的算法

1、标记清除算法

标记-清除算法分为标记和清除两个阶段。该算法首先从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进行回收

问题:

  • 效率问题:标记和清除两个过程的效率都不高
  • 空间问题:标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,因此标记清除之后会产生大量不连续的内存碎片

2 复制算法

制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

事实上,现在商用的虚拟机都采用这种算法来回收新生代。因为研究发现,新生代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。

问题:

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

3 标记整理算法

标记整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适用于对象存活率高的场景(老年代)

标记整理算法与标记清除算法最显著的区别是:标记清除算法不进行对象的移动,并且仅对不存活的对象进行处理;而标记整理算法会将所有的存活对象移动到一端,并对不存活对象进行处理,因此其不会产生内存碎片。

4 分代收集算法

对于一个大型的系统,当创建的对象和方法变量比较多时,堆内存中的对象也会比较多,如果逐一分析对象是否该回收,那么势必造成效率低下。分代收集算法是基于这样一个事实:不同的对象的生命周期(存活情况)是不一样的,而不同生命周期的对象位于堆中不同的区域,因此对堆内存不同区域采用不同的策略进行回收可以提高 JVM 的执行效率。

当代商用虚拟机使用的都是分代收集算法:新生代对象存活率低,就采用复制算法;老年代存活率高,就用标记清除算法或者标记整理算法。Java堆内存一般可以分为新生代、老年代和永久代三个模块

  • 新生代(Young Generation)新生代的目标就是尽可能快速的收集掉那些生命周期短的对象,一般情况下,所有新生成的对象首先都是放在新生代的。新生代发生的GC也叫做MinorGC,MinorGC发生频率比较高,不一定等 Eden区满了才触发。
  • 老年代(Old Generation)老年代存放的都是一些生命周期较长的对象,在新生代中经历了N次垃圾回收后仍然存活的对象就会被放到老年代中。当老年代满时会触发Major GC(Full GC),老年代对象存活时间比较长,因此FullGC发生的频率比较低。
  • 永久代(Permanent Generation)永久代主要用于存放静态文件,如Java类、方法等。永久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如使用反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置一个比较大的永久代空间来存放这些运行过程中新增的类。

jvm工具的使用

jstat

jstat -gc {pid}:

S代表:survivor,E代表Eden,O代表Old,M代表Meta元空间--jdk1.8后替代永久代
C代表:总容量,U代表已经使用
YGC:年轻代GC次数,YGCT:年轻代GC时间,FGC:老年代GC次数或者FullGc次数,FGCT:FullGc时间
空间单位--字节,时间单位--s

jstat -gcutil {pid}

S0: 第一个survivor区使用百分比;E:eden区使用百分比;O:老年代使用百分比;M:元空间使用百分比,以及GC时间

jstat -class {pid}

loaded:加载了多少类,Bytes占用了多少字节

jstack

jstack {pid}, jstack -F {pid} :查看线程堆栈

输出含义
线程名称;prio:线程优先级;tid:Java Thread id;nid:native线程的id
其中tid是java线程id,nid是linux系统的线程id,但是是16进制的,我们就通过nid定位那个线程耗时最长
线程状态(不是Thread状态):死锁-Deadlock,执行中-Runnable,等待资源-Waiting on condition,等待获取监视器-Waiting on monitor entry,暂停-Suspended,对象等待中-Object.wait() 或 TIMED_WAITING,阻塞-Blocked,停止-Parked

几种线程状态与相应的代码

1 Runnable
while (true) {...} 或者
等待IO:
int i = is.read();

2 Object.wait()
wait()..

3 deadlock
jstack 会帮忙定位到java级别的死锁,如果程序中有死锁会在最上方提示

4 waiting on condition -- TIMED_WAITING (sleeping)
Thread.sleep(1000)

5 waiting on condition -- WAITING (parking)
等待锁

6 blocked
阻塞状态。说明线程等待资源超时

// TODO

jmap

jmap -heap {pid} : 查看java堆信息,和jstat -gc看的东西差不多

jmap -histo {pid} :查看java堆中对象详细占用情况,可以看到有多少类,多少对象,占用多少字节

jmap -histo:live {pid} : 查看存活对象信息

jmap -dump:format=b file={filename} {pid} :将java堆对象使用二进制dump下来,保存到文件里,然后是用jhat查看

jhat -J-Xmx1024M [file]