- 这个Bug的排查之路,真的太有趣了
- 来源:ImportNew
(给ImportNew加星标,提高Java技能)转自:why技术邪乎到家必有鬼大家好呀,我是why哥。在《深入理解Java虚拟机》一书中有这样一段代码:
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
}).start();
}
//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(race);
}
}
你看到这段代码的第一反应是什么?是不是关注点都在 volatile 关键字上。甚至马上就要开始脱口而出:volatile 只保证可见性,不保证原子性。而代码中的 race++ 不是原子性的操作,巴拉巴拉巴拉...反正我就是这样的: 当他把代码发给我,我在 idea 里面一粘贴,然后把 main 方法运行起来后,神奇的事情出现了。
这个代码真的没有执行到输出语句,也没有任何报错。看起来就像是死循环了一样。不信的话,你也可以放到你的 idea 里面去执行一下。等等......死循环?代码里面不是就有一个死循环吗?//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
这段代码能有什么小心思呢?看起来人畜无害啊。但是程序员的直觉告诉我,这个地方就是有问题的。活跃线程一直是大于 1 的,所以导致 while 一直在死循环。算了,不想了,先 Debug 看一眼吧。Debug 了两遍之后,我才发现,这个事情,有点意思了。因为 Debug 的情况下,程序竟然正常结束了。 啥情况啊?分析一波走起。为啥停不下来?我是怎么分析这个问题的呢。
我就把程序又 Run 了起来,控制台还是啥输出都没有。我就盯着这个控制台想啊,会是啥原因呢?这样干看着也不是办法啊。反正我现在就是咬死这个 while 循环是有问题的,所以为了排除其他的干扰项。我把程序简化到了这个样子:public class VolatileTest {
public static volatile int race = 0;
public static void main(String[] args) {
while(Thread.activeCount()>1)
Thread.yield();
System.out.println("race = " + race);
}
}
运行起来之后,还是没有执行到输出语句,也就侧面证实了我的想法:while 循环有问题。而 while 循环的条件就是 Thread.activeCount()>1。朝着这个方向继续想下去,就是看看当前活跃线程到底有几个。于是程序又可以简化成这样: 直接运行看到输出结果是 2。 用 Debug 模式运行时返回的是 1。对比这运行结果,我心里基本上就有数了。先看一下这个 activeCount 方法是干啥的: 注意看画着下划线的地方:返回的值是一个 estimate。estimate 是啥? 你看,又在我这里学一个高级词汇。真是 very good。返回的是一个预估值。为什么呢?因为我们调用这个方法的一刻获取到值之后,线程数还是在动态变化的。也就是说返回的值只代表你调用的那一刻有几个活跃线程,也许当你调用完成后,有一个线程就立马嗝屁了。所以,这个值是个预估值。这一瞬间,我突然想到了量子力学中的测不准原理。 你不可能同时知道一个粒子的位置和它的速度,就像在多线程高并发的情况下你不可能同时知道调用 activeCount 方法得到的值和你要用这个值的时刻,这个值的真实值是多少。你看,刚学完英语又学量子力学。 好了,回到程序里面。虽然注释里面说了返回值是 estimate 的,但是在我们的程序中,并不存在这样的问题。看到 activeCount 方法的实现之后:public static int activeCount() {
return currentThread().getThreadGroup().activeCount();
}
我又想到,既然在直接 Run 的情况下,程序返回的数是 2,那我看看到底有哪些线程呢?其实最开始我想着去 Debug 一下的,但是 Debug 的情况下,返回的数是 1。我意识到,这个问题肯定和 idea 有关,而且必须得用日志调试大法才能知道原因。于是,我把程序改成了这样: 直接 Run 起来,可以看到,确实有两个线程。一个是 main 线程,我们熟悉。一个是 Monitor Ctrl-Break 线程,我不认识。但是当我用 Debug 的方式运行的时候,有意思的事情就发生了: Monitor Ctrl-Break 线程不见了!?于是,我问他: 是啊,问题解决了,但是啥原因啊?为什么 Run 不可以运行,而 Debug 可以运行呢? 当前线程有哪些?我们先梳理一下当前线程有哪些吧。
可以使用下面的代码获取当前所有的线程:public static Thread[] findAllThread(){
ThreadGroup currentGroup =Thread.currentThread().getThreadGroup();
while (currentGroup.getParent()!=null){
// 返回此线程组的父线程组
currentGroup=currentGroup.getParent();
}
//此线程组中活动线程的估计数
int noThreads = currentGroup.activeCount();
Thread[] lstThreads = new Thread[noThreads];
//把对此线程组中的所有活动子组的引用复制到指定数组中。
currentGroup.enumerate(lstThreads);
for (Thread thread : lstThreads) {
System.out.println("线程数量:"+noThreads+" " +
"线程id:" + thread.getId() +
" 线程名称:" + thread.getName() +
" 线程状态:" + thread.getState());
}
return lstThreads;
}
运行之后可以看到有 6 个线程: 也就是说,在 idea 里面,一个 main 方法 Run 起来之后,即使什么都不干,也会有 6 个线程运行。这 6 个线程分别是干啥的呢?我们一个个的说。Reference Handler 线程:JVM 在创建 main 线程后就创建 Reference Handler 线程,其优先级最高,为 10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题。Finalizer 线程:这个线程也是在 main 线程之后创建的,其优先级为 10,主要用于在垃圾收集前,调用对象的 finalize() 方法。
关于 Finalizer 线程的几点:
1)只有当开始一轮垃圾收集时,才会开始调用 finalize() 方法;因此并不是所有对象的 finalize() 方法都会被执行;
2)该线程也是 daemon 线程,因此如果虚拟机中没有其他非 daemon 线程,不管该线程有没有执行完 finalize() 方法,JVM 也会退出;
3) JVM 在垃圾收集时会将失去引用的对象包装成 Finalizer 对象(Reference的实现),并放入 ReferenceQueue,由 Finalizer 线程来处理;最后将该 Finalizer 对象的引用置为 null,由垃圾收集器来回收;
4) JVM 为什么要单独用一个线程来执行 finalize() 方法呢?如果 JVM 的垃圾收集线程自己来做,很有可能由于在 finalize() 方法中误操作导致 GC 线程停止或不可控,这对 GC 线程来说是一种灾难。Attach Listener 线程:Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求 jvm 给我们一些反馈信息。
如:java -version、jmap、jstack 等等。如果该线程在 jvm 启动的时候没有初始化,那么,则会在用户第一次执行 jvm 命令时,得到启动。Signal Dispatcher 线程:前面我们提到第一个 Attach Listener 线程的职责是接收外部 jvm 命令,当命令接收成功后,会交给 signal dispather 线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather 线程也是在第一次接收外部 jvm 命令时,进行初始化工作。main 线程:呃,这个不说了吧。大家都知道。Monitor Ctrl-Break 线程:先卖个关子,下一小节专门聊聊这个线程。上面线程的作用,我是从这个网页搬运过来的,还有很多其他的线程,大家可以去看看:http://ifeve.com/jvm-thread/我好事做到底,直接给你来个长截图,一网打尽。你先把图片保存起来,后面慢慢看: 现在跟着我去探寻 Monitor Ctrl-Break 线程的秘密。继续挖掘问题解决了,但是问题背后的问题,还没有得到解决。
Monitor Ctrl-Break 线程是啥?它是怎么来的?我们先 jstack 一把看看线程堆栈呗。而在 idea 里面,这里的“照相机”图标,就是 jstack 一样的功能。 我把程序恢复为最初的样子,然后把“照相机”就这么轻轻地一点: 从线程堆栈里面可以看到 Monitor Ctrl-Break 线程来自于这个地方:com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)而这个地方,一看名称,是 idea 的源码了啊?不属于我们的项目里面了,这咋个搞呢?思考了一下,想到了一种可能,于是我决定用 jps 命令验证一下: 看到执行结果的时候我笑了,一切就说得通了。果然,是用了 -javaagent 啊。那么 javaagent 是什么?你在命令行执行 java 命令,会输出一大串东西,其中就包含这个: 什么语言代理的,看不懂。叫我们参阅 java.lang.instrument。那它又是拿来干啥的?简单的一句话解释就是:使用 instrument 可以更加方便地使用字节码增强的技术,可以认为是一种 jvm 层面的截面。不需要对程序源代码进行任何侵入,就可以对其进行增强或者修改。总之,有点 AOP 内味。而 -javaagent 命令后面需要紧跟一个 jar 包。-javaagent:<jar 路径>[=<选项>]
instrument 机制要求,这个 jar 包必须有 MANIFEST.MF 文件,而 MANIFEST.MF 文件里面必须有 Premain-Class 这个东西。所以,回到我们的程序中,看一下 javaagent 后面跟的包是什么。在哪看呢?就这个地方: 你把它点开,命令非常的长。但是我们关心的 -javaagent 就在最开始的地方: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.3.4\lib\idea_rt.jar=61960可以看到,后面跟着的 jar 包是 idea_rt,按照文件目录找过去,也就是在这里: 我们解压这个 jar 包,打开它的 MANIFEST.MF 文件: 而这个类,不就是我们要找的它吗: 此时此刻,我们距离真相,只有一步之遥了。进到对应的包里,发现有三个 class 类: 主要关注 AppMainV2.class 文件: 在这个文件里面,就有一个 startMonitor 方法: 我说过什么来着?来,大声地跟我念一遍:源码之下无秘密。Monitor Ctrl-Break 线程就是这里来的。而仔细看一眼这里的代码,这个线程在干啥事呢?Socket client = new Socket("127.0.0.1", portNumber);啊,我的天呐,来看看这个可爱的小东西,socket 编程,太熟悉了,简直是梦回大学实验课的时候。它是链接到 127.0.0.1 的某个端口上,然后 while(true) 死循环等待接收命令。那么这个端口是哪个端口呢?就是这里的 62325: 需要注意的是,这个端口并不是固定的,每次启动这个端口都会变化。玩玩它既然它是 Socket 编程,那么我就玩玩它呗。
先搞个程序:public class SocketTest{
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("等待客户端连接.");
Socket socket = serverSocket.accept();
System.out.println("有客户端连接上了 "+ socket.getInetAddress() + ":" + socket.getPort() +"");
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true)
{
System.out.println("请输入指令: ");
String s = scanner.nextLine();
String message = s + "\n";
outputStream.write(message.getBytes("US-ASCII"));
}
}
}
我们把服务端的端口指定为了 12345。客户端这边的端口也得指定为 12345,那怎么指定呢?别想复杂了,简单的一比。把这行日志粘贴出来: 需要说明的是,我这边为了演示效果,在程序里面加了一个 for 循环。然后我们在这里把端口改为 12345: 把文件保存为 start.bat 文件,随便放一个地方。万事俱备。我们先把服务端运行起来: 然后,执行 bat 文件: 在 cmd 窗口里面输出了我们的日志,说明程序正常运行。而在服务端这边,显示有客户端连接成功。叫我们输入指令。输入啥指令呢?看一下客户端支持哪些指令呗: 可以看到,支持 STOP 命令。接受到该命令后,会退出程序。来,搞一波,动图走起: 搞定。
好了,本文技术部分就到这里了,恭喜你知道了 idea 中的 Monitor Ctrl-Break 线程,这个学了没啥卵用的知识 。如果要深挖的话,往 -javaagent 方向挖一挖。应用很多的,比如耳熟能详的 Java 诊断工具 Arthas 就是基于 JavaAgent 做的。有点意思。- EOF -推荐阅读 点击标题可跳转记一次找因 Redis 使用不当导致的应用卡死 Bug 过程写代码有这16个好习惯,可以减少80%非业务的bug
我给Apache顶级项目提了个Bug
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能 点赞和在看就是最大的支持❤️
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; i++){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
increase();
}
}
}).start();
}
//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
System.out.println(race);
}
}
你看到这段代码的第一反应是什么?是不是关注点都在 volatile 关键字上。甚至马上就要开始脱口而出:volatile 只保证可见性,不保证原子性。而代码中的 race++ 不是原子性的操作,巴拉巴拉巴拉...反正我就是这样的:
这个代码真的没有执行到输出语句,也没有任何报错。看起来就像是死循环了一样。不信的话,你也可以放到你的 idea 里面去执行一下。等等......死循环?代码里面不是就有一个死循环吗?//等待所有累加线程都结束
while(Thread.activeCount()>1)
Thread.yield();
这段代码能有什么小心思呢?看起来人畜无害啊。但是程序员的直觉告诉我,这个地方就是有问题的。活跃线程一直是大于 1 的,所以导致 while 一直在死循环。算了,不想了,先 Debug 看一眼吧。Debug 了两遍之后,我才发现,这个事情,有点意思了。因为 Debug 的情况下,程序竟然正常结束了。
我就把程序又 Run 了起来,控制台还是啥输出都没有。我就盯着这个控制台想啊,会是啥原因呢?这样干看着也不是办法啊。反正我现在就是咬死这个 while 循环是有问题的,所以为了排除其他的干扰项。我把程序简化到了这个样子:public class VolatileTest {
public static volatile int race = 0;
public static void main(String[] args) {
while(Thread.activeCount()>1)
Thread.yield();
System.out.println("race = " + race);
}
}
运行起来之后,还是没有执行到输出语句,也就侧面证实了我的想法:while 循环有问题。而 while 循环的条件就是 Thread.activeCount()>1。朝着这个方向继续想下去,就是看看当前活跃线程到底有几个。于是程序又可以简化成这样:
return currentThread().getThreadGroup().activeCount();
}
我又想到,既然在直接 Run 的情况下,程序返回的数是 2,那我看看到底有哪些线程呢?其实最开始我想着去 Debug 一下的,但是 Debug 的情况下,返回的数是 1。我意识到,这个问题肯定和 idea 有关,而且必须得用日志调试大法才能知道原因。于是,我把程序改成了这样:
可以使用下面的代码获取当前所有的线程:public static Thread[] findAllThread(){
ThreadGroup currentGroup =Thread.currentThread().getThreadGroup();
while (currentGroup.getParent()!=null){
// 返回此线程组的父线程组
currentGroup=currentGroup.getParent();
}
//此线程组中活动线程的估计数
int noThreads = currentGroup.activeCount();
Thread[] lstThreads = new Thread[noThreads];
//把对此线程组中的所有活动子组的引用复制到指定数组中。
currentGroup.enumerate(lstThreads);
for (Thread thread : lstThreads) {
System.out.println("线程数量:"+noThreads+" " +
"线程id:" + thread.getId() +
" 线程名称:" + thread.getName() +
" 线程状态:" + thread.getState());
}
return lstThreads;
}
运行之后可以看到有 6 个线程:
关于 Finalizer 线程的几点:
1)只有当开始一轮垃圾收集时,才会开始调用 finalize() 方法;因此并不是所有对象的 finalize() 方法都会被执行;
2)该线程也是 daemon 线程,因此如果虚拟机中没有其他非 daemon 线程,不管该线程有没有执行完 finalize() 方法,JVM 也会退出;
3) JVM 在垃圾收集时会将失去引用的对象包装成 Finalizer 对象(Reference的实现),并放入 ReferenceQueue,由 Finalizer 线程来处理;最后将该 Finalizer 对象的引用置为 null,由垃圾收集器来回收;
4) JVM 为什么要单独用一个线程来执行 finalize() 方法呢?如果 JVM 的垃圾收集线程自己来做,很有可能由于在 finalize() 方法中误操作导致 GC 线程停止或不可控,这对 GC 线程来说是一种灾难。Attach Listener 线程:Attach Listener 线程是负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者。通常我们会用一些命令去要求 jvm 给我们一些反馈信息。
如:java -version、jmap、jstack 等等。如果该线程在 jvm 启动的时候没有初始化,那么,则会在用户第一次执行 jvm 命令时,得到启动。Signal Dispatcher 线程:前面我们提到第一个 Attach Listener 线程的职责是接收外部 jvm 命令,当命令接收成功后,会交给 signal dispather 线程去进行分发到各个不同的模块处理命令,并且返回处理结果。signal dispather 线程也是在第一次接收外部 jvm 命令时,进行初始化工作。main 线程:呃,这个不说了吧。大家都知道。Monitor Ctrl-Break 线程:先卖个关子,下一小节专门聊聊这个线程。上面线程的作用,我是从这个网页搬运过来的,还有很多其他的线程,大家可以去看看:http://ifeve.com/jvm-thread/我好事做到底,直接给你来个长截图,一网打尽。你先把图片保存起来,后面慢慢看:
Monitor Ctrl-Break 线程是啥?它是怎么来的?我们先 jstack 一把看看线程堆栈呗。而在 idea 里面,这里的“照相机”图标,就是 jstack 一样的功能。
instrument 机制要求,这个 jar 包必须有 MANIFEST.MF 文件,而 MANIFEST.MF 文件里面必须有 Premain-Class 这个东西。所以,回到我们的程序中,看一下 javaagent 后面跟的包是什么。在哪看呢?就这个地方:
先搞个程序:public class SocketTest{
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(12345);
System.out.println("等待客户端连接.");
Socket socket = serverSocket.accept();
System.out.println("有客户端连接上了 "+ socket.getInetAddress() + ":" + socket.getPort() +"");
OutputStream outputStream = socket.getOutputStream();
Scanner scanner = new Scanner(System.in);
while (true)
{
System.out.println("请输入指令: ");
String s = scanner.nextLine();
String message = s + "\n";
outputStream.write(message.getBytes("US-ASCII"));
}
}
}
我们把服务端的端口指定为了 12345。客户端这边的端口也得指定为 12345,那怎么指定呢?别想复杂了,简单的一比。把这行日志粘贴出来:
好了,本文技术部分就到这里了,恭喜你知道了 idea 中的 Monitor Ctrl-Break 线程,这个学了没啥卵用的知识 。如果要深挖的话,往 -javaagent 方向挖一挖。应用很多的,比如耳熟能详的 Java 诊断工具 Arthas 就是基于 JavaAgent 做的。有点意思。- EOF -推荐阅读 点击标题可跳转记一次找因 Redis 使用不当导致的应用卡死 Bug 过程写代码有这16个好习惯,可以减少80%非业务的bug
我给Apache顶级项目提了个Bug
看完本文有收获?请转发分享给更多人
关注「ImportNew」,提升Java技能
时尚
-
-
- 图案专题|2021/2022 新传统主义印花趋势(三)
- 爱我你就按住我这是Jane私人微信,长按可以加我为好友万一通过了呢美丽的事物总是让人心旷神怡,将其抽象成图案着于身上,更是能轻易彰显自身的独特品味和魅力。之前栢哥已经跟大家分享了一些新传统主义印花&图...
- 艺栢图案设计
-
-
-
- 这五件珍珠也许是这个星球上最出名的
- NO1.“漫游者珍珠”“漫游者珍珠”的拥有者,是好莱坞鼎鼎大名的女演员伊丽莎白·泰勒。2011年12月,佳士得以1180万美元,售出了配以红宝石、钻石的“漫游者珍珠(La Peregrina Pearl)”项链,成为前史上最高价...
- 潘家园
-
-
-
- 能睡好觉的莫代尔居家睡衣!超柔软透气面料,慵懒的自在体验,居家安心放松~
- 经过一天劳累的工作什么购物、派对、见朋友通通被本社畜抛到九霄云外只想立刻回到家里甩掉穿了一天的工作服装马上换上睡衣!!!!那一刻我被禁锢了一整天的灵魂终于获得了自由!!穿上舒适的睡衣在家里瘫成咸鱼...
- 仙贝旅行
-
-
-
- 薛佳凝心态真好!没有修图黑眼圈眼袋很显老,但整个人很自信!
- 当初薛佳凝主演的电影《人见人爱》上映,她在片中饰演女大学生申玲玲,同时正式进入演艺圈,当时在人气上还没这么高,但是她的名字大家还是非常多人听说过的,而现在已经42岁的她在年龄上也不小了,但是薛佳凝心...
- 服装搭配技巧
-
-
-
- 张钧甯撕掉小清新标签,穿着一身洋气装扮秒变大女王!
- 张钧甯,说到这位气质美人,大家一定不陌生了吧。在前几年,张钧甯因出演了《咖啡》而凭借一张清新靓丽的面孔圈粉无数。向来温婉的张钧甯近日却撕掉了小清新标签,穿着一身洋气装扮的她秒变为大女王,不禁使网友...
- 要你穿得好看
-
-
-
- 今年春夏流行衣服穿“大一号”才好看!显瘦又百搭!
- 服饰与搭配公众号ID:fusdapei关注最近“Oversise”风又重新席卷而来了!毫无疑问,“穿大不穿小”的再一次火了。
- 服饰与搭配
-
-
-
- 倪妮、宋妍霏爱背的经典款 也太能打了吧!
- 如果说要让我选出一款近两年出新款最多的包包,那Gucci Jackie绝对可以排上Top3,这款包估计也是当下经典复刻款当中,最热门的包款。宋妍霏私服就将这款包的各种搭配方式都演绎了一遍,超A的oversized西装、休闲...
- 瑞丽up
-
-
-
- 2021日本春夏 上衣的八大流行趋势(上)
- 2021春夏怎么穿最时髦?关于上衣的八大流行元素已经发布,这些潮流单品你都入手了吗?如果还没有,那就挑几个自己喜欢的元素下手吧。壹背后蝴蝶结一枚蝴蝶结,恰似一缕清风,吹来典雅柔美的气息。蝴蝶结是女生热...
- 米娜
-
-
-
- 这个Bug的排查之路,真的太有趣了
- (给ImportNew加星标,提高Java技能)转自:why技术邪乎到家必有鬼大家好呀,我是why哥。在《深入理解Java虚拟机》一书中有这样一段代码:public class VolatileTest { public static vo
- ImportNew
-
-
-
- 营销成女神的刘涛,一放生图就成老大妈,素颜又黑又油难掩沧桑!
- 娱乐圈中虽然有靠颜值出道的女明星,但是也有既好看又有才华的小姐姐呢,就像刘涛不仅撑得起女王范段的起高贵风,而且各种风格切换毫无压力,也是不少网友心中的 白月光呀。不过女神这光环也是离不开各种营销呢...
- 搭配师
-
-
-
- 宋茜出席活动;李易峰、王一博、邓伦、INTO1成员现身机场;陈伟霆、景甜杂志封面
- 2021-05-03宋茜出席游戏直播活动绝美春日穿搭有李易峰北京出发飞往海口亲亲这位顺毛小仙草王一博南宁出发飞回北京全身广告位的男模走路带风给人帅到腿软邓伦长沙出发飞往上海绿色小包包里装满了可爱刘宇北京机场...
- 爱豆
-
-
-
- “挤奶衫+爸爸裤”巨显瘦!梨形身材一定要穿它!
- 粉丝速看,最近外卖补贴活动力度很大,如果你点外卖,只要关注公众号《外卖达人攻略》就可以每天领取大额隐藏补贴红包了!最高56元,小的也有4-10块,还有免费得外卖月卡活动!速度速度,,,手慢无~~服装搭配 |...
- 服装搭配技巧师
-
-
-
- How to be a「Miu」girl?|打造你的香水霓裳三部曲
- 都说香水是最后一件衣服,也就是说,香水是时尚完成的最后一步,Miu Miu女孩的世界里,也是如此。缤纷的印花,迷人的香水,时髦的衣服,高跟鞋已经提前通过免税店拥有了缪缪霓裳香水系列,赶紧来和高跟鞋一起探...
- 高跟鞋走地球
-
-
-
- 塞衣角”过时了,今年流行这几种穿法,时髦显瘦,明星都在穿
- 不知道大家发现没有,不知从什么时候开始"塞衣角"成为了大家穿衣搭配的一种基本搭配方法,它好像成为了你的穿搭是否时髦的一个评判标准。但是"塞衣角"它并没有大家想象中的那么神奇百搭。"塞衣角"整个腰腹部的位...
- 我爱街拍
-