/**
0. cd 文件所在目录
1. 将java文件编译成字节码文件 javac Math.java -->Math.class
2. 运行 java Math
*/
public class Math {
public int add(int a, int b) {
return a + b;
}
public static void main(String[] args) {
Math math = new Math();
System.out.println(math.add(1, 2));
}
}
复制代码
我们都知道java语言的一大特性就是夸平台,我们日常写的代码都是编译成字节码文件,运行在jvm虚拟机中,在开始JVM内存模型之前请简单运行此案例,并记住他
如图所示,JVM运行时一共分为如下几块数据区
我们都知道java 线成是运行在栈上面的,每个线程启动时会在栈上分配一块区域,供线程使用。当一个方法被调用时,Java 虚拟机会为该方法创建一个栈帧,并将其压入栈顶。当方法执行完毕后,虚拟机会弹出该栈帧,将控制权返回给调用该方法的方法。栈针包含局部变量表、操作数栈、动态链接、方法出口
我们都知道 我们在日常使用java的时候 通过 new 关键字创建的对象 大部分情况下都存储在堆中 (有一些情况会触发栈上分配后续讲),那么我们的对象 是个什么样的结构呢,以及对象创建的流程是什么样的呢?
我们都知道java的对象是被分配在栈上的,当我们的对象没有被引用的时候,会通过gc 回收掉,但是如果创建很多没有被引用的对象 的话会给gc造成一定的压力,在我们创建对象的过程中jvm会通过一中逃逸分析技术来判断我们创建的对象是否会逃逸处我们的方法 ,如果无法逃逸出去,并且空间足够就会实行栈上分配,这样做就会随着线程运行完直接销毁空间,减少gc压力
public User save1(){
User user = new User();
user.setId(1);
user.setName("admin");
//TODO 保存数据库
return user;
}
public void save2(){
User user = new User();
user.setId(1);
user.setName("admin");
//TODO 保存数据库
}
复制代码
save1 方法中创建的对象,最后会返回,也就是说user 对象有可能会被方法外的变量引用,但是save2 方法中创建的对象,逃不出save2 方法, 当save2方法运行结束 user 对象就会成为垃圾对象,对于这样的对象我们其实可以把对象分配到栈上,随着方法的技术释放内存
JVM对于这种情况可以通过开启逃逸分析参数(-XX:+DoEscapeAnalysis)来优化对象内存分配位置,使其通过标量替换优先分配在栈上(栈上分配),JDK7之后默认开启逃逸分析,如果要关闭使用参数(-XX:-DoEscapeAnalysis)
**标量替换:**通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是将该对象成员变量分解若干个被这个方法使用的成员变量所代替,这些代替的成员变量在栈帧或寄存器上分配空间,这样就不会因为没有一大块连续空间导致对象内存不够分配。开启标量替换参数(-XX:+EliminateAllocations),JDK7之后默认开启。
**标量与聚合量:**标量即不可被进一步分解的量,而JAVA的基本数据类型就是标量(如:int,long等基本数据类型以及reference类型等),标量的对立就是可以被进一步分解的量,而这种量称之为聚合量。而在JAVA中对象就是可以被进一步分解的聚合量。
测试栈上分配
/**
* 栈上分配,标量替换
* 代码调用了1亿次createUser(),如果是分配到堆上,大概需要1GB以上堆空间,如果堆空间小于该值,必然会触发GC。
*
* 使用如下参数不会发生GC
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* 使用如下参数都会发生大量GC
* -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
* -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
*/
public class Test {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
createUser();
}
System.out.println(System.currentTimeMillis() - start);
}
public static void createUser() {
User user = new User();
user.setId(1);
user.setName("admin");
}
}
复制代码
我们知道,大对数对象都是在eden区分配,当eden 区空间不足的时候会触发一次minor gc 将对象已到s0s1 区,我们先来测试一下,在测试之前我们来看下 minor gc 和full gc 有什么不同
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可
JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变化可以设置参数-XX:-UseAdaptiveSizePolicy
示例:
public class GCTest {
/**
* -XX:+PrintGCDetails
*
* @param args
*/
public static void main(String[] args) {
byte[] allocation1, allocation2/*, allocation3, allocation4, allocation5, allocation6*/;
allocation1 = new byte[60 * 1024 * 1024];
// allocation2 = new byte[8 * 1024 * 1024];
}
}
Heap
PSYoungGen total 76288K, used 65536K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)
eden space 65536K, 100% used [0x000000076ab00000,0x000000076eb00000,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 3276K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 362K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
我们看到 eden 分配 60M 的空间 此时 eden区已经站慢,我们在打开 allocation2 的注释,在在看gc 日志
[GC (Allocation Failure) [PSYoungGen: 65372K->608K(76288K)] 65372K->62048K(251392K), 0.0220770 secs] [Times: user=0.07 sys=0.02, real=0.02 secs]
Heap
PSYoungGen total 76288K, used 9455K [0x000000076ab00000, 0x0000000774000000, 0x00000007c0000000)
eden space 65536K, 13% used [0x000000076ab00000,0x000000076b3a3ef8,0x000000076eb00000)
from space 10752K, 5% used [0x000000076eb00000,0x000000076eb98020,0x000000076f580000)
to space 10752K, 0% used [0x0000000773580000,0x0000000773580000,0x0000000774000000)
ParOldGen total 175104K, used 61440K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)
object space 175104K, 35% used [0x00000006c0000000,0x00000006c3c00010,0x00000006cab00000)
Metaspace used 3276K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 362K, capacity 388K, committed 512K, reserved 1048576K
我们看到,此时触发了一次minorgc 会把eden 区 60M数据向**Survivor 区移动,但是survior 区空间 不足,最后将数据移动到了老年代**
复制代码
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下有效
jvm 每进行一次minor gc 每有没回收掉的对象 年龄就会+1 (对象年龄在对象头中又一个字段表示),当对象年龄大于 Jvm 设定的值时 (默认是 15 cms 回收器 默认是 6),就是把对象从新生代 挪到老年代 ,对象晋升到老年代的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
当前放对象的Survivor区域里(其中一块区域,放对象的那块s区),一批对象的总大小大于这块Survivor区域内存大小的50%(-XX:TargetSurvivorRatio可以指定),那么此时大于等于这批对象年龄最大值的对象,就可以直接进入老年代了.
例如
-XX:MaxTenuringThreshold = 15
当Survivor 区满的时候,对象按年龄从小算起 年领1+年龄2+年龄N 对象,指导对象占比空间超过 50%,会将 大于该年龄的以上的对象移入老年代
正对以上面的案例,年领1+年龄2 的对象 > 50%, 此时就会把 年龄≥2 的对象提前移入老年代
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间
如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象)
就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了
如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾,如果回收完还是没有足够空间存放新的对象就会发生"OOM"
当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”
一个对象没被一个变量引用,对象的引用计数就会加1 当对象引用计数为0的时候,可以判断次对象是垃圾对象。但是这存在一个循环引用的问题
public class CycleRefObject {
public CycleRefObject instance = null;
public static void main(String[] args) {
CycleRefObject a = new CycleRefObject(); // a 对象引用计数此时为1
CycleRefObject b = new CycleRefObject(); // b 对象引用计数此时为1
b.instance = a; // a 对象引用计数此时 +1 变为2
a.instance = b; // b 对象引用计数此时 +1 变为2
a = null; // a 对象引用计数此时 -1 变为1
b = null; // b 对象引用计数此时 -1 变为1
// 当我们程序运行完,a b 对象引用计数 分别为1 无法做到清零,这就是我们常说的循环引用
}
}
复制代码
将**“GC Roots”** 对象作为起点,从这些节点开始向下搜索引用的对象,找到的对象都标记为非垃圾对象,其余未标记的对象都是垃圾对象
GC Roots根节点:线程栈的本地变量、静态变量、本地方法栈的变量等等
java 中一共有四种引用类型 强引用、弱引用、软引用、虚引用
页面更新:2024-05-01
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号