Java源码分析-String类分析 javaspring源码
yuyutoo 2024-10-21 12:03 10 浏览 0 评论
文/小图灵视界
本头条号将会继续分析JDK8的源码,欢迎关注和收藏,也会将分析笔记开源。
位置:java.lang
String类是除了Object类外,最基础的类,最重要的类,在开发过程中使用最常用的类,也是使用最多的类。String类的定义如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
String类由final修饰符修饰,是不可被子类继承的类,是不可改变的类。String类实现了Serializable、Comparable、CharSequence三个接口,实现Serializable接口,对象就可以进行序列化,实现Comparable接口,String类的对象之间就可以进行比较,CharSequence接口提供了如下方法:
//计算长度
int length();
//返回某个位置的字符
char charAt(int index);
//返回从start到end位置的子CharSequence
CharSequence subSequence(int start, int end);
//输出的字符串
public String toString();
String实现CharSequence的上述所有接口方法,为String提供计算长度、定位字符串某个位置的字符、返回字符串的子字符串以及标准的输出字符。String类有如下几个重要的属性:
//保存字符串
private final char value[];
//缓存字符串的哈希码
private int hash;
//用于指定哪些字段需要被默认序列化
private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
String类底层本质是用char数组进行保存字符的,char数组value属性用来保存字符串,hash属性是哈希码的值,默认值为0,serialPersistentFields属性用于指定哪些字段需要被默认序列化。这些属性都是final修饰,不可改变,String类也没有提供获取和修改这些属性的方法。
常用的构造方法
//默认为空字符串
public String() {
this.value = "".value;
}
//用字符创构建字符串
public String(String original) {
this.value = original.value;
this.hash = original.hash
}
//用char数组初始化
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
//用StringBuffer初始化
public String(StringBuffer buffer) {
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
//用StringBuilder初始化
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
String类是不可变的类,在初始化的过程中都是采用副本进行赋值。String() 构造器是默认的构造器,用空字符串的副本进行初始化( this.value = "".value;),String(String original)构造器将字符串original的value和hash属性的副本赋值给新的字符串的value和hash属性。String(char value[])将char 数组的复制给this.value,是用 Arrays.copyOf方法复制的, Arrays.copyOf方法底层是新创建一个char数组,然后利用本地方法System.arraycopy将value数组的值复制给新创建char数组。
String(StringBuffer buffer) 构造函数将StringBuffer的char数组复制给字符串,这个构造器用synchronized对buffer加锁,原因是String类是不可变的,在多线程的环境下是安全的,但是StringBuffer类是不安全的,为了保证用StringBuffer初始化字符串时的安全性而加锁。最后一个常用的构造方法是String(StringBuilder builder),利用StringBuilder进行初始化,将StringBuilder的char数组复制给字符串,StringBuilder类是安全的。
用的方法
public int length() {
return value.length;
}
length()方法返回字符串的长度,这个长度就是底层char数组的长度。
public boolean isEmpty() {
return value.length == 0;
}
isEmpty()方法判断字符串的长度是否等于0,实际判断的是底层char数组的长度是否等于0。这个方法具有误导性,实际上并不是判空操作,在开发过程中判断一个字符串是否为空,常用if(str==null || str.length()==0)进行判空操作。
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
charAt(int index) 方法返回给定位置index的字符,首先判断给定位置的index是否合法,如果合法,返回char数组value的index位置的字符,否则,返回StringIndexOutOfBoundsException异常。
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
getChars(char dst[], int dstBegin)方法将字符串的value数组从dst数组的dstBegin位置复制到dst数组中。利用了 System.arraycopy本地方法复制。
public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) {
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
Objects.requireNonNull(dst);
int j = dstBegin;
int n = srcEnd;
int i = srcBegin;
//重新复制一个char数组
char[] val = value; /* avoid getfield opcode */
while (i < n) {
//将char强转为byte
dst[j++] = (byte)val[i++];
}
}
getBytes方法将字符串的char数组复制给byte数组。srcBegin参数表示的从value数组的srcBegin位置,srcEnd参数表示的从value数组的srcEnd位置,复制srcBegin位置到srcEnd位置的字符。这个方法首先判断参数是否越界,getBytes方法并不是直接操作字符串中的chat数组value,而是复制了一个value的副本val,原因是避免getfield操作,最后用循环遍历副本val各个位置的值转为byte类型赋值给dst数组。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String类重写了Object父类的equals方法,首先判断比较的对象anObject是否是这个对象this,如果是直接返回true,否则继续判断这个对象是否属于String类型,如果是的话,判断下anObject对象的value数组的长度是否等于这个字符串的value数组的长度,如果是的话,遍历两个比较对象的value数组中的每个位置的字符是否相等,如果相等返回true,否则就返回false。
equals方法比较巧妙,先判断是否是this和判断两个比较字符之间长度,这两个判断提高了快速判断两个字符串是否相等,只有这两个判断都成立了,才遍历比较两个字符相同位置的字符是否相等。
public boolean contentEquals(CharSequence cs) {
// Argument is a StringBuffer, StringBuilder
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
// Argument is a String
if (cs instanceof String) {
return equals(cs);
}
// Argument is a generic CharSequence
char v1[] = value;
int n = v1.length;
if (n != cs.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != cs.charAt(i)) {
return false;
}
}
return true;
}
contentEquals方法是判断字符序列(CharSequence)的内容与字符串的内容是否相等。传入的参数是CharSequence,首先判断传入的参数是否是AbstractStringBuilder类型,AbstractStringBuilder类型是StringBuffer类和StringBuilder类的父类,如果属于AbstractStringBuilder类型,则判断是否属于StringBuffer类型,如果是则用synchronized加锁调用nonSyncContentEquals方法,如果属于StringBuilder类则不加锁调用nonSyncContentEquals方法。如果参数属于String类型,则调用equals方法,如果属于一般的CharSequence类型,则判断字符串的长度与比较的字符序列的长度是否相等,如果不相等返回false,否则继续遍历比较两个比较对象底层char数组的每个位置的字符是否相等。最后再来分析下nonSyncContentEquals方法,nonSyncContentEquals方法如下:
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
char v1[] = value;
char v2[] = sb.getValue();
int n = v1.length;
if (n != sb.length()) {
return false;
}
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
return true;
}
nonSyncContentEquals方法就是判断两个比较对象底层的char数组之间的相同位置的字符是否相等。首先判断比较序列之间的长度是否相等,然后遍历两个对象底层数组相同位置的字符是否相等。
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
compareTo方法比较两个字符串的大小,首先获取两个字符串的长度,然后获得两个字符串的长度中较小的长度lim,遍历比较两个字符串小于lim范围里字符的大小,当两个字符串相同位置的字符不相等时,返回两个字符相减的结果,如果较小长度lim范围里,两个字符串的字符都相等,那么比较两个字符串的长度大小。
public boolean startsWith(String prefix, int toffset) {
char ta[] = value;
int to = toffset;
char pa[] = prefix.value;
int po = 0;
int pc = prefix.value.length;
// Note: toffset might be near -1>>>1.
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
while (--pc >= 0) {
if (ta[to++] != pa[po++]) {
return false;
}
}
return true;
}
//指定toffset参数为0,说明字符串是否包含指定的前缀prefix,索引从0开始。
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
//从value.length - suffix.value.length索引开始的子字符串是否包含指定的后缀suffix
public boolean endsWith(String suffix) {
return startsWith(suffix, value.length - suffix.value.length);
}
startsWith方法是从指定索引开始的字符串的子字符串是否包含指定的前缀。首先进行边界的判断,参数toffset是否小于0,以及toffset是否大于字符串的长度减去指定前缀的长度,如果toffset小于0或者toffset大于字符串的长度减去指定前缀的长度,则返回false,否则,遍历指定前缀与指定索引开始的字符串的子字符串的字符是否都相等,否则返回false。如果都不满足上述的条件,返回true。
startsWith(String prefix)方法和endsWith(String suffix) 都是在startsWith(String prefix, int toffset)方法的基础上来的,只是toffset的值不一样,当toffset等于0时,就变成了startsWith(String prefix)方法;当toffset等于value.length - suffix.value.length时,就变成了endsWith(String suffix)方法。
public int indexOf(int ch, int fromIndex) {
//边界判断
final int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// Note: fromIndex might be near -1>>>1.
return -1;
}
//当ch<65535,一个字节8位,两个字节16位,2的16次方等于65535
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
在讲解indexOf(int ch, int fromIndex) 方法时,先了解下Unicode 编码,Unicode 编码是为了解决能够使计算机实现跨语言、跨平台的文本转换及处理。可以容纳世界上所有文字和符号的字符编码方案。本来想讲解下Unicode ,但是发现网上已经通俗易懂讲解Unicode 编码的文章,所以就不展开来讲了,感兴趣的可以参考如下博客:
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
indexOf(int ch, int fromIndex) 方法首先进行边界判断,如果输入的参数字符ch小于65535,就直接遍历String类型的char数组value,参数小于65535,说明该字符由两个字节组成。当value数组中的字符与参数ch相等,就返回该位置的索引。如果输入的参数字符大于等于65535,就调用indexOfSupplementary(int ch, int fromIndex) 方法,该方法如下:
private int indexOfSupplementary(int ch, int fromIndex) {
//判断是否是合法的Unicode 对应的码点
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
final char hi = Character.highSurrogate(ch);
final char lo = Character.lowSurrogate(ch);
final int max = value.length - 1;
for (int i = fromIndex; i < max; i++) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
当输入参数大于等于两个字节,即大于等于65535时,调用indexOfSupplementary(int ch, int fromIndex) 方法,当输入的参数字符ch是合法的码点,获取输入参数ch的大端字符和小端字符,比较ch的大端字符和小端字符与value数组第i个位置和第i+1个位置的值是否相等,相等就返回索引i。如果没有找到相等的值,就返回-1。
public int lastIndexOf(int ch, int fromIndex) {
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// handle most cases here (ch is a BMP code point or a
// negative value (invalid code point))
final char[] value = this.value;
int i = Math.min(fromIndex, value.length - 1);
for (; i >= 0; i--) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return lastIndexOfSupplementary(ch, fromIndex);
}
}
private int lastIndexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
char hi = Character.highSurrogate(ch);
char lo = Character.lowSurrogate(ch);
int i = Math.min(fromIndex, value.length - 2);
for (; i >= 0; i--) {
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
return -1;
}
lastIndexOf(int ch, int fromIndex)方法与indexOf(int ch, int fromIndex) 方法逻辑基本一样,只是两者在遍历数组value的顺序不一样,lastIndexOf(int ch, int fromIndex)是从数组后面往前面遍历,indexOf(int ch, int fromIndex) 是从数组前面往后面遍历。
indexOf和lastIndexOf方法都很多其他参数的重载方法,都是在indexOf(int ch, int fromIndex) 方法和lastIndexOf(int ch, int fromIndex)方法的基础上拓展而来的,只要理解这两个方法,其他重载的方法都比较简单,这里就不分析了。
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
substring(int beginIndex)方法返回从beginIndex位置开始的子字符串,首先判断beginIndex是否合法,当beginIndex小于0或者子字符串的小于0时(value.length - beginIndex),抛出StringIndexOutOfBoundsException异常。当beginIndex等于0时,直接返回this,返回字符串是原来的字符串,否则创建新的String对象返回。
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
substring(int beginIndex, int endIndex) 方法是返回从beginIndex开始到endIndex位置结束的子字符串。substring(int beginIndex, int endIndex) 首先判断beginIndex和endIndex的合法性,当beginIndex和endIndex不合法时,抛出StringIndexOutOfBoundsException异常。beginIndex等于0并且endIndex 等于 value.length,返回this(原字符串),否则重新创建一个String对象返回。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
concat(String str)方法的作用是拼接两个字符串,当拼接的字符串str的长度等于0,直接返回原字符串。否则通过 Arrays.copyOf将value数组复制到长度为len + otherLen的字符数组buf中,然后将字符串复制到buf数组中,最后用buf数组创建新的字符串返回。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
//先找到字符串中与oldChar相等的值的位置i
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
//如果找到字符串中与oldChar相等的值的位置i
if (i < len) {
char buf[] = new char[len];
//将小于i位置的字符保存在buf中
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
//将所有等于oldChar值的替换为newChar,否则等于原来的字符串
while (i < len) {
char c = val[i];
//如果i位置的
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//返回新创建的String对象
return new String(buf, true);
}
}
return this;
}
replace(char oldChar, char newChar)方法的作用是将字符串中老字符oldChar替换为新字符newChar,如果oldChar等于newChar,直接返回原来的字符串,否则先找到字符串中与oldChar相等的第一个字符的位置i。如果找到字符串中与oldChar相等的第一个字符的位置i,将用两步来替换旧的值,第一步将小于i位置的字符保存在buf,第二部将所有等于oldChar值的替换为newChar,如果不等于oldChar将原来的字符串保存在buf,最后通过buf创建新的String对象。
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
//从前面找到不等于空字符' '的第一个位置
while ((st < len) && (val[st] <= ' ')) {
st++;
}
//从后面找到不等于空字符' '的第一个位置
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果原来字符串前面和后面存在空字符串' ',返回去除空字符串' '的子字符串
//否则,返回原来的字符串
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
trim() 方法是去除字符串两边的空字符‘ ’, trim()方法从字符串的前面和后面分别遍历字符,寻找不等于空字符‘ ’的位置,如果原来字符串前面和后面存在空字符串' ',返回去除空字符串' '的子字符串,否则,返回原来的字符串。
public char[] toCharArray() {
// Cannot use Arrays.copyOf because of class initialization order issues
char result[] = new char[value.length];
System.arraycopy(value, 0, result, 0, value.length);
return result;
}
toCharArray()方法返回字符串的char数组,通过本地方法 System.arraycopy复制字符串的字符到char数组中。该方法中有一段注释说明不能使用Arrays.copyOf复制字符到char数组中,是因为类初始化的顺序问题。这注释也没有很清楚解释为什么toCharArray()中不能用Arrays.copyOf复制字符。经过查找资料,找到一段清楚的解释:
引用:https://my.oschina.net/u/3268478/blog/3011267
虽然String 和Arrays 都属于rt.jar中的类,但是BootstrapClassloader 在加载这两个类的顺序是不同的。所以当String.class被加载进内存的时候,Arrays此时没有被加载,所以直接使用肯定会抛异常。而System.arrayCopy是使用native代码,则不会有这个问题。
public native String intern();
intern()是本地方法,当调用 intern 方法时,如果常量池中已经该字符串,则返回池中的字符串;否则将此字符串添加到常量池中,并返回字符串的引用。对于两个字符串s和t,如果s.intern() == t.intern()为true,则s.equals(t)为true。intern()方法在JVM的函数为Java_java_lang_String_intern:
JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
Java_java_lang_String_intern函数返回调用JVM_InternString函数的结果。JVM_InternString函数的方法为:
JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
JVMWrapper("JVM_InternString");
JvmtiVMObjectAllocEventCollector oam;
//如果java中的字符串等于null,直接返回null
if (str == NULL) return NULL;
//将java的字符串jstring转为c++中的普通对象oop
oop string = JNIHandles::resolve_non_null(str);
//从字符常量池StringTable中查找字符串,
oop result = StringTable::intern(string, CHECK_NULL);
//将从字符常量池StringTable中查找字符串的转换成java的字符串
return (jstring) JNIHandles::make_local(env, result);
JVM_END
JVM_InternString函数从StringTable常量池中寻找字符串,StringTable相等于java中hashTable表,如果不存在这个字符串,那么就将这个字符串的引用保存在StringTable常量池中,如果存在这个字符串,就返回这个字符串的引用。StringTable常量池中的字符串越来越多的时候,查找效率会越来越低,所有也不能大量的使用intern(),否则导致性能下降。
相关推荐
- 当 Linux 根分区 (/) 已满时如何释放空间?
-
根分区(/)是Linux文件系统的核心,包含操作系统核心文件、配置文件、日志文件、缓存和用户数据等。当根分区满载时,系统可能出现无法写入新文件、应用程序崩溃甚至无法启动的情况。常见原因包括:...
- 玩转 Linux 之:磁盘分区、挂载知多少?
-
今天来聊聊linux下磁盘分区、挂载的问题,篇幅所限,不会聊的太底层,纯当科普!!1、Linux分区简介1.1主分区vs扩展分区硬盘分区表中最多能存储四个分区,但我们实际使用时一般只分为两...
- Linux 文件搜索神器 find 实战详解,建议收藏
-
在Linux系统使用中,作为一个管理员,我希望能查找系统中所有的大小超过200M文件,查看近7天系统中哪些文件被修改过,找出所有子目录中的可执行文件,这些任务需求...
- Linux 操作系统磁盘操作(linux 磁盘命令)
-
一、文档介绍本文档描述Linux操作系统下多种场景下的磁盘操作情况。二、名词解释...
- Win10新版19603推送:一键清理磁盘空间、首次集成Linux文件管理器
-
继上周四的Build19592后,微软今晨面向快速通道的Insider会员推送Windows10新预览版,操作系统版本号Build19603。除了一些常规修复,本次更新还带了不少新功能,一起来了...
- Android 16允许Linux终端使用手机全部存储空间
-
IT之家4月20日消息,谷歌Pixel手机正朝着成为强大便携式计算设备的目标迈进。2025年3月的更新中,Linux终端应用的推出为这一转变奠定了重要基础。该应用允许兼容的安卓设备...
- Linux 系统管理大容量磁盘(2TB+)操作指南
-
对于容量超过2TB的磁盘,传统MBR分区表的32位寻址机制存在限制(最大支持2.2TB)。需采用GPT(GUIDPartitionTable)分区方案,其支持64位寻址,理论上限为9.4ZB(9....
- Linux 服务器上查看磁盘类型的方法
-
方法1:使用lsblk命令lsblk输出说明:TYPE列显示设备类型,如disk(物理磁盘)、part(分区)、rom(只读存储)等。...
- ESXI7虚机上的Ubuntu Linux 22.04 LVM空间扩容操作记录
-
本人在实际的使用中经常遇到Vmware上安装的Linux虚机的LVM扩容情况,最终实现lv的扩容,大多数情况因为虚机都是有备用或者可停机的情况,一般情况下通过添加一块物理盘再加入vg,然后扩容lv来实...
- 5.4K Star很容易!Windows读取Linux磁盘格式工具
-
[开源日记],分享10k+Star的优质开源项目...
- Linux 文件系统监控:用脚本自动化磁盘空间管理
-
在Linux系统中,文件系统监控是一项非常重要的任务,它可以帮助我们及时发现磁盘空间不足的问题,避免因磁盘满而导致的系统服务不可用。通过编写脚本自动化磁盘空间管理,我们可以更加高效地处理这一问题。下面...
- Linux磁盘管理LVM实战(linux实验磁盘管理)
-
LVM(逻辑卷管理器,LogicalVolumeManager)是一种在Linux系统中用于灵活管理磁盘空间的技术,通过将物理磁盘抽象为逻辑卷,实现动态调整存储容量、跨磁盘扩展等功能。本章节...
- Linux查看文件大小:`ls`和`du`为何结果不同?一文讲透原理!
-
Linux查看文件大小:ls和du为何结果不同?一文讲透原理!在Linux运维中,查看文件大小是日常高频操作。但你是否遇到过以下困惑?...
- 使用 df 命令检查服务器磁盘满了,但用 du 命令发现实际小于磁盘容量
-
在Linux系统中,管理员或开发者经常会遇到一个令人困惑的问题:使用...
- Linux磁盘爆满紧急救援指南:5步清理释放50GB+小白也能轻松搞定
-
“服务器卡死?网站崩溃?当Linux系统弹出‘Nospaceleft’的红色警报,别慌!本文手把手教你从‘删库到跑路’进阶为‘磁盘清理大师’,5个关键步骤+30条救命命令,快速释放磁盘空间,拯救你...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)